diff --git a/BBSHTMLConfig.c b/BBSHTMLConfig.c
index e4c8248..d328419 100644
--- a/BBSHTMLConfig.c
+++ b/BBSHTMLConfig.c
@@ -477,6 +477,13 @@ void ProcessMailHTTPMessage(struct HTTPConnectionInfo * Session, char * Method,
}
+
+ if (_memicmp(URL, "/Mail/API/v1/", 13) == 0)
+ {
+ *RLen = MailAPIProcessHTTPMessage(Session, Reply, Method, URL, input, LOCAL, Context);
+ return;
+ }
+
// There is a problem if Mail is reloaded without reloading the node
if (GotFirstMessage == 0)
@@ -494,12 +501,6 @@ void ProcessMailHTTPMessage(struct HTTPConnectionInfo * Session, char * Method,
return;
}
- if (_memicmp(URL, "/Mail/API/v1/", 13) == 0)
- {
- *RLen = MailAPIProcessHTTPMessage(Session, Reply, Method, URL, input, LOCAL, Context);
- return;
- }
-
if (strcmp(Method, "POST") == 0)
{
diff --git a/mailapi.c b/mailapi.c
index 5ec43fb..9a19657 100644
--- a/mailapi.c
+++ b/mailapi.c
@@ -570,9 +570,9 @@ int sendFwdConfig(struct HTTPConnectionInfo * Session, char * response, char * R
int i = 0;
int Len = 0;
- n = sprintf(ptr,"{\"forwardconfig\":[\r\n");
- ptr += n;
-
+ response[n] = 0;
+
+
for (USER = BBSChain; USER; USER = USER->BBSNext)
{
struct BBSForwardingInfo * FWDInfo = USER->ForwardingInfo;
@@ -619,15 +619,10 @@ int sendFwdConfig(struct HTTPConnectionInfo * Session, char * response, char * R
ptr += sprintf(ptr, " }\r\n},\r\n");
}
- if (response[n] == 0) // No entries
- {
- response[strlen(response) - 2] = '\0'; // remove \r\n
- strcat(response, "]}\r\n");
- }
- else
+ if (response[n]) // No entries
{
response[strlen(response)-3 ] = '\0'; // remove ,\r\n
- strcat(response, "\r\n]}\r\n");
+ strcat(response, "\r\n");
}
return strlen(response);
diff --git a/mailapi.c.bak b/mailapi.c.bak
new file mode 100644
index 0000000..fa3d893
--- /dev/null
+++ b/mailapi.c.bak
@@ -0,0 +1,691 @@
+// basic JASON API to BPQ Node
+
+// Authentication is via Telnet USER records.
+
+
+#define _CRT_SECURE_NO_DEPRECATE
+#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
+
+//#include
+#include "CHeaders.h"
+#include
+#include "bpqmail.h"
+#include "httpconnectioninfo.h"
+
+struct MsgInfo * GetMsgFromNumber(int msgno);
+BOOL CheckUserMsg(struct MsgInfo * Msg, char * Call, BOOL SYSOP);
+char * doXMLTransparency(char * string);
+
+
+// Constants
+#define TOKEN_SIZE 32 // Length of the authentication token
+#define TOKEN_EXPIRATION 7200 // Token expiration time in seconds (2 hours)
+
+// Token data structure
+typedef struct MailToken {
+ char token[TOKEN_SIZE + 1];
+ time_t expiration_time;
+ struct UserInfo * User;
+ char Call[10];
+ int Auth; // Security level of user
+
+ struct MailToken* next;
+} MailToken;
+
+static MailToken * token_list = NULL;
+
+typedef struct MailAPI
+{
+ char *URL;
+ int URLLen;
+ int (* APIRoutine)();
+ int Auth;
+} MailAPI;
+
+// Auth defines
+
+#define AuthNone 0
+#define AuthUser 1
+#define AuthBBSUser 2
+#define AuthSysop 4
+
+
+static int verify_token(const char* token);
+static void remove_expired_tokens();
+static int request_token(char * response);
+static void add_token_to_list(MailToken* token);
+static MailToken * find_token(const char* token);
+
+int sendMsgList(struct HTTPConnectionInfo * Session, char * response, char * Rest, int Auth);
+int sendFwdQueueLen(struct HTTPConnectionInfo * Session, char * response, char * Rest, int Auth);
+int sendFwdConfig(struct HTTPConnectionInfo * Session, char * response, char * Rest, int Auth);
+
+static struct MailAPI APIList[] =
+{
+ "/mail/api/v1/msgs", 17, sendMsgList, AuthBBSUser | AuthSysop,
+ "/mail/api/v1/FwdQLen", 20, sendFwdQueueLen, AuthBBSUser | AuthSysop,
+ "/mail/api/v1/FwdConfig", 22, sendFwdConfig, AuthBBSUser | AuthSysop,
+};
+
+static int APICount = sizeof(APIList) / sizeof(struct MailAPI);
+
+#ifndef WIN32
+iconv_t * icu = NULL;
+#endif
+
+void APIConvertTitletoUTF8(char * Title, char * UTF8Title, int Len)
+{
+ if (WebIsUTF8(Title, (int)strlen(Title)) == FALSE)
+ {
+ // With Windows it is simple - convert using current codepage
+ // I think the only reliable way is to convert to unicode and back
+
+ int origlen = (int)strlen(Title) + 1;
+#ifdef WIN32
+ WCHAR BufferW[128];
+ int wlen;
+ int len = origlen;
+
+ wlen = MultiByteToWideChar(CP_ACP, 0, Title, len, BufferW, origlen * 2);
+ len = WideCharToMultiByte(CP_UTF8, 0, BufferW, wlen, UTF8Title, origlen * 2, NULL, NULL);
+#else
+ size_t left = Len - 1;
+ size_t len = origlen;
+
+ if (icu == NULL)
+ icu = iconv_open("UTF-8//IGNORE", "CP1252");
+
+ if (icu == (iconv_t)-1)
+ {
+ strcpy(UTF8Title, Title);
+ icu = NULL;
+ return;
+ }
+
+ char * orig = UTF8Title;
+
+ iconv(icu, NULL, NULL, NULL, NULL); // Reset State Machine
+ iconv(icu, &Title, &len, (char ** __restrict__)&UTF8Title, &left);
+
+#endif
+ }
+ else
+ strcpy(UTF8Title, Title);
+}
+
+static MailToken * generate_token()
+{
+ // Generate a random authentication token
+ int i;
+
+ MailToken * token = malloc(sizeof(MailToken));
+
+ srand(time(NULL));
+
+ for (i = 0; i < TOKEN_SIZE; i++)
+ {
+ token->token[i] = 'A' + rand() % 26; // Random uppercase alphabet character
+ }
+ token->token[TOKEN_SIZE] = '\0'; // Null-terminate the token
+ token->expiration_time = time(NULL) + TOKEN_EXPIRATION; // Set token expiration time
+ add_token_to_list(token);
+ return token;
+}
+
+// Function to add the token to the token_list
+static void add_token_to_list(MailToken * token)
+{
+ if (token_list == NULL)
+ {
+ token_list = token;
+ token->next = NULL;
+ }
+ else
+ {
+ MailToken * current = token_list;
+
+ while (current->next != NULL)
+ current = current->next;
+
+ current->next = token;
+ token->next = NULL;
+ }
+}
+
+static int verify_token(const char* token)
+{
+ // Find the token in the token list
+ MailToken * existing_token = find_token(token);
+
+ if (existing_token != NULL)
+ {
+ // Check if the token has expired
+ time_t current_time = time(NULL);
+ if (current_time > existing_token->expiration_time)
+ {
+ // Token has expired, remove it from the token list
+ remove_expired_tokens();
+ return 0;
+ }
+ // Token is valid
+ return 1;
+ }
+
+ // Token doesn't exist in the token list
+ return 0;
+}
+
+static void remove_expired_tokens()
+{
+ time_t current_time = time(NULL);
+ MailToken* current_token = token_list;
+ MailToken* prev_token = NULL;
+ MailToken* next_token;
+
+ while (current_token != NULL)
+ {
+ if (current_time > current_token->expiration_time)
+ {
+ // Token has expired, remove it from the token list
+ if (prev_token == NULL)
+ {
+ token_list = current_token->next;
+ } else {
+ prev_token->next = current_token->next;
+ }
+ next_token = current_token->next;
+ free(current_token);
+ current_token = next_token;
+ } else {
+ prev_token = current_token;
+ current_token = current_token->next;
+ }
+ }
+}
+
+static MailToken * find_token(const char* token)
+{
+ MailToken * current_token = token_list;
+ while (current_token != NULL)
+ {
+ if (strcmp(current_token->token, token) == 0)
+ {
+ return current_token;
+ }
+ current_token = current_token->next;
+ }
+ return NULL;
+}
+
+static int send_http_response(char * response, const char* msg)
+{
+ return sprintf(response, "HTTP/1.1 %s\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", msg);
+}
+
+
+int MailAPIProcessHTTPMessage(struct HTTPConnectionInfo * Session, char * response, char * Method, char * URL, char * request, BOOL LOCAL, char *Params)
+{
+ char * pass = strlop(Params, '&');
+ int Flags = 0, n;
+ MailToken * Token;
+ char Msg[64];
+ struct UserInfo * User;
+ int Auth = 0;
+
+ if (LOCAL)
+ Auth = AuthSysop;
+
+ // Check if the request is for token generation
+
+ if (strcmp(Method, "GET") != 0)
+ return send_http_response(response, "403 (Bad Method)");
+
+ if (_stricmp(URL, "/mail/api/v1/login") == 0)
+ {
+ // Key is in Session->Key
+
+ // Signon may have been validated in Node. If Session->Callsign is set
+
+ if (Session->Callsign[0] == 0)
+ {
+ // Try BBS logon
+
+ User = LookupCall(Params);
+
+ if (User)
+ {
+ // Check Password
+
+ if (pass[0] == 0 || strcmp(User->pass, pass) != 0 || User->flags & F_Excluded)
+ return send_http_response(response, "403 (Login Failed)");
+
+ strcpy(Session->Callsign, User->Call);
+ Auth = AuthBBSUser;
+ }
+ }
+ else
+ {
+ User = LookupCall(Session->Callsign);
+
+ if (User)
+ {
+ Auth = AuthUser;
+ if (User->flags & F_SYSOP)
+ Auth |= AuthSysop;
+ }
+ }
+
+ n = sprintf_s(Msg, sizeof(Msg), "API Connect from %s", _strupr(Params));
+ WriteLogLine(NULL, '|',Msg, n, LOG_BBS);
+
+ Token = zalloc(sizeof(MailToken));
+
+ strcpy(Token->token, Session->Key);
+ strcpy(Token->Call, Session->Callsign);
+ Token->Auth = Auth;
+
+ Token->expiration_time = time(NULL) + TOKEN_EXPIRATION; // Set token expiration time
+ add_token_to_list(Token);
+
+ // Return Token
+
+ sprintf(response, "{\"access_token\":\"%s\", \"expires_at\":%d, \"scope\":\"create\"}\r\n",
+ Token->token, Token->expiration_time);
+
+ return strlen(response);
+ }
+
+ // Determine the requested API endpoint
+
+ for (n = 0; n < APICount; n++)
+ {
+ struct MailAPI * APIEntry;
+ char * rest;
+
+ APIEntry = &APIList[n];
+
+ if (_memicmp(URL, APIEntry->URL, APIEntry->URLLen) == 0)
+ {
+ rest = &request[4 + APIEntry->URLLen]; // Anything following?
+
+ if (rest[0] =='?')
+ {
+ //Key
+
+ strlop(rest, ' ');
+ strlop(rest, '&');
+
+ Token = find_token(&rest[1]);
+
+ if (Token)
+ {
+ strcpy(Session->Callsign, Token->Call);
+ strcpy(Session->Key, Token->token);
+ }
+ else
+ return send_http_response(response, "403 (Invalid Security Token)");
+ }
+
+ if (APIEntry->Auth)
+ {
+ // Check Level
+ }
+
+ if (rest[0] == ' ' || rest[0] == '/' || rest[0] == '?')
+ {
+ return APIEntry->APIRoutine(Session, response, rest, Auth);
+ }
+ }
+
+ }
+
+ return send_http_response(response, "401 Invalid API Call");
+
+
+ return 0;
+}
+
+int WebMailAPIProcessHTTPMessage(char * response, char * Method, char * URL, char * request, BOOL LOCAL, char *Params)
+{
+ char * pass = strlop(Params, '&');
+ int Flags = 0;
+ MailToken * Token;
+
+
+ // Check if the request is for token generation
+
+ if (strcmp(Method, "GET") != 0)
+ return send_http_response(response, "403 (Bad Method)");
+
+ if (_stricmp(URL, "/mail/api/login") == 0)
+ {
+ // user is in Params and Password in pass
+
+ struct UserInfo * User;
+ char Msg[256];
+ int n;
+
+ User = LookupCall(Params);
+
+ if (User)
+ {
+ // Check Password
+
+ if (pass[0] == 0 || strcmp(User->pass, pass) != 0 || User->flags & F_Excluded)
+ return send_http_response(response, "403 (Login Failed)");
+
+ n=sprintf_s(Msg, sizeof(Msg), "API Connect from %s", _strupr(Params));
+ WriteLogLine(NULL, '|',Msg, n, LOG_BBS);
+
+ Token = generate_token();
+ add_token_to_list(Token);
+
+ Token->User = User;
+
+ strcpy(Token->Call, Params);
+
+ // Return Token
+
+ sprintf(response, "{\"access_token\":\"%s\", \"expires_in\":%d, \"scope\":\"create\"}\r\n",
+ Token->token, Token->expiration_time);
+
+ return strlen(response);
+
+ }
+ }
+
+ return 0;
+}
+
+// Unauthorised users can only get bulls.
+// Autothorised may read only users message or all messages depending on sysop status
+
+int sendMsgList(struct HTTPConnectionInfo * Session, char * response, char * Rest, int Auth)
+{
+ struct UserInfo * User = LookupCall(Session->Callsign);
+ int m;
+ struct MsgInfo * Msg;
+ char * ptr = response;
+ int n = NumberofMessages; //LineCount;
+ char Via[64];
+ int Count = 0;
+ struct UserInfo DummyUser = {""};
+ ptr[0] = 0;
+
+ if (User == 0)
+ User=&DummyUser;
+
+ n = sprintf(ptr,"{\"msgs\":[\r\n");
+ ptr += n;
+
+ for (m = LatestMsg; m >= 1; m--)
+ {
+ if (ptr > &response[244000])
+ break; // protect buffer
+
+ Msg = GetMsgFromNumber(m);
+
+ if (Msg == 0 || Msg->type == 0 || Msg->status == 0)
+ continue; // Protect against corrupt messages
+
+ if (Msg && CheckUserMsg(Msg, User->Call, Auth & AuthSysop))
+ {
+ char UTF8Title[4096];
+ char * EncodedTitle;
+
+ // List if it is the right type and in the page range we want
+
+
+ if (Count++ < Session->WebMailSkip)
+ continue;
+
+ ptr += sprintf(ptr, "{\r\n");
+
+
+ strcpy(Via, Msg->via);
+ strlop(Via, '.');
+
+ // make sure title is HTML safe (no < > etc) and UTF 8 encoded
+
+ EncodedTitle = doXMLTransparency(Msg->title);
+
+ memset(UTF8Title, 0, 4096); // In case convert fails part way through
+ APIConvertTitletoUTF8(EncodedTitle, UTF8Title, 4095);
+
+ ptr += sprintf(ptr, "\"id\": \"%d\",\r\n", Msg->number);
+ ptr += sprintf(ptr, "\"mid\": \"%s\",\r\n", Msg->bid);
+ ptr += sprintf(ptr, "\"rcvd\": \"%d\",\r\n", Msg->datecreated);
+ ptr += sprintf(ptr, "\"type\": \"%c\",\r\n", Msg->type);
+ ptr += sprintf(ptr, "\"status\": \"%c\",\r\n", Msg->status);
+ ptr += sprintf(ptr, "\"to\": \"%s\",\r\n", Msg->to);
+ ptr += sprintf(ptr, "\"from\": \"%s\",\r\n", Msg->from);
+ ptr += sprintf(ptr, "\"size\": \"%d\",\r\n", Msg->length);
+ ptr += sprintf(ptr, "\"subject\": \"%s\"\r\n", UTF8Title);
+
+ free(EncodedTitle);
+
+ // ptr += sprintf(ptr, "%6d %s %c%c %5d %-8s%-8s%-8s%s\r\n",
+ // Session->Key, Msg->number, Msg->number,
+ // FormatDateAndTime((time_t)Msg->datecreated, TRUE), Msg->type,
+ // Msg->status, Msg->length, Msg->to, Via,
+ // Msg->from, UTF8Title);
+
+ ptr += sprintf(ptr, "},\r\n");
+
+ n--;
+
+ if (n == 0)
+ break;
+ }
+ }
+
+ if (response[n] == 0) // No entries
+ {
+ response[strlen(response) - 2] = '\0'; // remove \r\n
+ strcat(response, "]}\r\n");
+ }
+ else
+ {
+ response[strlen(response)-3 ] = '\0'; // remove ,\r\n
+ strcat(response, "\r\n]}\r\n");
+ }
+ return strlen(response);
+}
+
+int sendFwdQueueLen(struct HTTPConnectionInfo * Session, char * response, char * Rest, int Auth)
+{
+ struct UserInfo * USER;
+ char * ptr = response;
+ int n;
+ int i = 0;
+ int Len = 0;
+
+ n = sprintf(ptr,"{\"forwardqueuelength\":[\r\n");
+ ptr += n;
+
+ for (USER = BBSChain; USER; USER = USER->BBSNext)
+ {
+ int Count = CountMessagestoForward (USER);
+
+ ptr += sprintf(ptr, "{");
+ ptr += sprintf(ptr, "\"call\": \"%s\",", USER->Call);
+ ptr += sprintf(ptr, "\"queueLength\": \"%d\"", Count);
+ ptr += sprintf(ptr, "},\r\n");
+ }
+
+ if (response[n] == 0) // No entries
+ {
+ response[strlen(response) - 2] = '\0'; // remove \r\n
+ strcat(response, "]}\r\n");
+ }
+ else
+ {
+ response[strlen(response)-3 ] = '\0'; // remove ,\r\n
+ strcat(response, "\r\n]}\r\n");
+ }
+ return strlen(response);
+}
+
+VOID APIMultiStringValue(char ** values, char * Multi)
+{
+ char ** Calls;
+ char * ptr = &Multi[0];
+
+ *ptr = 0;
+
+ if (values)
+ {
+ Calls = values;
+
+ while(Calls[0])
+ {
+ ptr += sprintf(ptr, "\"%s\",", Calls[0]);
+ Calls++;
+ }
+ if (ptr != &Multi[0])
+ *(--ptr) = 0;
+ }
+}
+
+char * APIConvTime(int ss)
+{
+ int hh, mm;
+ static char timebuf[64];
+
+ hh = ss / 3600;
+ mm = (ss - (hh * 3600)) / 60;
+ ss = ss % 60;
+
+ sprintf(timebuf, "\"%02d:%02d:%02d\"", hh, mm, ss);
+
+ return timebuf;
+}
+
+
+int sendFwdConfig(struct HTTPConnectionInfo * Session, char * response, char * Rest, int Auth)
+{
+ struct UserInfo * USER;
+ char * ptr = response;
+ int n = 0;
+ int i = 0;
+ int Len = 0;
+
+
+ for (USER = BBSChain; USER; USER = USER->BBSNext)
+ {
+ struct BBSForwardingInfo * FWDInfo = USER->ForwardingInfo;
+
+ int Count = CountMessagestoForward (USER);
+
+ char TO[2048] = "";
+ char AT[2048] = "";
+ char TIMES[2048] = "";
+ char FWD[100000] = "";
+ char HRB[2048] = "";
+ char HRP[2048] = "";
+
+ APIMultiStringValue(FWDInfo->TOCalls, TO);
+ APIMultiStringValue(FWDInfo->ATCalls, AT);
+ APIMultiStringValue(FWDInfo->FWDTimes, TIMES);
+ APIMultiStringValue(FWDInfo->ConnectScript, FWD);
+ APIMultiStringValue(FWDInfo->Haddresses, HRB);
+ APIMultiStringValue(FWDInfo->HaddressesP, HRP);
+
+
+ ptr += sprintf(ptr, "{\r\n");
+ ptr += sprintf(ptr, " \"%s\": {\r\n", USER->Call);
+ ptr += sprintf(ptr, " \"queueLength\": %d,\r\n", Count);
+ ptr += sprintf(ptr, " \"to\": [%s],\r\n", TO);
+ ptr += sprintf(ptr, " \"at\": [%s],\r\n", AT);
+ ptr += sprintf(ptr, " \"hrp\": [%s],\r\n",HRP);
+ ptr += sprintf(ptr, " \"hrb\": [%s],\r\n",HRB);
+ ptr += sprintf(ptr, " \"times\": [%s],\r\n",TIMES);
+ ptr += sprintf(ptr, " \"connectScript\": [%s],\r\n",FWD);
+ ptr += sprintf(ptr, " \"bbsHa\": \"%s\",\r\n", (FWDInfo->BBSHA)?FWDInfo->BBSHA:"");
+ ptr += sprintf(ptr, " \"enableForwarding\": %s,\r\n", (FWDInfo->Enabled)?"true":"false");
+ ptr += sprintf(ptr, " \"forwardingInterval\": %s,\r\n", APIConvTime(FWDInfo->FwdInterval));
+ ptr += sprintf(ptr, " \"requestReverse\": %s,\r\n", (FWDInfo->ReverseFlag)?"true":"false");
+ ptr += sprintf(ptr, " \"reverseInterval\": %s,\r\n", APIConvTime(FWDInfo->RevFwdInterval));
+ ptr += sprintf(ptr, " \"sendNewMessagesWithoutWaiting\": %s,\r\n", (FWDInfo->SendNew)?"true":"false");
+ ptr += sprintf(ptr, " \"fbbBlocked\": %s,\r\n", (FWDInfo->AllowBlocked)?"true":"false");
+ ptr += sprintf(ptr, " \"maxBlock\": %d,\r\n", FWDInfo->MaxFBBBlockSize);
+ ptr += sprintf(ptr, " \"sendPersonalMailOnly\": %s,\r\n", (FWDInfo->PersonalOnly)?"true":"false");
+ ptr += sprintf(ptr, " \"allowBinary\": %s,\r\n", (FWDInfo->AllowCompressed)?"true":"false");
+ ptr += sprintf(ptr, " \"useB1Protocol\": %s,\r\n", (FWDInfo->AllowB1)?"true":"false");
+ ptr += sprintf(ptr, " \"useB2Protocol\": %s,\r\n", (FWDInfo->AllowB2)?"true":"false");
+ ptr += sprintf(ptr, " \"incomingConnectTimeout\": %s\r\n", APIConvTime(FWDInfo->ConTimeout));
+ ptr += sprintf(ptr, " }\r\n},\r\n");
+ }
+
+ if (response[n]) // No entries
+ {
+ response[strlen(response)-3 ] = '\0'; // remove ,\r\n
+ strcat(response, "\r\n]}\r\n");
+ }
+
+ return strlen(response);
+}
+
+
+
+/*
+{
+ "GB7BEX": {
+ "queueLength": 0,
+ "forwardingConfig": {
+ "to": [],
+ "at": [
+ "OARC",
+ "GBR",
+ "WW"
+ ],
+ "times": [],
+ "connectScript": [
+ "PAUSE 3",
+ "INTERLOCK 3",
+ "NC 3 !GB7BEX"
+ ],
+ "hierarchicalRoutes": [],
+ "hr": [
+ "#38.GBR.EURO"
+ ],
+ "bbsHa": "GB7BEX.#38.GBR.EURO",
+ "enableForwarding": true,
+ "forwardingInterval": "00:56:40",
+ "requestReverse": false,
+ "reverseInterval": "00:56:40",
+ "sendNewMessagesWithoutWaiting": true,
+ "fbbBlocked": true,
+ "maxBlock": 1000,
+ "sendPersonalMailOnly": false,
+ "allowBinary": true,
+ "useB1Protocol": false,
+ "useB2Protocol": true,
+ "sendCtrlZInsteadOfEx": false,
+ "incomingConnectTimeout": "00:02:00"
+ }
+ },
+ "GB7RDG": {
+ "queueLength": 0,
+ "forwardingConfig": {
+ "to": [],
+...
+ "incomingConnectTimeout": "00:02:00"
+ }
+ }
+}
+
+
+
+# HELP packetmail_queue_length The number of messages in the packetmail queue
+# TYPE packetmail_queue_length gauge
+packetmail_queue_length{partner="DM4RW"} 0 1729090716916
+packetmail_queue_length{partner="G8BPQ"} 3 1729090716916
+packetmail_queue_length{partner="GB7BEX"} 0 1729090716916
+packetmail_queue_length{partner="GB7BPQ"} 1 1729090716916
+packetmail_queue_length{partner="GB7MNS"} 0 1729090716916
+packetmail_queue_length{partner="GB7NOT"} 0 1729090716916
+packetmail_queue_length{partner="GB7NWL"} 0 1729090716916
+packetmail_queue_length{partner="GM8BPQ"} 0 1729090716916
+
+*/
\ No newline at end of file