From 4671b9b8d5e66524de96f07239671db504a6e02a Mon Sep 17 00:00:00 2001 From: Wilfried Goesgens Date: Mon, 10 Jan 2011 23:38:36 +0100 Subject: [PATCH] Libev / libc-ares Migration - Bounce-o-matic: - use parsend Queue structure instead of parsing the queue message ourselves - only Compose the bounce message, if we're shure we'll need it - malloc all memory first, and fail if.. - restructure Message Queue documentation - parse not yet identified Submitted-Header into queue item - Alloc QueueItem->StatusMessage on creation so we have it easier writing it later - Start implementing timeout/abort conditions - c-ares integration: start/stop the right event-watchers - c-ares integration: give it its own watcher structs --- citadel/event_client.c | 50 ++++-- citadel/event_client.h | 14 +- citadel/modules/c-ares-dns/serv_c-ares-dns.c | 23 ++- citadel/modules/smtp/serv_smtpeventclient.c | 20 ++- citadel/modules/smtp/serv_smtpqueue.c | 178 ++++++++++++++++++- citadel/modules/smtp/smtpqueue.h | 12 ++ citadel/techdoc/delivery-list.txt | 21 +-- 7 files changed, 280 insertions(+), 38 deletions(-) diff --git a/citadel/event_client.c b/citadel/event_client.c index 1d1ca925c..7cbecd6e2 100644 --- a/citadel/event_client.c +++ b/citadel/event_client.c @@ -238,8 +238,7 @@ IO_send_callback(struct ev_loop *loop, ev_io *watcher, int revents) rc = StrBuf_write_one_chunk_callback(watcher->fd, 0/*TODO*/, &IO->SendBuf); if (rc == 0) - { - + { #ifdef BIGBAD_IODBG { int rv = 0; @@ -298,13 +297,25 @@ IO_send_callback(struct ev_loop *loop, ev_io *watcher, int revents) break; } } - else if (rc > 0) - ShutDownCLient(IO); -// else - ///abort! + else if (rc < 0) + IO->Timeout(IO); + /* else : must write more. */ } +static void +IO_Timout_callback(struct ev_loop *loop, ev_timer *watcher, int revents) +{ + AsyncIO *IO = watcher->data; + + IO->Timeout(IO); +} +static void +IO_connfail_callback(struct ev_loop *loop, ev_timer *watcher, int revents) +{ + AsyncIO *IO = watcher->data; + IO->ConnFail(IO); +} static void IO_recv_callback(struct ev_loop *loop, ev_io *watcher, int revents) { @@ -326,7 +337,7 @@ IO_recv_callback(struct ev_loop *loop, ev_io *watcher, int revents) if (nbytes > 0) { HandleInbound(IO); } else if (nbytes == 0) { - ShutDownCLient(IO); + IO->Timeout(IO); /// TODO: this is a timeout??? sock_buff_invoke_free(sb, 0); seems as if socket is gone then? return; } else if (nbytes == -1) { @@ -337,8 +348,13 @@ IO_recv_callback(struct ev_loop *loop, ev_io *watcher, int revents) static void -set_start_callback(struct ev_loop *loop, AsyncIO *IO, int revents) +set_start_callback(struct ev_loop *loop, AsyncIO *IO, int revents, int first_rw_timeout) { + ev_timer_init(&IO->conn_timeout, + IO_Timout_callback, + first_rw_timeout, 0); + IO->conn_timeout.data = IO; + switch(IO->NextState) { case eReadMessage: ev_io_start(event_base, &IO->recv_event); @@ -355,7 +371,7 @@ set_start_callback(struct ev_loop *loop, AsyncIO *IO, int revents) } } -int event_connect_socket(AsyncIO *IO) +int event_connect_socket(AsyncIO *IO, int conn_timeout, int first_rw_timeout) { struct sockaddr_in saddr; int fdflags; @@ -413,9 +429,17 @@ IO->curr_ai->ai_family, /// TODO: ipv6?? (IO->HEnt->h_addrtype == AF_INET6)? /* sizeof(in6_addr):*/ sizeof(struct sockaddr_in)); - if ((rc >= 0) || (errno == EINPROGRESS)){ + if (rc >= 0){ //// freeaddrinfo(res); - set_start_callback(event_base, IO, 0); + set_start_callback(event_base, IO, 0, first_rw_timeout); + return 0; + } + else if (errno == EINPROGRESS) { + set_start_callback(event_base, IO, 0, first_rw_timeout); + ev_timer_init(&IO->conn_fail, + IO_connfail_callback, + conn_timeout, 0); + IO->conn_fail.data = IO; return 0; } @@ -439,6 +463,8 @@ void InitEventIO(AsyncIO *IO, IO_CallBack Timeout, IO_CallBack ConnFail, IO_LineReaderCallback LineReader, + int conn_timeout, + int first_rw_timeout, int ReadFirst) { IO->Data = pData; @@ -455,7 +481,7 @@ void InitEventIO(AsyncIO *IO, } IO->IP6 = IO->HEnt->h_addrtype == AF_INET6; // IO->res = HEnt->h_addr_list[0]; - event_connect_socket(IO); + event_connect_socket(IO, conn_timeout, first_rw_timeout); } diff --git a/citadel/event_client.h b/citadel/event_client.h index f9cf037b5..35ce4246e 100644 --- a/citadel/event_client.h +++ b/citadel/event_client.h @@ -36,10 +36,11 @@ struct AsyncIO { unsigned short dport; int active_event; eNextState NextState; + + ev_timer conn_fail, + conn_timeout; ev_io recv_event, - send_event, - dns_recv_event, - dns_send_event; + send_event; StrBuf *ErrMsg; /* if we fail to connect, or lookup, error goes here. */ /* read/send related... */ @@ -56,10 +57,16 @@ struct AsyncIO { IO_LineReaderCallback LineReader; /* if we have linereaders, maybe we want to read more lines before the real application logic is called? */ + + int active_dns_event; + ev_io dns_recv_event, + dns_send_event; struct ares_options DNSOptions; ares_channel DNSChannel; + ParseDNSAnswerCb DNS_CB; IO_CallBack PostDNS; + int DNSStatus; void *VParsedDNSReply; FreeDNSReply DNSReplyFree; @@ -87,6 +94,7 @@ void InitEventIO(AsyncIO *IO, IO_CallBack Timeout, IO_CallBack ConnFail, IO_LineReaderCallback LineReader, + int conn_timeout, int first_rw_timeout, int ReadFirst); int QueueQuery(ns_type Type, char *name, AsyncIO *IO, IO_CallBack PostDNS); diff --git a/citadel/modules/c-ares-dns/serv_c-ares-dns.c b/citadel/modules/c-ares-dns/serv_c-ares-dns.c index 9cd52b9af..6c1be1f16 100644 --- a/citadel/modules/c-ares-dns/serv_c-ares-dns.c +++ b/citadel/modules/c-ares-dns/serv_c-ares-dns.c @@ -315,21 +315,27 @@ void SockStateCb(void *data, int sock, int read, int write) if (read) { if ((IO->dns_recv_event.fd != sock) && - (IO->dns_recv_event.fd != 0)) { + (IO->dns_recv_event.fd != 0) && + ((IO->active_dns_event & EV_READ) != 0)) { ev_io_stop(event_base, &IO->dns_recv_event); } IO->dns_recv_event.fd = sock; ev_io_init(&IO->dns_recv_event, DNS_recv_callback, IO->dns_recv_event.fd, EV_READ); IO->dns_recv_event.data = IO; - - } else if (write) { + ev_io_start(event_base, &IO->dns_recv_event); + IO->active_dns_event = IO->active_dns_event | EV_READ; + } + if (write) { if ((IO->dns_send_event.fd != sock) && - (IO->dns_send_event.fd != 0)) { + (IO->dns_send_event.fd != 0) && + ((IO->active_dns_event & EV_WRITE) != 0)) { ev_io_stop(event_base, &IO->dns_send_event); } IO->dns_send_event.fd = sock; ev_io_init(&IO->dns_send_event, DNS_send_callback, IO->dns_send_event.fd, EV_WRITE); IO->dns_send_event.data = IO; + ev_io_start(event_base, &IO->dns_send_event); + IO->active_dns_event = IO->active_dns_event | EV_WRITE; } /* @@ -340,9 +346,14 @@ void SockStateCb(void *data, int sock, int read, int write) ret = ares_timeout(IO->DNSChannel, &maxtv, &tvbuf); } - if ((read == 0) && (write == 0)) { -// ev_io_stop(event_base, &IO->dns_io_event); */ + if ((read == 0) && (write == 0)) { + if ((IO->active_dns_event & EV_READ) != 0) + ev_io_stop(event_base, &IO->dns_recv_event); + if ((IO->active_dns_event & EV_WRITE) != 0) + ev_io_stop(event_base, &IO->dns_send_event); + IO->active_dns_event = 0; + } } CTDL_MODULE_INIT(c_ares_client) diff --git a/citadel/modules/smtp/serv_smtpeventclient.c b/citadel/modules/smtp/serv_smtpeventclient.c index 839f138af..8146959bc 100644 --- a/citadel/modules/smtp/serv_smtpeventclient.c +++ b/citadel/modules/smtp/serv_smtpeventclient.c @@ -108,6 +108,7 @@ typedef enum _eSMTP_C_States { eMaxSMTPC } eSMTP_C_States; +const long SMTP_C_ConnTimeout = 300; /* wail 5 minutes for connections... */ const long SMTP_C_ReadTimeouts[eMaxSMTPC] = { 90, /* Greeting... */ 30, /* EHLO */ @@ -167,6 +168,7 @@ void DeleteSmtpOutMsg(void *v) SmtpOutMsg *Msg = v; ares_free_data(Msg->AllMX); + FreeStrBuf(&Msg->msgtext); FreeAsyncIOContents(&Msg->IO); free(Msg); @@ -191,7 +193,9 @@ typedef eNextState (*SMTPSendHandler)(SmtpOutMsg *Msg); #define SMTP_VERROR(WHICH_ERR) do {\ SendMsg->MyQEntry->Status = WHICH_ERR; \ - StrBufAppendBufPlain(SendMsg->MyQEntry->StatusMessage, &ChrPtr(SendMsg->IO.IOBuf)[4], -1, 0); \ + StrBufPlain(SendMsg->MyQEntry->StatusMessage, \ + ChrPtr(SendMsg->IO.IOBuf) + 4, \ + StrLength(SendMsg->IO.IOBuf) - 4); \ return eAbort; } \ while (0) @@ -210,14 +214,14 @@ void FinalizeMessageSend(SmtpOutMsg *Msg) nRemain = CountActiveQueueEntries(Msg->MyQItem); - if (nRemain > 0) - MsgData = SerializeQueueItem(Msg->MyQItem); + MsgData = SerializeQueueItem(Msg->MyQItem); /* * Uncompleted delivery instructions remain, so delete the old * instructions and replace with the updated ones. */ CtdlDeleteMessages(SMTP_SPOOLOUT_ROOM, &Msg->MyQItem->QueMsgID, 1, ""); - + smtpq_do_bounce(Msg->MyQItem, + Msg->msgtext); if (nRemain > 0) { struct CtdlMessage *msg; msg = malloc(sizeof(struct CtdlMessage)); @@ -253,11 +257,15 @@ void get_one_mx_host_ip_done(void *Ctx, SmtpOutMsg *SendMsg = IO->Data; if ((status == ARES_SUCCESS) && (hostent != NULL) ) { CtdlLogPrintf(CTDL_DEBUG, - "SMTP client[%ld]: connecting to %s : %d ...\n", + "SMTP client[%ld]: connecting to %s [ip]: %d ...\n", SendMsg->n, SendMsg->mx_host, SendMsg->IO.dport); + SendMsg->MyQEntry->Status = 5; + StrBufPrintf(SendMsg->MyQEntry->StatusMessage, + "Timeout while connecting %s", + SendMsg->mx_host); SendMsg->IO.HEnt = hostent; InitEventIO(IO, SendMsg, @@ -267,6 +275,8 @@ void get_one_mx_host_ip_done(void *Ctx, SMTP_C_Timeout, SMTP_C_ConnFail, SMTP_C_ReadServerStatus, + SMTP_C_ConnTimeout, + SMTP_C_ReadTimeouts[0], 1); } diff --git a/citadel/modules/smtp/serv_smtpqueue.c b/citadel/modules/smtp/serv_smtpqueue.c index a05d2f26b..4eccf60ef 100644 --- a/citadel/modules/smtp/serv_smtpqueue.c +++ b/citadel/modules/smtp/serv_smtpqueue.c @@ -85,7 +85,7 @@ #include "ctdl_module.h" -#include "smtp_util.h" +///#include "smtp_util.h" #include "smtpqueue.h" #include "event_client.h" @@ -260,6 +260,9 @@ StrBuf *SerializeQueueItem(OneQueItem *MyQItem) StrBufAppendBufPlain(QMessage, HKEY("\nmsgid|"), 0); StrBufAppendPrintf(QMessage, "%ld", MyQItem->MessageID); + StrBufAppendBufPlain(QMessage, HKEY("\nsubmitted|"), 0); + StrBufAppendPrintf(QMessage, "%ld", MyQItem->Submitted); + if (StrLength(MyQItem->BounceTo) > 0) { StrBufAppendBufPlain(QMessage, HKEY("\nbounceto|"), 0); StrBufAppendBuf(QMessage, MyQItem->BounceTo, 0); @@ -280,6 +283,7 @@ StrBuf *SerializeQueueItem(OneQueItem *MyQItem) continue; /* skip already sent ones from the spoolfile. */ for (i=0; i < ThisItem->nAttempts; i++) { + /* TODO: most probably there is just one retry/attempted per message! */ StrBufAppendBufPlain(QMessage, HKEY("\nretry|"), 0); StrBufAppendPrintf(QMessage, "%ld", ThisItem->Attempts[i].retry); @@ -311,6 +315,7 @@ void NewMailQEntry(OneQueItem *Item) if (Item->MailQEntries == NULL) Item->MailQEntries = NewHash(1, Flathash); + Item->Current->StatusMessage = NewStrBuf(); Item->Current->n = GetCount(Item->MailQEntries); Put(Item->MailQEntries, IKEY(Item->Current->n), Item->Current, FreeMailQEntry); } @@ -360,6 +365,13 @@ void QItem_Handle_retry(OneQueItem *Item, StrBuf *Line, const char **Pos) Item->Current->Attempts[Item->Current->nAttempts].retry = StrBufExtractNext_int(Line, Pos, '|'); } + +void QItem_Handle_Submitted(OneQueItem *Item, StrBuf *Line, const char **Pos) +{ + Item->Submitted = atol(*Pos); + +} + void QItem_Handle_Attempted(OneQueItem *Item, StrBuf *Line, const char **Pos) { if (Item->Current == NULL) @@ -409,6 +421,167 @@ StrBuf *smtp_load_msg(OneQueItem *MyQItem, int n) +/* + * smtp_do_bounce() is caled by smtp_do_procmsg() to scan a set of delivery + * instructions for "5" codes (permanent fatal errors) and produce/deliver + * a "bounce" message (delivery status notification). + */ +void smtpq_do_bounce(OneQueItem *MyQItem, StrBuf *OMsgTxt) +{ + StrBuf *boundary, *Msg = NULL; + int num_bounces = 0; + struct CtdlMessage *bmsg = NULL; + int give_up = 0; + struct recptypes *valid; + int successful_bounce = 0; + static int seq = 0; + StrBuf *BounceMB; + HashPos *It; + void *vQE; + long len; + const char *Key; + + CtdlLogPrintf(CTDL_DEBUG, "smtp_do_bounce() called\n"); + + if ( (time(NULL) - MyQItem->Submitted) > SMTP_GIVE_UP ) { + give_up = 1;/// TODO: replace time by libevq timer get + } + + /* + * Now go through the instructions checking for stuff. + */ + It = GetNewHashPos(MyQItem->MailQEntries, 0); + while (GetNextHashPos(MyQItem->MailQEntries, It, &len, &Key, &vQE)) + { + MailQEntry *ThisItem = vQE; + if ((ThisItem->Status == 5) || /* failed now? */ + ((give_up == 1) && (ThisItem->Status != 2))) /* giving up after failed attempts... */ + { + if (num_bounces == 0) + Msg = NewStrBufPlain(NULL, 1024); + ++num_bounces; + + StrBufAppendBuf(Msg, ThisItem->Recipient, 0); + StrBufAppendBufPlain(Msg, HKEY(": "), 0); + StrBufAppendBuf(Msg, ThisItem->StatusMessage, 0); + StrBufAppendBufPlain(Msg, HKEY("\r\n"), 0); + } + } + DeleteHashPos(&It); + + /* Deliver the bounce if there's anything worth mentioning */ + CtdlLogPrintf(CTDL_DEBUG, "num_bounces = %d\n", num_bounces); + + if (num_bounces == 0) + return; + + boundary = NewStrBufPlain(HKEY("=_Citadel_Multipart_")); + StrBufAppendPrintf(boundary, "%s_%04x%04x", config.c_fqdn, getpid(), ++seq); + + /* Start building our bounce message; go shopping for memory first. */ + BounceMB = NewStrBufPlain(NULL, + 1024 + /* mime stuff.... */ + StrLength(Msg) + /* the bounce information... */ + StrLength(OMsgTxt)); /* the original message */ + if (BounceMB == NULL) { + FreeStrBuf(&boundary); + CtdlLogPrintf(CTDL_ERR, "Failed to alloc() bounce message.\n"); + + return; + } + + bmsg = (struct CtdlMessage *) malloc(sizeof(struct CtdlMessage)); + if (bmsg == NULL) { + FreeStrBuf(&boundary); + FreeStrBuf(&BounceMB); + CtdlLogPrintf(CTDL_ERR, "Failed to alloc() bounce message.\n"); + + return; + } + memset(bmsg, 0, sizeof(struct CtdlMessage)); + + + StrBufAppendBufPlain(BounceMB, HKEY("Content-type: multipart/mixed; boundary=\""), 0); + StrBufAppendBuf(BounceMB, boundary, 0); + StrBufAppendBufPlain(BounceMB, HKEY("\"\r\n"), 0); + StrBufAppendBufPlain(BounceMB, HKEY("MIME-Version: 1.0\r\n"), 0); + StrBufAppendBufPlain(BounceMB, HKEY("X-Mailer: " CITADEL "\r\n"), 0); + StrBufAppendBufPlain(BounceMB, HKEY("\r\nThis is a multipart message in MIME format.\r\n\r\n"), 0); + StrBufAppendBufPlain(BounceMB, HKEY("--"), 0); + StrBufAppendBuf(BounceMB, boundary, 0); + StrBufAppendBufPlain(BounceMB, HKEY("\r\n"), 0); + StrBufAppendBufPlain(BounceMB, HKEY("Content-type: text/plain\r\n\r\n"), 0); + + if (give_up) + StrBufAppendBufPlain( + BounceMB, + HKEY( + "A message you sent could not be delivered to some or all of its recipients\n" + "due to prolonged unavailability of its destination(s).\n" + "Giving up on the following addresses:\n\n" + ), 0); + else + StrBufAppendBufPlain( + BounceMB, + HKEY( + "A message you sent could not be delivered to some or all of its recipients.\n" + "The following addresses were undeliverable:\n\n" + ), 0); + + StrBufAppendBuf(BounceMB, Msg, 0); + FreeStrBuf(&Msg); + + /* Attach the original message */ + StrBufAppendBufPlain(BounceMB, HKEY("--"), 0); + StrBufAppendBuf(BounceMB, boundary, 0); + StrBufAppendBufPlain(BounceMB, HKEY("\r\n"), 0); + StrBufAppendBufPlain(BounceMB, HKEY("Content-type: message/rfc822\r\n"), 0); + StrBufAppendBufPlain(BounceMB, HKEY("Content-Transfer-Encoding: 7bit\r\n"), 0); + StrBufAppendBufPlain(BounceMB, HKEY("Content-Disposition: inline\r\n"), 0); + StrBufAppendBufPlain(BounceMB, HKEY("\r\n"), 0); + StrBufAppendBuf(BounceMB, OMsgTxt, 0); + + /* Close the multipart MIME scope */ + StrBufAppendBufPlain(BounceMB, HKEY("--"), 0); + StrBufAppendBuf(BounceMB, boundary, 0); + StrBufAppendBufPlain(BounceMB, HKEY("--\r\n"), 0); + + + + bmsg->cm_magic = CTDLMESSAGE_MAGIC; + bmsg->cm_anon_type = MES_NORMAL; + bmsg->cm_format_type = FMT_RFC822; + + bmsg->cm_fields['O'] = strdup(MAILROOM); + bmsg->cm_fields['N'] = strdup(config.c_nodename); + bmsg->cm_fields['U'] = strdup("Delivery Status Notification (Failure)"); + bmsg->cm_fields['A'] = SmashStrBuf(&BounceMB); + + /* First try the user who sent the message */ + if (StrLength(MyQItem->BounceTo) == 0) + CtdlLogPrintf(CTDL_ERR, "No bounce address specified\n"); + else + CtdlLogPrintf(CTDL_DEBUG, "bounce to user? <%s>\n", ChrPtr(MyQItem->BounceTo)); + + /* Can we deliver the bounce to the original sender? */ + valid = validate_recipients(ChrPtr(MyQItem->BounceTo), NULL, 0); + if ((valid != NULL) && (valid->num_error == 0)) { + CtdlSubmitMsg(bmsg, valid, "", QP_EADDR); + successful_bounce = 1; + } + + /* If not, post it in the Aide> room */ + if (successful_bounce == 0) { + CtdlSubmitMsg(bmsg, NULL, config.c_aideroom, QP_EADDR); + } + + /* Free up the memory we used */ + free_recipients(valid); + FreeStrBuf(&boundary); + CtdlFreeMessage(bmsg); + CtdlLogPrintf(CTDL_DEBUG, "Done processing bounces\n"); +} + /* * smtp_do_procmsg() * @@ -655,7 +828,8 @@ CTDL_MODULE_INIT(smtp_queu) Put(QItemHandlers, HKEY("attempted"), QItem_Handle_Attempted, reference_free_handler); Put(QItemHandlers, HKEY("remote"), QItem_Handle_Recipient, reference_free_handler); Put(QItemHandlers, HKEY("bounceto"), QItem_Handle_BounceTo, reference_free_handler); -///submitted /TODO: flush qitemhandlers on exit + Put(QItemHandlers, HKEY("submitted"), QItem_Handle_Submitted, reference_free_handler); +////TODO: flush qitemhandlers on exit smtp_init_spoolout(); CtdlRegisterCleanupHook(smtp_evq_cleanup); diff --git a/citadel/modules/smtp/smtpqueue.h b/citadel/modules/smtp/smtpqueue.h index a7b97cd90..023b479ca 100644 --- a/citadel/modules/smtp/smtpqueue.h +++ b/citadel/modules/smtp/smtpqueue.h @@ -14,6 +14,15 @@ typedef struct _mailq_entry { StrBuf *Recipient; StrBuf *StatusMessage; int Status; + /**< + * 0 = No delivery has yet been attempted + * 2 = Delivery was successful + * 4 = A transient error was experienced ... try again later + * 5 = Delivery to this address failed permanently. The error message + * should be placed in the fourth field so that a bounce message may + * be generated. + */ + int n; int Active; }MailQEntry; @@ -21,6 +30,7 @@ typedef struct _mailq_entry { typedef struct queueitem { long MessageID; long QueMsgID; + long Submitted; int FailNow; HashList *MailQEntries; MailQEntry *Current; /* copy of the currently parsed item in the MailQEntries list; if null add a new one. */ @@ -35,3 +45,5 @@ int DecreaseQReference(OneQueItem *MyQItem); void RemoveQItem(OneQueItem *MyQItem); int CountActiveQueueEntries(OneQueItem *MyQItem); StrBuf *SerializeQueueItem(OneQueItem *MyQItem); + +void smtpq_do_bounce(OneQueItem *MyQItem, StrBuf *OMsgTxt); diff --git a/citadel/techdoc/delivery-list.txt b/citadel/techdoc/delivery-list.txt index a852af552..f1c221942 100644 --- a/citadel/techdoc/delivery-list.txt +++ b/citadel/techdoc/delivery-list.txt @@ -23,7 +23,7 @@ a single instruction (usually a recipient). Fields are separated by the vertical bar character ("|") and there will always be at least one field on each line. - + -- Once per Queue-Item -- INSTRUCTION: msgid SYNTAX: msgid|0000000 @@ -67,7 +67,16 @@ fails). CtdlSaveMsg(), and therefore may be a local username, a user on another Citadel, or an Internet e-mail address. - + + INSTRUCTION: envelope_from + SYNTAX: envelope_from|blurdybloop@example.com + DESCRIPTION: + Sets a value to be used as the envelope sender during the 'MAIL FROM:' + phase of the SMTP transaction. If an envelope sender is not supplied, + one is extracted from the message body. + +-- Once per Remote-part per Queue-Item -- + INSTRUCTION: remote SYNTAX: remote|friko@mumjiboolean.com|0|delivery status message DESCRIPTION: @@ -79,11 +88,3 @@ fails). 5 = Delivery to this address failed permanently. The error message should be placed in the fourth field so that a bounce message may be generated. - - - INSTRUCTION: envelope_from - SYNTAX: envelope_from|blurdybloop@example.com - DESCRIPTION: - Sets a value to be used as the envelope sender during the 'MAIL FROM:' - phase of the SMTP transaction. If an envelope sender is not supplied, - one is extracted from the message body. -- 2.30.2