From 6388e9c69a4d74bedd72a0864d6f4ac37d3bb9ba Mon Sep 17 00:00:00 2001 From: InterLinked1 <24227567+InterLinked1@users.noreply.github.com> Date: Fri, 16 Feb 2024 07:47:54 -0500 Subject: [PATCH] mod_smtp_filter_dmarc: Add DMARC reporting support and integration. * Facilitate aggregate DMARC reporting using the opendmarc scripts by logging DMARC results to a history file that can be consumed by the opendmarc scripts. * Send immediate, individual failure reports (ruf) upon DMARC failure, if configured. * Improve the general robustness of DMARC verification and reporting. Related fixes and improvements: * mod_smtp_filter: Fix Authentication-Results header being generated prior to DMARC verification. * net_smtp: More clearly differentiate between MAIL FROM and From header address where needed. * mail.c: Extend mailer API to allow submitting entire RFC822 messages. * mail.c: Fix missing unlock. * lock.c: Print lock error if one occurs. --- README.rst | 4 + bbs/config.c | 3 +- bbs/lock.c | 6 +- bbs/mail.c | 184 +++++++++- bbs/node.c | 2 +- configs/mod_smtp_filter_dmarc.conf | 47 +++ include/mail.h | 49 ++- include/net_smtp.h | 21 +- modules/mod_auth_mysql.c | 2 +- modules/mod_ncurses.c | 2 +- modules/mod_sendmail.c | 77 +++-- modules/mod_smtp_delivery_external.c | 6 +- modules/mod_smtp_delivery_local.c | 10 + modules/mod_smtp_filter.c | 2 +- modules/mod_smtp_filter_arc.c | 3 +- modules/mod_smtp_filter_dkim.c | 2 +- modules/mod_smtp_filter_dmarc.c | 488 ++++++++++++++++++++++++--- nets/net_smtp.c | 101 +++++- scripts/libopendmarc_reporting.sh | 94 ++++++ 19 files changed, 989 insertions(+), 114 deletions(-) create mode 100644 configs/mod_smtp_filter_dmarc.conf create mode 100755 scripts/libopendmarc_reporting.sh diff --git a/README.rst b/README.rst index 2a8729b6..760f65bc 100644 --- a/README.rst +++ b/README.rst @@ -165,6 +165,10 @@ Config files go in :code:`/etc/lbbs` and are as follows: * :code:`mod_smtp_fetchmail.conf` - SMTP remote message queue management +* :code:`mod_smtp_filter_dkim.conf` - DKIM signing + +* :code:`mod_smtp_filter_dmarc.conf` - DMARC verification and reporting + * :code:`mod_smtp_mailing_lists.conf` - Mailing list configuration * :code:`modules.conf` - module loading settings (to disable a module, you do it here) diff --git a/bbs/config.c b/bbs/config.c index cafc9d35..e0c2a6c7 100644 --- a/bbs/config.c +++ b/bbs/config.c @@ -290,7 +290,8 @@ int bbs_config_free(struct bbs_config *c) RWLIST_UNLOCK(&configs); if (!cfg) { - bbs_error("Couldn't find config %s\n", c->name); + bbs_error("Couldn't find config\n"); /* c->name might not be valid here so don't print it? */ + bbs_log_backtrace(); /* So we can see what module this was for */ } else { config_free(cfg); } diff --git a/bbs/lock.c b/bbs/lock.c index cb22cc28..69a8c478 100644 --- a/bbs/lock.c +++ b/bbs/lock.c @@ -177,7 +177,7 @@ int __bbs_mutex_lock(bbs_mutex_t *t, const char *filename, int lineno, const cha res = pthread_mutex_lock(&t->mutex); #endif /* DETECT_DEADLOCKS */ if (unlikely(res)) { - lock_warning("Failed to obtain mutex %s\n", name); + lock_warning("Failed to obtain mutex %s: %s\n", name, strerror(res)); } else { t->info.lastlocked = time(NULL); if (unlikely(++t->info.owners != 1)) { @@ -298,7 +298,7 @@ int __bbs_rwlock_rdlock(bbs_rwlock_t *t, const char *filename, int lineno, const res = pthread_rwlock_rdlock(&t->lock); if (unlikely(res)) { - lock_warning("Failed to obtain rdlock %s\n", name); + lock_warning("Failed to obtain rdlock %s: %s\n", name, strerror(res)); } else { pthread_mutex_lock(&t->intlock); t->info.lastlocked = time(NULL); @@ -324,7 +324,7 @@ int __bbs_rwlock_wrlock(bbs_rwlock_t *t, const char *filename, int lineno, const res = pthread_rwlock_wrlock(&t->lock); if (unlikely(res)) { - lock_warning("Failed to obtain wrlock %s\n", name); + lock_warning("Failed to obtain wrlock %s: %s\n", name, strerror(res)); } else { /* If wrlock succeeded, we don't need to lock the internal mutex, since there can't be any readers right now */ t->info.lastlocked = time(NULL); diff --git a/bbs/mail.c b/bbs/mail.c index 4a3025fd..22957951 100644 --- a/bbs/mail.c +++ b/bbs/mail.c @@ -43,9 +43,11 @@ #include "include/linkedlists.h" #include "include/startup.h" #include "include/reload.h" +#include "include/stringlist.h" struct mailer { - int (*mailer)(MAILER_PARAMS); + int (*simple_mailer)(SIMPLE_MAILER_PARAMS); + int (*full_mailer)(FULL_MAILER_PARAMS); struct bbs_module *module; RWLIST_ENTRY(mailer) entry; unsigned int priority; @@ -237,13 +239,13 @@ int __attribute__ ((format (gnu_printf, 6, 7))) bbs_mail_fmt(int async, const ch return res; } -int __bbs_register_mailer(int (*mailer)(MAILER_PARAMS), void *mod, int priority) +int __bbs_register_mailer(int (*simple_mailer)(SIMPLE_MAILER_PARAMS), int (*full_mailer)(FULL_MAILER_PARAMS), void *mod, int priority) { struct mailer *m; RWLIST_WRLOCK(&mailers); RWLIST_TRAVERSE(&mailers, m, entry) { - if (m->mailer == mailer) { + if (m->simple_mailer == simple_mailer && m->full_mailer == full_mailer) { break; } } @@ -257,7 +259,8 @@ int __bbs_register_mailer(int (*mailer)(MAILER_PARAMS), void *mod, int priority) RWLIST_UNLOCK(&mailers); return -1; } - m->mailer = mailer; + m->simple_mailer = simple_mailer; + m->full_mailer = full_mailer; m->module = mod; m->priority = (unsigned int) priority; RWLIST_INSERT_SORTED(&mailers, m, entry, priority); /* Insert in order of priority */ @@ -265,11 +268,20 @@ int __bbs_register_mailer(int (*mailer)(MAILER_PARAMS), void *mod, int priority) return 0; } -int bbs_unregister_mailer(int (*mailer)(MAILER_PARAMS)) +int bbs_unregister_mailer(int (*simple_mailer)(SIMPLE_MAILER_PARAMS), int (*full_mailer)(FULL_MAILER_PARAMS)) { struct mailer *m; - m = RWLIST_WRLOCK_REMOVE_BY_FIELD(&mailers, mailer, mailer, entry); + RWLIST_WRLOCK(&mailers); + RWLIST_TRAVERSE_SAFE_BEGIN(&mailers, m, entry) { + if (m->simple_mailer == simple_mailer && m->full_mailer == full_mailer) { + RWLIST_REMOVE_CURRENT(entry); + break; + } + } + RWLIST_TRAVERSE_SAFE_END; + RWLIST_UNLOCK(&mailers); + if (!m) { bbs_error("Failed to unregister mailer: not currently registered\n"); return -1; @@ -307,8 +319,11 @@ int bbs_mail(int async, const char *to, const char *from, const char *replyto, c /* Hand off the delivery of the message itself to the appropriate module */ RWLIST_TRAVERSE(&mailers, m, entry) { + if (!m->simple_mailer) { + continue; + } bbs_module_ref(m->module, 1); - res = m->mailer(async, to, from, replyto, errorsto, subject, body); + res = m->simple_mailer(async, to, from, replyto, errorsto, subject, body); bbs_module_unref(m->module, 1); if (res >= 0) { break; @@ -318,3 +333,158 @@ int bbs_mail(int async, const char *to, const char *from, const char *replyto, c return res; } + +static void push_recipients(struct stringlist *restrict slist, int *restrict count, char *restrict s) +{ + char *recip, *recips = s; + + if (strlen_zero(recips)) { + return; + } + + bbs_term_line(recips); + while ((recip = strsep(&recips, ","))) { + char buf[256]; + if (strlen_zero(recip)) { + continue; + } + trim(recip); + if (strlen_zero(recip)) { + continue; + } + snprintf(buf, sizeof(buf), "<%s>", recip); /* Recipients need to be surrounded by <> */ + bbs_debug(6, "Adding recipient '%s'\n", buf); + stringlist_push(slist, buf); + (*count)++; + } +} + +int bbs_mail_message(const char *tmpfile, const char *mailfrom, struct stringlist *recipients) +{ + struct mailer *m; + char mailfrombuf[256]; + char tmpfile2[256]; + int res = -1; + struct stringlist reciplist; + + if (!mailfrom) { + mailfrom = ""; /* Empty MAIL FROM address */ + } else { + const char *tmpaddr = strchr(mailfrom, '<'); + /* This is just for the MAIL FROM, so just the address, no name */ + if (tmpaddr) { + /* Shouldn't have <>, but if it does, transparently remove them */ + bbs_strncpy_until(mailfrombuf, tmpaddr + 1, sizeof(mailfrombuf), '>'); + mailfrom = mailfrombuf; + } + } + + /* Extract recipients from message if needed */ + if (!recipients) { + int rewrite = 0; + int in_header = 0; + int recipient_count = 0; + char line[1002]; + /* Parse message for recipients to add. + * Check To, Cc, and Bcc headers. */ + FILE *fp = fopen(tmpfile, "r"); + if (!fp) { + bbs_error("fopen(%s) failed: %s\n", tmpfile, strerror(errno)); + return -1; + } + /* Process each line until end of headers */ + stringlist_init(&reciplist); + while ((fgets(line, sizeof(line), fp))) { + if (strlen(line) <= 2) { + break; + } + if (STARTS_WITH(line, "To:")) { + push_recipients(&reciplist, &recipient_count, line + STRLEN("To:")); + in_header = 1; + } else if (STARTS_WITH(line, "Cc:")) { + push_recipients(&reciplist, &recipient_count, line + STRLEN("Cc:")); + in_header = 1; + } else if (STARTS_WITH(line, "Bcc:")) { + push_recipients(&reciplist, &recipient_count, line + STRLEN("Bcc:")); + in_header = 1; + rewrite = 1; + } + if (line[0] == ' ') { + /* Continue previous header */ + if (in_header) { /* In header we care about */ + push_recipients(&reciplist, &recipient_count, line + 1); + } + } else { + in_header = 0; + } + } + bbs_debug(4, "Parsed %d recipient%s from message\n", recipient_count, ESS(recipient_count)); + /* If there are any Bcc headers, we need to remove those recipients, + * and regenerate the message. */ + if (rewrite) { + int inheaders = 1; + FILE *fp2; + strcpy(tmpfile2, "/tmp/smtpbccXXXXXX"); + fp2 = bbs_mkftemp(tmpfile2, MAIL_FILE_MODE); + if (!fp2) { + stringlist_empty_destroy(&reciplist); + return -1; + } + rewind(fp); + bbs_debug(2, "Rewriting message since it contains a Bcc header\n"); + while ((fgets(line, sizeof(line), fp))) { + if (inheaders) { + if (!strncmp(line, "\r\n", 2)) { + inheaders = 0; + } else if (STARTS_WITH(line, "Bcc:")) { + in_header = 1; + continue; /* Skip this line */ + } else if (line[0] == ' ') { + if (in_header) { + /* Skip if this is a multiline Bcc header */ + continue; + } + } else { + in_header = 0; + } + } + /* Copy line */ + fwrite(line, 1, strlen(line), fp2); + } + fclose(fp2); + fclose(fp); + /* Swap the files */ + bbs_delete_file(tmpfile); + tmpfile = tmpfile2; + } else { + fclose(fp); + } + } + + /* XXX smtp_inject consumes the stringlist and assumes responsibility for destroying it. + * The code here is in theory set up to try another mailer if the first one fails. + * However, it seems possible that the stringlist could have been modified prior to failure, + * meaning a retry using another mailer wouldn't get the full list. + * In practice, this is not currently an issue, since there are only two mailers, + * and only in net_smtp do we accept a stringlist of recipients. + * However, in the future, especially if that changes, it might be worth duplicating + * the stringlist here prior to each attempt, just to be safe. + * If we did that, we'd also want to destroy the stringlist after the list iteration. + */ + + RWLIST_RDLOCK(&mailers); + RWLIST_TRAVERSE(&mailers, m, entry) { + if (!m->full_mailer) { + continue; + } + bbs_module_ref(m->module, 2); + res = m->full_mailer(tmpfile, mailfrom, recipients ? recipients : &reciplist); + bbs_module_unref(m->module, 2); + if (res >= 0) { + break; + } + } + RWLIST_UNLOCK(&mailers); + + return res; +} diff --git a/bbs/node.c b/bbs/node.c index da390346..e306794c 100644 --- a/bbs/node.c +++ b/bbs/node.c @@ -825,7 +825,7 @@ int bbs_interrupt_node(unsigned int nodenum) void __bbs_node_interrupt_ack(struct bbs_node *node, const char *file, int line, const char *func) { bbs_assert(node->thread == pthread_self()); - bbs_debug(2, "Node %u acknowledged interrupt at %s:%d %s()\n", node->id, file, line, func); + __bbs_log(LOG_DEBUG, 2, file, line, func, "Node %u acknowledged interrupt\n", node->id); node->interruptack = 1; } diff --git a/configs/mod_smtp_filter_dmarc.conf b/configs/mod_smtp_filter_dmarc.conf new file mode 100644 index 00000000..54331814 --- /dev/null +++ b/configs/mod_smtp_filter_dmarc.conf @@ -0,0 +1,47 @@ +; mod_smtp_filter_dmarc.conf + +; DMARC enforcement +[enforcement] +reject=yes ; Reject messages that fail DMARC if sending domain's policy says to. (Can set to 'no' for debugging) +quarantine=yes ; Quarantine messages that fail DMARC if sending domain's policy says to. (Can set to 'no' for debugging) + +; Note: if you have specified addresses for the rua/ruf parameters of your DMARC records that belong to a different domain, +; you will need to ensure you have authorized cross-domain delivery of DMARC reports for delivery to succeed. +; See: https://dmarc.org/2015/08/receiving-dmarc-reports-outside-your-domain/ +; +; You may find it useful to your own testing to ensure that policy enforcement is as desired. +; It is best to do this between servers under your control to avoid penalizing a server that "shouldn't" be sending messages. +; A recommended tool for this is "swaks", which you can easily run on another server, e.g.: +; $ cd /tmp && wget https://raw.githubusercontent.com/jetmore/swaks/develop/swaks && chmod +x swaks +; $ ./swaks --from me@example.com --helo example.com --server smtp.example.net --to user@example.net +; +; Note for the privacy conscious: DMARC reports can expose information about your email infrastructure that you may not have +; intended to be public. For example, suppose Alice sends Bob an email. Normally, Bob's email provider (B), will send aggregate DMARC +; reports to Alice's email server (A). However, suppose Bob forwards his email to another mail provider, C, but doesn't want +; people to know email sent to B will get forwarded to C's servers. However, C, if configured to send DMARC reports, will send +; DMARC reports to A (likely showing an SPF fail, but a DKIM pass, assuming B did not modify the message before forwarding it). +; If A has access to the DMARC reports (say she operates the mail server) and she doesn't know anyone else using mail server C, +; then she can deduce the emails sent to B were forwarded there. +; This is not a security vulnerability; it is by design; however, the privacy-conscious may wish to NOT enable DMARC reporting +; on this server, if you do not want people to know you are forwarding mail here. + +; DMARC reporting configuration +[reporting] +;reportfailures=yes ; Report DMARC failures to the sending domain (ruf). Messages will be sent from dmarc-noreply@ + ; Also known as "forensic" reports. Default is no. + ; Most mail servers no longer send these reports, so you may not want to enable this without a good reason, see: + ; https://dmarcian.com/where-are-the-forensicfailure-reports/ and https://dmarcian.com/where-are-the-forensicfailure-reports/ +;reportbcc=dmarc-outbound@example.com ; Bcc the specified address on all failure reports (ruf) that are sent. No default. + ; This can be useful if you are troubleshooting delivery issues and want to receive copies of any DMARC failure reports. + +; History file used for aggregate (rua) DMARC reporting. +; LBBS does not handle aggregate DMARC reporting itself; the provided Perl reporting scripts +; in the OpenDMARC project's source tree should be used. +; This file has the same functionality as the log file denoted by the HistoryFile setting +; in the native OpenDMARC filter. Using OpenDMARC provided scripts, this file can be periodically +; imported into a database and further scripts can thence generate the actual DMARC reports. +; More details here: https://github.com/trusteddomainproject/OpenDMARC/blob/master/opendmarc/opendmarc.conf.sample#L203 +; Default is 'none' (no log file is created by default). + +; This is the default path hardcoded into opendmarc-importstats. If you change this, make sure to update that script as well. +;historyfile=/var/tmp/dmarc.dat diff --git a/include/mail.h b/include/mail.h index 3aaa8c01..6bd3ee6f 100644 --- a/include/mail.h +++ b/include/mail.h @@ -13,8 +13,37 @@ * */ +/* Forward declaration for bbs_mail_message prototype */ +struct stringlist; + +#define MAIL_FILE_MODE 0600 +#define SIMPLE_MAILER_PARAMS int async, const char *to, const char *from, const char *replyto, const char *errorsto, const char *subject, const char *body +#define FULL_MAILER_PARAMS const char *tmpfile, const char *mailfrom, struct stringlist *recipients + +/*! + * \brief Send an entire RFC822 message + * \param tmpfile Temporary file containing RFC822 message, which will be deleted after sending. MUST use CR LF line endings. + * \param mailfrom MAIL FROM address. If NULL, the empty MAIL FROM is used. Do not include <>. + * \param rcpt Recipient stringlist (including <>, but no names). If NULL, recipients will be extracted from the message itself, + * using the To, Cc, and Bcc headers. + * If the message contains any Bcc headers, you should pass NULL for this argument, + * since that will force recipients to be extracted and remove any Bcc headers + * in the sent message. If recipients is non-NULL, the message will NOT be modified + * at all, and thus should NOT include any Bcc headers, since this would leak information. + * The provided stringlist will be consumed and cleaned up, and should not be used afterwards. + * \retval 0 on success, -1 on failure + */ +int bbs_mail_message(const char *tmpfile, const char *mailfrom, struct stringlist *recipients); + +/*! \note "Simple" messages have only a single recipient, + * a limited number of customizable headers, + * and use the default Content-Type. + * If this does not meet the requirements of a message, + * bbs_mail_message should be used instead, since that + * accepts a raw RFC822 message for delivery. */ + /*! - * \brief Send an email + * \brief Create and send a simple email * \param async Whether to send the email asynchronously * \param to Recipient. If NULL, default from mail.conf will be used. Name is optional (use name \ format). * \param from Sender. If NULL, default from mail.conf will be used. Name is optional (use name \ format). @@ -26,7 +55,7 @@ int bbs_mail(int async, const char *to, const char *from, const char *replyto, const char *subject, const char *body); /*! - * \brief Send an email, with variadic printf-style arguments + * \brief Create and send a simple email, with variadic printf-style arguments * \param async Whether to send the email asynchronously * \param to Recipient. If NULL, default from mail.conf will be used. Name is optional (use email \ format). * \param from Sender. If NULL, default from mail.conf will be used. Name is optional (use email \ format). @@ -38,7 +67,7 @@ int bbs_mail(int async, const char *to, const char *from, const char *replyto, c int bbs_mail_fmt(int async, const char *to, const char *from, const char *replyto, const char *subject, const char *fmt, ...) __attribute__ ((format (gnu_printf, 6, 7))) ; /*! - * \brief Create an email + * \brief Create a simple email * \param p File handler into which to write the email * \param subject Email subject * \param body Email body @@ -56,18 +85,18 @@ int bbs_make_email_file(FILE *p, const char *subject, const char *body, const ch /*! \brief Initialize mail config */ int bbs_mail_init(void); -#define MAIL_FILE_MODE 0600 -#define MAILER_PARAMS int async, const char *to, const char *from, const char *replyto, const char *errorsto, const char *subject, const char *body - /*! * \brief Register a mailer - * \param mailer A callback that will send an email (accepting MAILER_PARAMS), that should return 0 if the message was handled, -1 if not handled, and 1 if delivery failed. + * \param simple_mailer A callback that will send an email (accepting SIMPLE_MAILER_PARAMS), that should return 0 if the message was handled, -1 if not handled, and 1 if delivery failed. * Note that "handled" does not necessarily mean "delivery guaranteed". + * \param full_mailer Same as simple mailer, but accepts a filename containing an entire RFC 822 message instead, and accepts responsibility for deleting it. + * Callback arguments will be non-NULL (even if bbs_mail_message is called with NULL arguments). + * Callback is responsible for cleaning up the recipients stringlist. * \param priority Positive priority to control order of callback preference. Like with MX records, a lower priority is preferred. * \retval 0 on success, -1 on failure */ -#define bbs_register_mailer(mailer, priority) __bbs_register_mailer(mailer, BBS_MODULE_SELF, priority) +#define bbs_register_mailer(simple_mailer, full_mailer, priority) __bbs_register_mailer(simple_mailer, full_mailer, BBS_MODULE_SELF, priority) -int __bbs_register_mailer(int (*mailer)(MAILER_PARAMS), void *mod, int priority); +int __bbs_register_mailer(int (*simple_mailer)(SIMPLE_MAILER_PARAMS), int (*full_mailer)(FULL_MAILER_PARAMS), void *mod, int priority); -int bbs_unregister_mailer(int (*mailer)(MAILER_PARAMS)); +int bbs_unregister_mailer(int (*simple_mailer)(SIMPLE_MAILER_PARAMS), int (*full_mailer)(FULL_MAILER_PARAMS)); diff --git a/include/net_smtp.h b/include/net_smtp.h index dbb88e26..c2a2cb43 100644 --- a/include/net_smtp.h +++ b/include/net_smtp.h @@ -83,12 +83,15 @@ struct smtp_filter_data { struct bbs_node *node; /*!< Node */ const char *from; /*!< Envelope from */ const char *helohost; /*!< HELO/EHLO hostname */ - /* Set by filter callbacks */ + /* Set by filter callbacks, but accessible only during filter execution */ char *spf; /*!< Allocated SPF header value */ char *dkim; /*!< Allocated DKIM results */ char *dmarc; /*!< Allocated DMARC results */ char *arc; /*!< Allocated ARC results */ char *authresults; /*!< Allocated Authentication-Results header */ + /* Set by filter callbacks, and accessible after filter execution */ + unsigned int reject:1; /*!< Set by filter(s) to TRUE to reject acceptance of message. */ + unsigned int quarantine:1; /*!< Set by filter(s) to TRUE to quarantine message. */ /* INTERNAL: Do not access these fields directly. Use the publicly exposed functions. */ int outputfd; /*!< File descriptor to write to, to prepend to message */ char outputfile[64]; /*!< Temporary output file name */ @@ -123,13 +126,22 @@ int smtp_filter_unregister(struct smtp_filter_provider *provider); /*! \brief Get the BBS node of an SMTP session */ struct bbs_node *smtp_node(struct smtp_session *smtp); +/*! \brief Get the upstream IP address */ +const char *smtp_sender_ip(struct smtp_session *smtp); + /*! \brief Get SMTP protocol used */ const char *smtp_protname(struct smtp_session *smtp); -/*! \brief Get the SMTP MAIL FROM address */ +/*! \brief Get the MAIL FROM address */ const char *smtp_from(struct smtp_session *smtp); -/*! \brief Get the SMTP MAIL FROM domain */ +/*! \brief Get the domain of the MAIL FROM address */ +const char *smtp_mail_from_domain(struct smtp_session *smtp); + +/*! + * \brief Get the MAIL FROM or From address domain + * \note This will return the From address domain if a From address is available and the MAIL FROM domain if not + */ const char *smtp_from_domain(struct smtp_session *smtp); /*! \brief Whether SPF validation should be performed */ @@ -174,6 +186,9 @@ int smtp_filter_add_header(struct smtp_filter_data *f, const char *name, const c /*! \brief Run a group of SMTP filters */ void smtp_run_filters(struct smtp_filter_data *fdata, enum smtp_direction dir); +/*! \brief Whether a message should be quarantined when delivered */ +int smtp_message_quarantinable(struct smtp_session *smtp); + /* == SMTP processor callbacks - these determine what will happen to a message, based on the message, but do not modify it == */ #define SMTP_MSG_DIRECTION_IN 0 diff --git a/modules/mod_auth_mysql.c b/modules/mod_auth_mysql.c index 43de6658..af6b9acc 100644 --- a/modules/mod_auth_mysql.c +++ b/modules/mod_auth_mysql.c @@ -327,7 +327,7 @@ static int make_user(const char *username, const char *password, const char *ful } /*! \note Sysop can always manually adjust the database if needed to override */ -#define USERNAME_RESERVED(u) (!strcasecmp(u, "root") || !strcasecmp(u, "sysop") || !strcasecmp(u, "bbs") || !strcasecmp(u, "ChanServ") || !strcasecmp(u, "NickServ") || !strcasecmp(u, "MessageServ") || !strcasecmp(u, "services") || !strcasecmp(u, "postmaster") || !strcasecmp(u, "newsmaster") || !strcasecmp(u, "anonymous")) +#define USERNAME_RESERVED(u) (!strcasecmp(u, "root") || !strcasecmp(u, "admin") || !strcasecmp(u, "sysop") || !strcasecmp(u, "bbs") || !strcasecmp(u, "ChanServ") || !strcasecmp(u, "NickServ") || !strcasecmp(u, "MessageServ") || !strcasecmp(u, "services") || !strcasecmp(u, "postmaster") || !strcasecmp(u, "newsmaster") || !strcasecmp(u, "anonymous") || !strcasecmp(u, "noreply")) static int username_reserved(const char *username) { diff --git a/modules/mod_ncurses.c b/modules/mod_ncurses.c index 22381a59..a91d89ee 100644 --- a/modules/mod_ncurses.c +++ b/modules/mod_ncurses.c @@ -442,7 +442,7 @@ int bbs_ncurses_menu_getopt(struct bbs_node *node, struct bbs_ncurses_menu *menu /* If the ncurses process is killed by a signal, * then the terminal will be messed up because endwin() * was not called to restore the terminal. - * Manually try to clean up, akin to what reset_shell_mode() would do. */ + * Manually restore the terminal settings to what they were originally. */ if (tcsetattr(node->slavefd, TCSANOW, &term)) { bbs_error("Failed to restore terminal settings: %s\n", strerror(errno)); } diff --git a/modules/mod_sendmail.c b/modules/mod_sendmail.c index d709c2bf..cd01fc08 100644 --- a/modules/mod_sendmail.c +++ b/modules/mod_sendmail.c @@ -40,30 +40,9 @@ #define SENDMAIL_ARG "-t" #define SENDMAIL_CMD "/usr/sbin/sendmail -t" -static int sendmail(MAILER_PARAMS) +static int sendmail_helper(const char *tmp, FILE *p, int async) { int res; - FILE *p; - char tmp[80] = "/tmp/bbsmail-XXXXXX"; - - /* We can't count on sendmail existing. Check first. */ - if (eaccess(SENDMAIL, R_OK)) { - bbs_error("System mailer '%s' does not exist, unable to send email to %s\n", SENDMAIL, to); - return -1; - } - - bbs_debug(4, "Sending %semail: %s -> %s (replyto %s), subject: %s\n", async ? "async " : "", from, to, S_IF(replyto), subject); - - /* Make a temporary file instead of piping directly to sendmail: - * a) to make debugging easier - * b) in case the mail command hangs - */ - p = bbs_mkftemp(tmp, MAIL_FILE_MODE); - if (!p) { - bbs_error("Unable to launch '%s' (can't create temporary file)\n", SENDMAIL_CMD); - return -1; - } - bbs_make_email_file(p, subject, body, to, from, replyto, errorsto, NULL, 0); /* XXX We could be calling this function from a node thread. * If it's async, it's totally fine and there's no problem, but if not, we're really hoping sendmail doesn't block very long or it will block shutdown. @@ -109,9 +88,57 @@ static int sendmail(MAILER_PARAMS) if (res != 0) { res = -1; /* If nonzero, ignore */ } else { - bbs_debug(1, "%s sent mail to %s with command '%s'\n", async ? "Asynchronously" : "Synchronously", to, SENDMAIL_CMD); + bbs_debug(1, "%s sent mail with command '%s'\n", async ? "Asynchronously" : "Synchronously", SENDMAIL_CMD); } } + return res; +} + +static int sendmail_full(FULL_MAILER_PARAMS) +{ + FILE *p; + + if (mailfrom) { + return -1; /* Can't handle explicit MAIL FROM */ + } else if (recipients) { + return -1; /* Can't handle explicit recipients, since sendmail will extract from the message */ + } + + p = fopen(tmpfile, "r"); + if (!p) { + fprintf(stderr, "fopen(%s) failed: %s\n", tmpfile, strerror(errno)); + return -1; + } + + return sendmail_helper(tmpfile, p, 0); +} + +static int sendmail_simple(SIMPLE_MAILER_PARAMS) +{ + FILE *p; + int res; + char tmp[80] = "/tmp/bbsmail-XXXXXX"; + + /* We can't count on sendmail existing. Check first. */ + if (eaccess(SENDMAIL, R_OK)) { + bbs_error("System mailer '%s' does not exist, unable to send email to %s\n", SENDMAIL, to); + return -1; + } + + bbs_debug(4, "Sending %semail: %s -> %s (replyto %s), subject: %s\n", async ? "async " : "", from, to, S_IF(replyto), subject); + + /* Make a temporary file instead of piping directly to sendmail: + * a) to make debugging easier + * b) in case the mail command hangs + */ + p = bbs_mkftemp(tmp, MAIL_FILE_MODE); + if (!p) { + bbs_error("Unable to launch '%s' (can't create temporary file)\n", SENDMAIL_CMD); + return -1; + } + bbs_make_email_file(p, subject, body, to, from, replyto, errorsto, NULL, 0); + + res = sendmail_helper(tmp, p, async); if (res) { bbs_error("Failed to send email to %s\n", to); } @@ -120,12 +147,12 @@ static int sendmail(MAILER_PARAMS) static int load_module(void) { - return bbs_register_mailer(sendmail, 10); + return bbs_register_mailer(sendmail_simple, sendmail_full, 10); } static int unload_module(void) { - return bbs_unregister_mailer(sendmail); + return bbs_unregister_mailer(sendmail_simple, sendmail_full); } BBS_MODULE_INFO_STANDARD("SendMail email transmission"); diff --git a/modules/mod_smtp_delivery_external.c b/modules/mod_smtp_delivery_external.c index 23a88bbf..7171ac42 100644 --- a/modules/mod_smtp_delivery_external.c +++ b/modules/mod_smtp_delivery_external.c @@ -1380,7 +1380,7 @@ static int queue_processor(struct smtp_session *smtp, const char *cmd, const cha * if the connected host is asking for somebody else's mail to be relayed. * But we shouldn't use bbs_ip_match_ipv4, we should use static_routes. */ - if (authorized_for_hostname(smtp_node(smtp)->ip, args)) { + if (authorized_for_hostname(smtp_sender_ip(smtp), args)) { identity_confirmed = 1; } else { bbs_debug(3, "Requested mail for '%s', but source IP address does not match source route\n", args); @@ -1592,7 +1592,7 @@ static int external_delivery(struct smtp_session *smtp, struct smtp_response *re } if (smtp_is_exempt_relay(smtp)) { - bbs_debug(2, "%s is explicitly authorized to relay mail from %s\n", smtp_node(smtp)->ip, smtp_from_domain(smtp)); + bbs_debug(2, "%s is explicitly authorized to relay mail from %s\n", smtp_sender_ip(smtp), smtp_from_domain(smtp)); } else if (get_static_routes(domain)) { bbs_debug(2, "%s has static route(s)\n", domain); } else { @@ -1800,7 +1800,7 @@ static int exists(struct smtp_session *smtp, struct smtp_response *resp, const c if (smtp_is_exempt_relay(smtp)) { /* Allow an external host to relay messages for a domain if it's explicitly authorized to. */ - bbs_debug(2, "%s is explicitly authorized to relay mail from %s\n", smtp_node(smtp)->ip, smtp_from_domain(smtp)); + bbs_debug(2, "%s is explicitly authorized to relay mail from %s\n", smtp_sender_ip(smtp), smtp_from_domain(smtp)); return 1; } diff --git a/modules/mod_smtp_delivery_local.c b/modules/mod_smtp_delivery_local.c index d68bd698..a308fe8b 100644 --- a/modules/mod_smtp_delivery_local.c +++ b/modules/mod_smtp_delivery_local.c @@ -247,6 +247,16 @@ static int do_local_delivery(struct smtp_session *smtp, struct smtp_response *re mproc.direction = SMTP_MSG_DIRECTION_IN; mproc.mbox = mbox; mproc.userid = 0; + + if (smtp_message_quarantinable(smtp)) { + /* We set the override mailbox before running callbacks, + * because users should have the final say in being able + * to override moving messages to particular mailboxes. + * Moving quarantined messages to "Junk" is just the default. */ + bbs_debug(5, "Message should be quarantined, so initializing destination mailbox to 'Junk'\n"); + mproc.newdir = strdup("Junk"); + } + if (smtp_run_callbacks(&mproc)) { return -1; /* If returned nonzero, it's assumed it responded with an SMTP error code as appropriate. */ } diff --git a/modules/mod_smtp_filter.c b/modules/mod_smtp_filter.c index 03368efe..1ccefa21 100644 --- a/modules/mod_smtp_filter.c +++ b/modules/mod_smtp_filter.c @@ -156,7 +156,7 @@ static int load_module(void) smtp_filter_register(&builtin_filter, SMTP_FILTER_PREPEND, SMTP_SCOPE_INDIVIDUAL, SMTP_DIRECTION_IN | SMTP_DIRECTION_SUBMIT, 0); smtp_filter_register(&relay_filter, SMTP_FILTER_PREPEND, SMTP_SCOPE_COMBINED, SMTP_DIRECTION_OUT, 1); /* For messages that are being relayed */ /* Run this only after the SPF, DKIM, and DMARC filters have run: */ - smtp_filter_register(&auth_filter, SMTP_FILTER_PREPEND, SMTP_SCOPE_COMBINED, SMTP_DIRECTION_IN, 5); + smtp_filter_register(&auth_filter, SMTP_FILTER_PREPEND, SMTP_SCOPE_COMBINED, SMTP_DIRECTION_IN, 6); return 0; } diff --git a/modules/mod_smtp_filter_arc.c b/modules/mod_smtp_filter_arc.c index 8623e1f6..c7915fd0 100644 --- a/modules/mod_smtp_filter_arc.c +++ b/modules/mod_smtp_filter_arc.c @@ -24,7 +24,6 @@ #include #include "include/module.h" -#include "include/node.h" #include "include/utils.h" #include "include/net_smtp.h" @@ -178,7 +177,7 @@ static int arc_filter_sign_cb(struct smtp_filter_data *f) } if (smtp_is_exempt_relay(f->smtp)) { - bbs_debug(2, "Skipping ARC signing (%s explicitly authorized to relay mail from %s)\n", smtp_node(f->smtp)->ip, smtp_from_domain(f->smtp)); + bbs_debug(2, "Skipping ARC signing (%s explicitly authorized to relay mail from %s)\n", smtp_sender_ip(f->smtp), smtp_from_domain(f->smtp)); return 0; } diff --git a/modules/mod_smtp_filter_dkim.c b/modules/mod_smtp_filter_dkim.c index 8508f9a3..04c799ac 100644 --- a/modules/mod_smtp_filter_dkim.c +++ b/modules/mod_smtp_filter_dkim.c @@ -152,7 +152,7 @@ static int dkim_verify_filter_cb(struct smtp_filter_data *f) if (!smtp_should_validate_dkim(f->smtp)) { const char *domain = smtp_from_domain(f->smtp); - if (domain && smtp_node(f->smtp) && smtp_relay_authorized(smtp_node(f->smtp)->ip, domain)) { + if (domain && smtp_node(f->smtp) && smtp_relay_authorized(smtp_sender_ip(f->smtp), domain)) { /* Nothing to verify, but we can mabe sign it, so do that instead. * If we can't, we'll just return early anyways. */ bbs_debug(1, "Message being relayed from authorized host is not DKIM signed, seeing if we can sign it...\n"); diff --git a/modules/mod_smtp_filter_dmarc.c b/modules/mod_smtp_filter_dmarc.c index 6c9396ab..9dc2f6b7 100644 --- a/modules/mod_smtp_filter_dmarc.c +++ b/modules/mod_smtp_filter_dmarc.c @@ -24,21 +24,114 @@ #include "include/module.h" #include "include/node.h" #include "include/utils.h" +#include "include/config.h" +#include "include/mail.h" #include "include/net_smtp.h" +static int enforce_rejects = 1; +static int enforce_quarantines = 1; +static int report_failures = 0; + +/* == Reporting related == */ +static char report_bcc[256] = ""; +static char log_filename[256] = ""; +static FILE *logfp = NULL; +static bbs_mutex_t loglock; + +/* Taken from OpenDMARC opendmarc/opendmarc-ar.h, for ABI compatibility when using enums as ints */ +#define ARES_RESULT_PASS 0 +#define ARES_RESULT_SOFTFAIL 2 +#define ARES_RESULT_NEUTRAL 3 +#define ARES_RESULT_TEMPERROR 4 +#define ARES_RESULT_PERMERROR 5 +#define ARES_RESULT_NONE 6 +#define ARES_RESULT_FAIL 7 + +/* From opendmarc/opendmarc.h */ +#define DMARC_RESULT_REJECT 0 +#define DMARC_RESULT_ACCEPT 2 +#define DMARC_RESULT_TEMPFAIL 3 +#define DMARC_RESULT_QUARANTINE 4 + +#define DMARC_ARC_POLICY_RESULT_PASS 0 +#define DMARC_ARC_POLICY_RESULT_FAIL 2 + +static inline int dmarcf_spf_res(const char *result) +{ + if (!strcasecmp(result, "pass")) { + return ARES_RESULT_PASS; + } else if (!strcasecmp(result, "fail")) { + return ARES_RESULT_FAIL; + } else if (!strcasecmp(result, "softfail")) { + return ARES_RESULT_SOFTFAIL; + } else if (!strcasecmp(result, "neutral")) { + return ARES_RESULT_NEUTRAL; + } else if (!strcasecmp(result, "temperror")) { + return ARES_RESULT_TEMPERROR; + } else if (!strcasecmp(result, "none")) { + return ARES_RESULT_NONE; + } else { + return ARES_RESULT_PERMERROR; + } +} + +static inline int spf_to_dmarc_res(int res) +{ + switch (res) { + case DMARC_POLICY_SPF_OUTCOME_PASS: return ARES_RESULT_PASS; + case DMARC_POLICY_SPF_OUTCOME_NONE: return ARES_RESULT_NONE; + case DMARC_POLICY_SPF_OUTCOME_TMPFAIL: return ARES_RESULT_TEMPERROR; + case DMARC_POLICY_SPF_OUTCOME_FAIL: return ARES_RESULT_FAIL; + default: return ARES_RESULT_PERMERROR; + } + __builtin_unreachable(); +} + +static inline int arc_res(const char *result) +{ + if (!strcasecmp(result, "pass")) { + return ARES_RESULT_PASS; + } else if (!strcasecmp(result, "fail")) { + return ARES_RESULT_FAIL; + } else if (!strcasecmp(result, "none")) { + return ARES_RESULT_NONE; + } else { + bbs_warning("Unexpected ARC result '%s'\n", result); + return ARES_RESULT_NONE; + } +} + +static inline void check_log_file(void) +{ + /* Check if the log file has changed from underneath us. + * This would happen if opendmarc-importstats has been run since the last logging. + * More lightweight than using inotify to detect this. */ + if (!bbs_file_exists(log_filename)) { + bbs_debug(1, "File '%s' has been rotated since last written to, reopening log file\n", log_filename); + fclose(logfp); + logfp = fopen(log_filename, "a"); + if (!logfp) { + bbs_error("Failed to open %s for appending: %s\n", log_filename, strerror(errno)); + } + } +} + +static unsigned int jobid = 0; + +/* == Main filter logic == */ + static const char *policy_name(int p) { switch (p) { - case DMARC_RECORD_P_NONE: - return "NONE"; case DMARC_RECORD_P_QUARANTINE: - return "QUARANTINE"; + return "quarantine"; case DMARC_RECORD_P_REJECT: - return "REJECT"; + return "reject"; case DMARC_RECORD_P_UNSPECIFIED: + case DMARC_RECORD_P_NONE: default: - return ""; + return "none"; } } @@ -48,21 +141,28 @@ static int dmarc_filter_cb(struct smtp_filter_data *f) { int dres; const char *domain; - OPENDMARC_STATUS_T status; + OPENDMARC_STATUS_T status, policy, apused; int spf_alignment, dkim_alignment; int is_ipv6; DMARC_POLICY_T *pctx; char dmarc_domain[256]; + int result, pct, enforce; char dmarc_result[sizeof(dmarc_domain) + 128]; - const char *result = NULL; + const char *adisposition, *aresult; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" + int spfres; +#pragma GCC diagnostic pop int p = 0, sp = 0; + char mctx_jobid[48]; + time_t now; if (smtp_is_exempt_relay(f->smtp)) { return 0; } is_ipv6 = !bbs_hostname_is_ipv4(f->node->ip); /* If it's not IPv4, must be IPv6? */ - domain = bbs_strcnext(f->from, '@'); + domain = smtp_mail_from_domain(f->smtp); if (!domain) { bbs_warning("Missing domain for received email?\n"); return 0; @@ -89,7 +189,7 @@ static int dmarc_filter_cb(struct smtp_filter_data *f) dresult = DMARC_POLICY_SPF_OUTCOME_NONE; } /* We always use the MAIL FROM domain, not the HELO/EHLO domain */ - dres = opendmarc_policy_store_spf(pctx, (unsigned char*) domain, dresult, DMARC_POLICY_SPF_ORIGIN_MAILFROM, (unsigned char*) f->spf); + spfres = dres = opendmarc_policy_store_spf(pctx, (unsigned char*) domain, dresult, DMARC_POLICY_SPF_ORIGIN_MAILFROM, (unsigned char*) f->spf); if (dres != DMARC_PARSE_OKAY) { bbs_warning("Failed to parse SPF for DMARC: %d\n", dres); } @@ -126,6 +226,9 @@ static int dmarc_filter_cb(struct smtp_filter_data *f) } } + /* Enforcement percentage */ + opendmarc_policy_fetch_pct(pctx, &pct); + status = opendmarc_policy_query_dmarc(pctx, (unsigned char*) domain); #pragma GCC diagnostic pop switch (status) { @@ -148,28 +251,13 @@ static int dmarc_filter_cb(struct smtp_filter_data *f) break; } - status = opendmarc_get_policy_to_enforce(pctx); - switch (status) { - case DMARC_POLICY_ABSENT: /* No DMARC record */ - bbs_debug(5, "No DMARC record found for domain %s\n", domain); - goto cleanup; - case DMARC_POLICY_NONE: /* Accept */ - result = "none"; - break; - case DMARC_POLICY_REJECT: - result = "reject"; - break; - case DMARC_POLICY_QUARANTINE: - result = "quarantine"; - break; - case DMARC_POLICY_PASS: - result = "pass"; - break; - /* These should never happen */ - case DMARC_FROM_DOMAIN_ABSENT: - case DMARC_PARSE_ERROR_NULL_CTX: - bbs_warning("Unexpected status %d\n", status); - goto cleanup; + /* This is not the policy in the DNS record, but the actual result of the DMARC check */ + policy = opendmarc_get_policy_to_enforce(pctx); + + if (opendmarc_policy_fetch_p(pctx, &p) != DMARC_PARSE_OKAY) { + bbs_warning("Failed to parse DMARC p\n"); + } else if (opendmarc_policy_fetch_sp(pctx, &sp) != DMARC_PARSE_OKAY) { + bbs_warning("Failed to parse DMARC sp\n"); } status = opendmarc_policy_fetch_alignment(pctx, &spf_alignment, &dkim_alignment); @@ -179,21 +267,307 @@ static int dmarc_filter_cb(struct smtp_filter_data *f) dkim_alignment == DMARC_POLICY_DKIM_ALIGNMENT_PASS ? "pass" : "fail"); } - if (opendmarc_policy_fetch_p(pctx, &p) != DMARC_PARSE_OKAY) { - bbs_warning("Failed to parse DMARC p\n"); - } else if (opendmarc_policy_fetch_sp(pctx, &sp) != DMARC_PARSE_OKAY) { - bbs_warning("Failed to parse DMARC sp\n"); - } - if (opendmarc_policy_fetch_utilized_domain(pctx, (unsigned char*) dmarc_domain, sizeof(dmarc_domain)) != DMARC_PARSE_OKAY) { bbs_warning("Failed to get DMARC domain\n"); } - snprintf(dmarc_result, sizeof(dmarc_result), "%s (p=%s sp=%s) header.from=%s", - result, policy_name(p), policy_name(sp), dmarc_domain); + apused = opendmarc_get_policy_token_used(pctx); + pct = pct ? pct : 100; /* If no %, default to 100% */ + enforce = random() % 100 < pct; /* Should we enforce the sending domain's policy, according to the enforcement percentage? */ + + /* LBBS doesn't have a concept of "job IDs", like most other MTAs, do. + * Just use epoch time + monotonically increasing number. */ + now = time(NULL); + bbs_mutex_lock(&loglock); + snprintf(mctx_jobid, sizeof(mctx_jobid), "%ld-%u", now, ++jobid); /* Safe to increment, since surrounded by loglock */ + bbs_mutex_unlock(&loglock); + + /* If DMARC failed, send failure report */ + switch (policy) { + case DMARC_POLICY_NONE: /* Alignment failed, but policy is 'none' - accept and report */ + case DMARC_POLICY_REJECT: /* Explicit reject */ + case DMARC_POLICY_QUARANTINE: /* Explicit quarantine */ + if (report_failures) { + char tmpfilename[128] = "/tmp/dmarcrufXXXXXX"; + FILE *fp; + unsigned char **ruv = opendmarc_policy_fetch_ruf(pctx, NULL, 0, 1); + fp = bbs_mkftemp(tmpfilename, MAIL_FILE_MODE); + if (fp) { + char date[48]; + char mailfrom[256]; + struct tm tm; + int c; + int recipients = 0; + + /* We can't use bbs_make_email_file, since we have a different Content-Type */ + strftime(date, sizeof(date), "%a, %d %b %Y %H:%M:%S %z", localtime_r(&now, &tm)); + fprintf(fp, "Date: %s\r\n", date); + fprintf(fp, "From: DMARC Reporter \r\n", smtp_hostname()); + for (c = 0; ruv && ruv[c]; c++) { + char *recip; + if (!STARTS_WITH((const char*) ruv[c], "mailto:")) { + continue; + } + recip = (char*) ruv[c] + STRLEN("mailto:"); + if (strlen_zero(recip)) { + continue; /* Empty mailto */ + } + bbs_strterm(recip, '!'); + if (strlen_zero(recip)) { + continue; + } + bbs_term_line(recip); + if (strlen_zero(recip)) { + continue; + } + /* We assume that all the recipients will fit on one line... which they SHOULD */ + bbs_debug(5, "Adding RUF report recipient: '%s'\n", recip); + if (!recipients++) { + fprintf(fp, "To: %s", recip); + } else { + fprintf(fp, ", %s", recip); + } + } + /* Bcc */ + if (!s_strlen_zero(report_bcc)) { + /* If only recipient, for some reason, then just use To, + * since nobody else is receiving the report, just in + * case the receiving email doesn't like emails without a To. + * Otherwise, add as Bcc. */ + bbs_debug(5, "Adding Bcc to report: %s\n", report_bcc); + if (!recipients++) { + fprintf(fp, "To: %s", report_bcc); + } else { + fprintf(fp, "\r\nBcc: %s", report_bcc); + } + } + if (!recipients) { + /* Report has no recipients, no point continuing */ + bbs_debug(2, "Aborting DMARC failure report addressed to no recipients\n"); + fclose(fp); + unlink(tmpfilename); + } else { + fprintf(fp, "\r\n"); /* Finish To (or Bcc) header */ + fprintf(fp, "Subject: DMARC failure report for job %s\r\n", mctx_jobid); + fprintf(fp, "Message-ID: <%s-%s>\r\n", "dmarcfail", mctx_jobid); /* job ID should already be unique, so use that as part of message ID here */ + fprintf(fp, "MIME-Version: 1.0\r\n"); + fprintf(fp, "Content-Type: multipart/report;\r\n" + "\treport-type=feedback-report;\r\n" + "\tboundary=\"%s:%s\"\r\n", smtp_hostname(), mctx_jobid); + fprintf(fp, "\r\n"); /* EOH */ + fprintf(fp, "--%s:%s\r\n" + "Content-Type: text/plain\r\n", + smtp_hostname(), mctx_jobid); + fprintf(fp, "\r\n"); /* EOH */ + fprintf(fp, "This is an authentication failure report for an email message received\r\n" + "from IP %s on %s.\r\n\r\n", smtp_sender_ip(f->smtp), date); + fprintf(fp, "--%s:%s\n" + "Content-Type: message/feedback-report\r\n", smtp_hostname(), mctx_jobid); + fprintf(fp, "\r\n"); /* EOH */ + fprintf(fp, "Feedback-Type: auth-failure\r\n"); + fprintf(fp, "Version: 1\r\n"); + fprintf(fp, "User-Agent: LBBS %s %s\r\n", BBS_VERSION, "DMARC Failure Reporter"); + fprintf(fp, "Auth-Failure: dmarc\r\n"); + fprintf(fp, "Authentication-Results: %s; dmarc=fail header.from=%s\r\n", bbs_hostname(), dmarc_domain); + fprintf(fp, "Original-Envelope-Id: %s\r\n", mctx_jobid); + fprintf(fp, "Original-Mail-From: %s\r\n", smtp_from(f->smtp)); + fprintf(fp, "Source-IP: %s\r\n", smtp_sender_ip(f->smtp)); + fprintf(fp, "Reported-Domain: %s\r\n", smtp_from_domain(f->smtp)); + /* If the headers were available, we could also add them all here + * fprintf(fp, "--%s:%s\r\nContent-Type: text/rfc822-headers\r\n", smtp_hostname(), mctx_jobid); + * foreach header => fprintf(fp, "%s: %s\r\n", hdr, val); + * fprintf(fp, "\r\n--%s:%s--\r\n", smtp_hostname(), mctx_jobid); + * + * However, this obviously has some privacy implications. + * Many major mail providers no longer send failure reports at all for that reason. + * Therefore, it could be argued that NOT including headers here is a feature, not a bug! + */ + fclose(fp); + snprintf(mailfrom, sizeof(mailfrom), "dmarc-noreply@%s", smtp_hostname()); + bbs_mail_message(tmpfilename, mailfrom, NULL); /* Thin wrapper around smtp_inject, effectively, which extracts recipients from message for us */ + } + } + } else { + bbs_debug(4, "DMARC failed for %s, but not sending failure report due to local policy\n", dmarc_domain); + } + break; + default: + break; + } + + /* DMARC result (whether message successfully verified or not) */ + switch (policy) { + case DMARC_POLICY_ABSENT: /* No DMARC record found */ + case DMARC_FROM_DOMAIN_ABSENT: /* No From: domain */ + aresult = "none"; + result = DMARC_RESULT_ACCEPT; + break; + case DMARC_POLICY_NONE: /* Alignment failed, but policy is 'none' */ + aresult = "fail"; /* Accept and report */ + result = DMARC_RESULT_ACCEPT; + break; + case DMARC_POLICY_PASS: /* Explicit accept */ + aresult = "pass"; + result = DMARC_RESULT_ACCEPT; + break; + case DMARC_POLICY_REJECT: /* Explicit reject */ + aresult = "fail"; + result = DMARC_RESULT_ACCEPT; + if (!enforce_rejects) { + bbs_notice("Message failed DMARC policy for %s, but not rejected due to local policy\n", dmarc_domain); + } else if (enforce) { + result = DMARC_RESULT_REJECT; + bbs_notice("Message rejected by DMARC policy for %s\n", dmarc_domain); + } else { + bbs_notice("Message failed DMARC policy for %s, but not rejected due to sampling percentage\n", dmarc_domain); + } + break; + case DMARC_POLICY_QUARANTINE: /* Explicit quarantine */ + aresult = "fail"; + result = DMARC_RESULT_ACCEPT; + if (!enforce_quarantines) { + bbs_notice("Message failed DMARC policy for %s, but not quarantined due to local policy\n", dmarc_domain); + } else if (enforce) { + result = DMARC_RESULT_QUARANTINE; + bbs_notice("Message quarantined by DMARC policy for %s\n", dmarc_domain); + } else { + bbs_notice("Message failed DMARC policy for %s, but not quarantined due to sampling percentage\n", dmarc_domain); + } + break; + default: + aresult = "temperror"; + result = DMARC_RESULT_TEMPFAIL; + break; + } + + /* If DMARC failed but ARC passed, override */ + if (result == DMARC_RESULT_REJECT) { + if (f->arc && !strcmp(f->arc, "pass")) { /* f->arc could be none, fail, pass */ + bbs_debug(2, "Message failed DMARC, but passed ARC, so accepting\n"); + result = DMARC_RESULT_ACCEPT; + /* Leave aresult as is */ + } else if (f->arc) { + bbs_debug(2, "Message failed DMARC (%s) and ARC (%s)\n", aresult, f->arc); + } else { + bbs_debug(2, "Message failed DMARC and no ARC results available\n"); + } + } + + /* Disposition: actual effective result (whether message will be accepted or not) */ + switch (result) { + case DMARC_RESULT_REJECT: /* We intend to reject this message */ + adisposition = "reject"; + break; + case DMARC_RESULT_QUARANTINE: /* We intend to quarantine this message */ + adisposition = "quarantine"; + break; + case DMARC_RESULT_ACCEPT: + default: /* We're not going to do anything to this message (just let it proceed normally) */ + adisposition = "none"; + break; + } + + bbs_debug(4, "Used %s policy (%s), %d%% enforcement\n", DMARC_USED_POLICY_IS_SP ? "subdomain" : "domain", policy_name(apused == DMARC_USED_POLICY_IS_SP ? sp : p), pct); + snprintf(dmarc_result, sizeof(dmarc_result), "%s (p=%s sp=%s dis=%s) header.from=%s", + aresult, /* actual result - whether things verified successfully or not */ + policy_name(apused == DMARC_USED_POLICY_IS_SP ? sp : p), /* p= (domain policy) */ + policy_name(sp), /* sp= (subdomain policy) */ + adisposition, /* dis= (disposition) - what we're doing with the message */ + dmarc_domain); REPLACE(f->dmarc, dmarc_result); -cleanup: + if (logfp) { + unsigned char **ruv; + int adkim, aspf; + + /* If we have a OpenDMARC-style history file for logging, log it. + * We log exactly the same way that OpenDMARC's opendmarc/opendmarc.c does. + * However, it's a lot easier for us since each filter isn't running in a separate process, + * and we can just write to the file all at once, rather than building up a string. */ + bbs_mutex_lock(&loglock); + check_log_file(); + + /* For cross-referencing with the OpenDMARC source code, + * actual function names from their code are used, + * but transparently redirected to fprintf, so if needed, + * it should be easy to find where this logic came from to debug. + * Not all the variable names provided for printf arguments are named the same, + * but the format string and everything prior to that should be identical. */ +#define dmarcf_dstring_printf(hb, fmt, ...) fprintf(logfp, fmt, ## __VA_ARGS__) + + /* General */ + dmarcf_dstring_printf(dfc->mctx_histbuf, "job %s\n", mctx_jobid); + + /* Hostname is smfi_getsymval with sendmail macro "j". + * https://web.mit.edu/freebsd/head/contrib/sendmail/libmilter/docs/smfi_getsymval.html + * $j is the full hostname: https://docstore.mik.ua/orelly/networking_2ndEd/tcp/appe_03.htm + * This is OUR hostname, not the remote hostname. */ + + dmarcf_dstring_printf(dfc->mctx_histbuf, "reporter %s\n", smtp_hostname()); + dmarcf_dstring_printf(dfc->mctx_histbuf, "received %ld\n", now); + dmarcf_dstring_printf(dfc->mctx_histbuf, "ipaddr %s\n", smtp_sender_ip(f->smtp)); + dmarcf_dstring_printf(dfc->mctx_histbuf, "from %s\n", smtp_from_domain(f->smtp)); /* From domain */ + dmarcf_dstring_printf(dfc->mctx_histbuf, "mfrom %s\n", smtp_mail_from_domain(f->smtp)); /* MAIL FROM domain */ + /* Skip ARC SPF/DKIM logging, since we don't check ARC in this module */ + /* SPF */ + if (f->spf) { + dmarcf_dstring_printf(dfc->mctx_histbuf, "spf %d\n", dmarcf_spf_res(f->spf)); + /* Can't be used uninitialized since we only use spfres when f->spf, and always set on that path */ + dmarcf_dstring_printf(dfc->mctx_histbuf, "spf %d\n", spf_to_dmarc_res(spfres)); + } + /* DMARC */ + dmarcf_dstring_printf(dfc->mctx_histbuf, "pdomain %s\n", dmarc_domain); + dmarcf_dstring_printf(dfc->mctx_histbuf, "policy %d\n", policy); + ruv = opendmarc_policy_fetch_rua(pctx, NULL, 0, 1); + if (ruv) { + int c; + for (c = 0; ruv[c]; c++) { + dmarcf_dstring_printf(dfc->mctx_histbuf, "rua %s\n", ruv[c]); + } + } else { + dmarcf_dstring_printf(dfc->mctx_histbuf, "rua -\n"); + } + + dmarcf_dstring_printf(dfc->mctx_histbuf, "pct %d\n", pct); + + opendmarc_policy_fetch_adkim(pctx, &adkim); + dmarcf_dstring_printf(dfc->mctx_histbuf, "adkim %d\n", adkim); + + opendmarc_policy_fetch_aspf(pctx, &aspf); + dmarcf_dstring_printf(dfc->mctx_histbuf, "aspf %d\n", aspf); + + dmarcf_dstring_printf(dfc->mctx_histbuf, "p %d\n", p); + dmarcf_dstring_printf(dfc->mctx_histbuf, "sp %d\n", sp); + dmarcf_dstring_printf(dfc->mctx_histbuf, "align_dkim %d\n", spf_alignment); + dmarcf_dstring_printf(dfc->mctx_histbuf, "align_spf %d\n", dkim_alignment); + + /* ARC Override */ + if (f->arc) { + int arcpolicypass = strcmp(f->arc, "fail") ? DMARC_ARC_POLICY_RESULT_PASS : DMARC_ARC_POLICY_RESULT_FAIL; + dmarcf_dstring_printf(dfc->mctx_histbuf, "arc %d\n", arc_res(f->arc)); + /* We can't really do the arc_policy one here (iterating through ARC-Seal headers), + * since that would require access to the internals of the ARC parser + * in mod_smtp_filter_arc, and we can't stick our gubbins in there from here. + * However, this field is mandatory in the DB schema so just use an empty one for now. */ + dmarcf_dstring_printf(dfc->mctx_histbuf, "arc_policy %d json:[]\n", arcpolicypass); + } + dmarcf_dstring_printf(dfc->mctx_histbuf, "action %d\n", result); + + fflush(logfp); + bbs_mutex_unlock(&loglock); + } + + /* If disposition is REJECT or QUARANTINE, mark that for processing once filter execution has completed. + * We still return 0 for QUARANTINE, since return 1 would abort filter execution, and we do want the Authentication-Results + * header to get added if we're saving the message. */ + if (result == DMARC_RESULT_REJECT) { + f->reject = 1; + opendmarc_policy_connect_shutdown(pctx); + return 1; + } else if (result == DMARC_RESULT_QUARANTINE) { + f->quarantine = 1; + } + opendmarc_policy_connect_shutdown(pctx); return 0; } @@ -202,6 +576,30 @@ struct smtp_filter_provider dmarc_filter = { .on_body = dmarc_filter_cb, }; +static int load_config(void) +{ + struct bbs_config *cfg = bbs_config_load("mod_smtp_filter_dmarc.conf", 1); + + if (!cfg) { + return 0; + } + + bbs_config_val_set_true(cfg, "enforcement", "reject", &enforce_rejects); + bbs_config_val_set_true(cfg, "enforcement", "quarantine", &enforce_quarantines); + + bbs_config_val_set_true(cfg, "reporting", "reportfailures", &report_failures); + bbs_config_val_set_str(cfg, "reporting", "reportbcc", report_bcc, sizeof(report_bcc)); + if (!bbs_config_val_set_str(cfg, "reporting", "historyfile", log_filename, sizeof(log_filename))) { + logfp = fopen(log_filename, "a"); + if (!logfp) { + bbs_error("Failed to open %s for appending: %s\n", log_filename, strerror(errno)); + } + } + + bbs_config_free(cfg); + return 0; +} + static int load_module(void) { if (opendmarc_policy_library_init(&lib) != DMARC_PARSE_OKAY) { @@ -209,7 +607,11 @@ static int load_module(void) return -1; } - /* Wait until SPF and DKIM/ARC have completed (priorities 1 and 2 respectively) before making any DMARC assessment */ + load_config(); + bbs_mutex_init(&loglock, NULL); + + /* Wait until SPF and DKIM/ARC have completed (priorities 1 and 2 respectively) before making any DMARC assessment. + * However, we need to run before auth_filter in mod_smtp_filter. */ smtp_filter_register(&dmarc_filter, SMTP_FILTER_PREPEND, SMTP_SCOPE_COMBINED, SMTP_DIRECTION_IN, 5); return 0; } @@ -217,6 +619,10 @@ static int load_module(void) static int unload_module(void) { smtp_filter_unregister(&dmarc_filter); + if (logfp) { + fclose(logfp); + } + bbs_mutex_destroy(&loglock); opendmarc_policy_library_shutdown(&lib); return 0; } diff --git a/nets/net_smtp.c b/nets/net_smtp.c index cd872f9b..c50b9300 100644 --- a/nets/net_smtp.c +++ b/nets/net_smtp.c @@ -168,7 +168,7 @@ struct smtp_session { int wfd; /* Transaction data */ - char *from; + char *from; /* MAIL FROM address */ struct stringlist recipients; struct stringlist sentrecipients; @@ -176,7 +176,8 @@ struct smtp_session { char *contenttype; /* Primary Content-Type of message */ /* AUTH: Temporary */ char *authuser; /* Authentication username */ - char *fromheaderaddress; /* Address in the From: header */ + char *fromheaderaddress; /* Address in the From: header, e.g. "John Smith" */ + char *fromaddr; /* Normalized from address, e.g. jsmith@example.com */ const char *datafile; @@ -196,6 +197,7 @@ struct smtp_session { unsigned int dkimsig:1; /* Message has a DKIM-Signature header */ unsigned int is8bit:1; /* 8BITMIME */ unsigned int relay:1; /* Message being relayed */ + unsigned int quarantine:1; /* Quarantine message */ } tflags; /* Transaction flags */ /* Not affected by RSET */ @@ -211,6 +213,7 @@ static void smtp_reset(struct smtp_session *smtp) { free_if(smtp->authuser); free_if(smtp->fromheaderaddress); + free_if(smtp->fromaddr); free_if(smtp->from); free_if(smtp->contenttype); stringlist_empty(&smtp->recipients); @@ -957,6 +960,10 @@ static int handle_mail(struct smtp_session *smtp, char *s) bbs_debug(5, "MAIL FROM is empty\n"); smtp->fromlocal = 0; REPLACE(smtp->from, ""); + if (!smtp->fromheaderaddress) { + /* Don't have a From address yet, so this is our most specific sender identity */ + REPLACE(smtp->fromaddr, smtp->from); + } smtp_reply(smtp, 250, 2.0.0, "OK"); return 0; } @@ -985,6 +992,10 @@ static int handle_mail(struct smtp_session *smtp, char *s) } REPLACE(smtp->from, from); + if (!smtp->fromheaderaddress) { + /* Don't have a From address yet, so this is our most specific sender identity */ + REPLACE(smtp->fromaddr, smtp->from); + } smtp_reply(smtp, 250, 2.1.0, "OK"); return 0; } @@ -1180,6 +1191,11 @@ struct bbs_node *smtp_node(struct smtp_session *smtp) return smtp->node; } +const char *smtp_sender_ip(struct smtp_session *smtp) +{ + return smtp->node ? smtp->node->ip : "127.0.0.1"; +} + const char *smtp_protname(struct smtp_session *smtp) { /* RFC 2822, RFC 3848, RFC 2033 */ @@ -1201,12 +1217,22 @@ const char *smtp_from(struct smtp_session *smtp) return smtp->from; } +const char *smtp_mail_from_domain(struct smtp_session *smtp) +{ + return bbs_strcnext(smtp->from, '@'); +} + const char *smtp_from_domain(struct smtp_session *smtp) { if (!smtp->from) { return NULL; } - return bbs_strcnext(smtp->from, '@'); + if (smtp->fromaddr) { + /* Use From header email address if available */ + return bbs_strcnext(smtp->fromaddr, '@'); + } + /* Fall back to MAIL FROM address if not */ + return smtp_mail_from_domain(smtp); } int smtp_is_exempt_relay(struct smtp_session *smtp) @@ -1780,6 +1806,11 @@ static int duplicate_loop_avoidance(struct smtp_session *smtp, char *recipient) return 0; } +int smtp_message_quarantinable(struct smtp_session *smtp) +{ + return smtp->tflags.quarantine; +} + /*! \brief "Stand and deliver" that email! */ static int expand_and_deliver(struct smtp_session *smtp, const char *filename, size_t datalen) { @@ -1825,6 +1856,27 @@ static int expand_and_deliver(struct smtp_session *smtp, const char *filename, s filterdata.outputfd = -1; smtp_run_filters(&filterdata, smtp->msa ? SMTP_DIRECTION_SUBMIT : SMTP_DIRECTION_IN); + if (filterdata.reject) { + /* A filter has indicated that this message should be rejected. + * XXX Currently, this only happens if a DMARC reject occured, so that is hardcoded here for now. */ + close(srcfd); + smtp_reply(smtp, 550, 5.7.1, "Message rejected due to policy failure"); + return 0; /* Return 0 to inhibit normal failure message, since we already responded */ + } else if (filterdata.quarantine) { + /* This is kind of a clunky hack. + * We need to be able to move quarantined messages into "Junk" + * in the local delivery handler. However, it only has access to the mproc structure, + * which is stack allocated inside the handler, so we can't access it from net_smtp. + * As a workaround, save off the quarantine flag onto the SMTP structure for permanence, + * and then check that from within the delivery handler. + * + * If we defined a structure that could be passed into all delivery handlers, + * instead of passing all the arguments directly, it would be appropriate to remove + * this bitfield from the SMTP struct and add it to that instead, and remove the API to check. + */ + smtp->tflags.quarantine = 1; + } + /* Since outputfd was originally -1, if it's not any longer, * that means the source has been modified and we should use that as the new source */ if (filterdata.outputfd != -1) { @@ -1867,7 +1919,7 @@ static int expand_and_deliver(struct smtp_session *smtp, const char *filename, s } if (*recipient != '<') { - bbs_warning("Malformed recipient: %s\n", recipient); + bbs_warning("Malformed recipient (missing <>): %s\n", recipient); } dup = strdup(recipient); @@ -1993,7 +2045,7 @@ int smtp_inject(const char *mailfrom, struct stringlist *recipients, const char } /*! - * \brief Inject a message to deliver via SMTP, from outside of the SMTP protocol + * \brief Inject a message to deliver via SMTP, to a single recipient, from outside of the SMTP protocol * \param filename Entire RFC822 message * \param from MAIL FROM. Do not include <>. * \param recipient RCPT TO. Must include <>. @@ -2003,7 +2055,6 @@ static int nosmtp_deliver(const char *filename, const char *sender, const char * { struct stringlist slist; - /*! \todo The mail interface should probably accept a stringlist globally, since it's reasonable to have multiple recipients */ stringlist_init(&slist); stringlist_push(&slist, recipient); @@ -2011,7 +2062,7 @@ static int nosmtp_deliver(const char *filename, const char *sender, const char * } /*! \brief Accept messages injected from the BBS to deliver, to local or external recipients */ -static int injectmail(MAILER_PARAMS) +static int injectmail_simple(SIMPLE_MAILER_PARAMS) { int res; FILE *fp; @@ -2047,7 +2098,6 @@ static int injectmail(MAILER_PARAMS) } #pragma GCC diagnostic pop #pragma GCC diagnostic pop - /* This should be enclosed in <>, but there must not be a name. * That's because the queue file writer expects us to provide <> around TO, but not FROM... it's a bit flimsy. */ @@ -2059,7 +2109,6 @@ static int injectmail(MAILER_PARAMS) } res = nosmtp_deliver(tmp, sender, recipient, (size_t) length); - unlink(tmp); bbs_debug(3, "injectmail res=%d, sender=%s, recipient=%s\n", res, sender, recipient); @@ -2068,6 +2117,30 @@ static int injectmail(MAILER_PARAMS) return res ? -1 : 0; } +static int injectmail_full(const char *tmpfile, const char *mailfrom, struct stringlist *recipients) +{ + int res; + FILE *fp; + long int length; + + fp = fopen(tmpfile, "r"); + if (!fp) { + bbs_error("fopen(%s) failed: %s\n", tmpfile, strerror(errno)); + stringlist_empty_destroy(recipients); + return -1; + } + + fseek(fp, 0L, SEEK_END); /* Go to EOF */ + length = ftell(fp); + fclose(fp); + + res = smtp_inject(mailfrom, recipients, tmpfile, (size_t) length); + unlink(tmpfile); + bbs_debug(3, "injectmail res=%d, mailfrom=%s\n", res, mailfrom); + + return res ? -1 : 0; +} + static int check_identity(struct smtp_session *smtp, char *s) { char *user, *domain; @@ -2344,8 +2417,8 @@ static int do_deliver(struct smtp_session *smtp, const char *filename, size_t da fromaddr = fromhdrdup; } bbs_debug(4, "Updating internal from address from '%s' to '%s'\n", smtp->from, fromaddr); - REPLACE(smtp->from, fromaddr); - free_if(smtp->fromheaderaddress); + REPLACE(smtp->fromaddr, fromaddr); + /* Don't free smtp->fromheaderaddress yet, that we can still use it */ } res = expand_and_deliver(smtp, filename, datalen); @@ -2490,7 +2563,7 @@ static int handle_burl(struct smtp_session *smtp, char *s) bbs_strterm(buf, '\r'); from = buf + STRLEN("From:"); ltrim(from); - smtp->fromheaderaddress = strdup(from); + REPLACE(smtp->fromheaderaddress, from); break; } else if (!strcmp(buf, "\r\n")) { bbs_warning("BURL submission is missing From header\n"); /* Transmission will probably be rejected, but not our concern here. */ @@ -2954,7 +3027,7 @@ static int load_module(void) } bbs_register_tests(tests); - bbs_register_mailer(injectmail, 1); + bbs_register_mailer(injectmail_simple, injectmail_full, 1); bbs_cli_register_multiple(cli_commands_smtp); return 0; @@ -2967,7 +3040,7 @@ static int load_module(void) static int unload_module(void) { - bbs_unregister_mailer(injectmail); + bbs_unregister_mailer(injectmail_simple, injectmail_full); bbs_unregister_tests(tests); bbs_cli_unregister_multiple(cli_commands_smtp); if (smtp_enabled) { diff --git a/scripts/libopendmarc_reporting.sh b/scripts/libopendmarc_reporting.sh new file mode 100755 index 00000000..db540c03 --- /dev/null +++ b/scripts/libopendmarc_reporting.sh @@ -0,0 +1,94 @@ +#!/bin/sh + +# libopendmarc_reporting.sh +# Set up the DMARC reporting database and scripts +# to process reporting logs from mod_smtp_filter_dmarc + +set -e + +# Install prereqs for perl scripts +apt-get install -y libswitch-perl libdbd-mysql-perl +export PERL_MM_USE_DEFAULT=1 +cpan JSON + +# Build OpenDMARC from source to compile the reports scripts, +# but don't install it, since we already installed the package. + +cd /usr/local/src +git clone https://github.com/trusteddomainproject/OpenDMARC.git +cd OpenDMARC +aclocal && autoconf && autoreconf --install && automake --add-missing && ./configure + +# Build, but don't install everything, just the reports +make +cd reports + +# Get hostname to use, since default reporting address is postmaster@, which is unsuitable +BBS_HOSTNAME=$( grep "hostname" /etc/lbbs/nodes.conf | cut -d'=' -f2 | cut -d' ' -f1 | tr -d '\n' ) +printf "Autodetected SMTP hostname: %s\n", "$BBS_HOSTNAME" + +# The opendmarc-reports perl script uses the system hostname for HELO/EHLO +# If this isn't a FQDN, then some MTAs might not like that if they check the HELO hostname. +hostname | grep -F "." +if [ $? -ne 0 ]; then + printf "System hostname is not a FQDN: " + hostname + # Since it's not a FQDN, replace hostfqdn() in the reporting script with $BBS_HOSTNAME + # so we use the right HELO (to avoid updating the system hostname just for this) + printf "Modifying opendmarc-reports script to use hardcoded hostname '%s' instead - you will need to change this if your hostname changes!\n" $BBS_HOSTNAME + sed -i "s/hostfqdn()/\"$BBS_HOSTNAME\"/" opendmarc-reports.in +fi + +# Install the reporting scripts +make install + +# Set up the database + +# You should ideally change the password in the scripts, +# but worst case it's a local user so it probably doesn't matter anyways. +# The opendmarc scripts use opendmarc as the default so change it to that. +sed -i 's/changeme/opendmarc/' db/schema.mysql +sed -i 's/-- CREATE/CREATE/' db/schema.mysql +sed -i 's/-- GRANT/GRANT/' db/schema.mysql + +# Script is broken by default: https://github.com/trusteddomainproject/OpenDMARC/issues/184 +sed -i 's/1970-01-01 00:00:00/1970-01-01 00:00:01/' db/schema.mysql + +mariadb < db/schema.mysql + +# Script should now be able to run and connect to the DB using the script defaults +# opendmarc-import is used to read messages from a log file and put them in the DB +# opendmarc-importstats is the wrapper around opendmarc-import, to be called from cron +# opendmarc-reports is used to actually generate and send reports +# opendmarc-expire is used to periodically expire old data that is no longer needed + +# The following scripts need to be called automatically (via cron), +# but do a test run first and make sure all is well. +# Helps if you send a test email prior to running these, so you can see something. + +opendmarc-expire --verbose + +# Import from log file into database +# This will rotate /var/tmp/dmarc.dat, and mod_smtp_filter_dmarc will subsequently reopen the file +# so that future logging goes to a fresh log file. +# (The opendmarc milter opens the file each time, so it doesn't need to check) + +# This script will call opendmarc-import just using the program name, so it needs to be installed to be in PATH. +# If this fails, it will also fail at runtime. +opendmarc-importstats + +# Process database and send reports. Since --test will generate funky named XML files, cd to /tmp first. +cd /tmp +opendmarc-reports --test --verbose --verbose --verbose + +# Go ahead and add everything to crontab. +# Assuming this machine is in UTC, reports should go out at midnight every night. + +# We need to manually set a PATH for the script, since opendmarc-importstats calls opendmarc-import, +# and that will fail if PATH is not set, rotating the file, but not importing it, basically discarding the log. +# Since we include /usr/local/bin explicitly in the PATH for the job, we can omit the full path to the other commands. +# The other directories in path are needed for mv and ls, other programs called by opendmarc-importstats +# Spawning a subshell doesn't seem to work in crontab for grouping commands for redirection, so +# we just spawn /bin/sh for compatibility, so we can append all output to a log file easily. + +(crontab -l ; echo "0 0 * * * PATH=/usr/bin:/usr/local/bin:/usr/local/sbin /bin/sh -c 'opendmarc-importstats && opendmarc-reports --verbose --verbose --verbose --report-org=${BBS_HOSTNAME} --report-email=dmarc-noreply@${BBS_HOSTNAME} && opendmarc-expire' >> /var/log/lbbs/dmarcrua.log 2>&1") | sort - | uniq - | crontab -