/* libspawner, an implementation of the MTA side of Sendmail's Milter protocol. Copyright (C) 2005-07 Hilko Bengen This library is free software; you can redistribute it and/or modify it under the terms of version 2.1 of the GNU Lesser General Public License as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "spawner.h" #include "protocol.h" #include #include #include /* struct sockaddr_un */ #include #include /* malloc() */ #include #include #include /* For byte order conversion */ #include #include #include /* For debug callback */ #include #include #include #define is_valid_state(x,y) ( (!x&&!y) ? 1 : x&y ) #define invalid_state(sp) (sp->strict_state?SPAWNER_ERROR_STATE:SPAWNER_OK) #define BODY_CHUNKSIZE 8192 #define DEBUG_BUFSIZE 256 /** Constants for internal state */ #define STATE_INIT 0x00 #define STATE_CONNECT 0x01 #define STATE_HELO 0x02 #define STATE_RCPT 0x04 #define STATE_MAIL 0x08 #define STATE_HEADER 0x20 #define STATE_BODY 0x10 #define STATE_EOH 0x40 #define STATE_EOM 0x80000000 /** \brief Structure associated with a connection to a milter process. This structure is opaque to programs using this library. */ struct spawner { char* name; /**< Filter name */ /** Specification for the milter connection; see the Sendmail Installation and Operation Guide for details */ char* spec; int af; /**< Address family of socket */ size_t salen; /**< size of sockaddr */ /** socket address used by spawner_open() */ struct sockaddr *sa; /** Socket used to communicate with the mail filter */ int socket; int error; u_int32_t state; /**< internal state */ /** ignore internal state (return SPAWNER_OK for messages or macros that are sent out of order */ int strict_state; u_int32_t version; /**< Milter protocol API version */ u_int32_t flags; /**< modification flags SMFIF_* */ u_int32_t actions; /**< protocol actions SMFIP_* */ /** Custom reply text that should be passed to the SMTP client */ char* replytext; /** Timeout for connecting to a filter (C), default=5m */ unsigned int connect_timeout; /** Timeout for sending information from the MTA to a filter (S), default=10s */ unsigned int send_timeout; /** Timeout for reading reply from the filter (R), default=10s */ unsigned int recv_timeout; /** Timeout between sending end-of-message to filter and waiting for the final acknowledgment (E), default=5m */ unsigned int eom_timeout; /** Callback for "add envelope recipient" modification action */ void (*add_rcpt)(struct spawner*, char*); /** Callback for "remove envelope recipient" modification action */ void (*del_rcpt)(struct spawner*, char*); /** Callback for "add header" modification action */ void (*add_header)(struct spawner*, char*, char*); /** Callback for "change header" modification action */ void (*change_header)(struct spawner*, int, char*, char*); /** Callback for "replace body" modification action */ void (*replace_body)(struct spawner*, int, char*); /** Callback for "quarantine" modification action */ void (*quarantine)(struct spawner*, char*); /** Callback for debug output */ void (*debug)(struct spawner*, int, char*); }; void spawner_stdout_debug(spawner *sp, int level, char *buf); /** \brief Read response from milter socket Reads from the socket, expecting an "accept/reject action" If a custom replytext is sent by the milter, its text part is copied into sp->replytext. @param sp Pointer to spawner structure @return CONTINUE/ACCEPT/REJECT/DISCARD/TEMPFAIL */ int spawner_recv_response(spawner* sp) { smfi_msg* msg = NULL; char res; if (read_msg(sp->socket, &msg, sp->recv_timeout) != 0) { res = SPAWNER_ERROR_IO; goto exit; } free(sp->replytext); sp->replytext = NULL; switch(msg->cmd) { /* FIXME state machine ---v */ case SMFIR_ACCEPT: res = SPAWNER_ACCEPT; break; case SMFIR_CONTINUE: res = SPAWNER_CONTINUE; break; case SMFIR_DISCARD: res = SPAWNER_DISCARD; break; case SMFIR_REJECT: res = SPAWNER_REJECT; break; case SMFIR_TEMPFAIL: res = SPAWNER_TEMPFAIL; break; case SMFIR_REPLYCODE: sp->replytext = strdup(msg->data+4); /* "Parse" reply code. (only the first digit) */ switch(msg->data[0]) { case '2': res = SPAWNER_CONTINUE; break; case '3': res = SPAWNER_CONTINUE; break; case '4': res = SPAWNER_TEMPFAIL; break; case '5': res = SPAWNER_REJECT; break; default: res = SPAWNER_ERROR_PROTO; break; } default: res = SPAWNER_ERROR_PROTO; break; } exit: free(msg); return res; } /** \brief Read response from milter socket Reads from the socket, expecting zero or more "modification actions" and one "accept/reject action". If callback functions for "modification actions" have been registered by the application, they are called. If a custom replytext is sent by the milter, its text part is copied into sp->replytext. @param sp Pointer to spawner structure @return CONTINUE/ACCEPT/REJECT/DISCARD/TEMPFAIL */ int spawner_recv_eom_response(spawner* sp) { smfi_msg* msg = NULL; char res; char *s1, *s2; int i; time_t now = time(NULL); while (1) { if (read_msg(sp->socket, &msg, sp->recv_timeout) != 0) { res = SPAWNER_ERROR_IO; goto exit; } free(sp->replytext); sp->replytext = NULL; switch(msg->cmd) { case SMFIR_ACCEPT: res = SPAWNER_ACCEPT; goto exit; case SMFIR_CONTINUE: res = SPAWNER_CONTINUE; goto exit; case SMFIR_DISCARD: res = SPAWNER_DISCARD; goto exit; case SMFIR_REJECT: res = SPAWNER_REJECT; goto exit; case SMFIR_TEMPFAIL: res = SPAWNER_TEMPFAIL; goto exit; case SMFIR_REPLYCODE: sp->replytext = strdup(msg->data+4); /* "Parse" reply code. (Well, only the first digit. */ switch(msg->data[0]) { case '2': res = SPAWNER_CONTINUE; goto exit; case '3': res = SPAWNER_CONTINUE; goto exit; case '4': res = SPAWNER_TEMPFAIL; goto exit; case '5': res = SPAWNER_REJECT; goto exit; default: res = SPAWNER_ERROR_PROTO; goto exit; } } /* check for timeout */ /* XXX that cast necessary? */ if ((unsigned)time(NULL) > now + sp->eom_timeout) { res = SPAWNER_ERROR_IO; goto exit; } /* modification actions */ switch(msg->cmd) { case SMFIR_ADDRCPT: s1 = msg->data; if (sp->add_rcpt != NULL) sp->add_rcpt(sp, s1); break; case SMFIR_DELRCPT: s1 = msg->data; if (sp->del_rcpt != NULL) sp->del_rcpt(sp, s1); break; case SMFIR_ADDHEADER: s1 = msg->data; for ( s2=s1; *s2!='\0'; s2++); s2++; if (sp->add_header != NULL) sp->add_header(sp, s1, s2); break; case SMFIR_CHGHEADER: i = *((int*)msg->data); s1 = msg->data+4; for ( s2=s1; *s2!='\0'; s2++); s2++; if (sp->change_header != NULL) sp->change_header(sp, i, s1, s2); break; case SMFIR_QUARANTINE: s1 = msg->data; if (sp->quarantine != NULL) sp->quarantine(sp, s1); break; case SMFIR_REPLBODY: i = msg->size - 1; s1 = msg->data; if (sp->replace_body != NULL) sp->replace_body(sp, i, s1); break; default: res = SPAWNER_ERROR_PROTO; break; } } exit: free(msg); return res; } /** \brief Send command, consisting of a command byte and two parameter strings @param sp Pointer to spawner structure @param cmd Command @param s1 Parameter 1 @param s2 Parameter 2 @return error value (SPAWNER_OK on success) */ int spawner_send_cmd_twostrings(spawner* sp, char cmd, char* s1, char* s2) { int s; int size; int res = SPAWNER_OK; smfi_msg* msg = NULL; if (s1 == NULL) s1=""; if (s2 == NULL) s2=""; sp->error = 0; /* Strings need to be null-terminated */ s = strlen(s1) + 1; size = s + strlen(s2) + 1; msg = malloc(sizeof(smfi_msg) + size); if (msg == NULL) { sp->error = ENOMEM; res = SPAWNER_ERROR; goto exit; } msg->cmd = cmd; msg->size = htonl(size + 1); strcpy(msg->data, s1); strcpy(msg->data+s, s2); if (write_msg(sp->socket, msg, sp->send_timeout) != 0) { res = SPAWNER_ERROR_IO; goto exit; } exit: free(msg); return res; } /** \brief Send simple command, consisting of a single byte @param sp Pointer to spawner structure @param cmd Command @return error value (SPAWNER_OK on success) */ int spawner_send_cmd_simple(spawner* sp, char cmd) { int res = SPAWNER_OK; smfi_msg msg; msg.size = htonl(1); msg.cmd = cmd; if ( write_msg(sp->socket, &msg, sp->send_timeout) != 0) res = SPAWNER_ERROR_IO; return res; } /** \brief Allocates and initializes the "spawner" structure which is used to identify a milter connection. @return A pointer to the new spawner structure. */ spawner* spawner_init() { spawner* sp; if ((sp=malloc(sizeof(spawner))) == NULL) return NULL; sp->name = NULL; sp->spec = NULL; sp->af = AF_UNSPEC; sp->salen = 0; sp->sa = NULL; sp->socket = -1; sp->error = 0; sp->state = STATE_INIT; sp->strict_state = 1; sp->version = 2; /* We will offer the mail filter to skip any stage */ sp->flags = SMFIP_NOCONNECT | SMFIP_NOHELO | SMFIP_NOMAIL \ | SMFIP_NORCPT | SMFIP_NOBODY | SMFIP_NOHDRS | SMFIP_NOEOH; sp->actions = 0; /* Default timeout values */ sp->connect_timeout = 5*60; sp->send_timeout = 10; sp->recv_timeout = 10; sp->eom_timeout = 5*60; sp->replytext = NULL; sp->add_rcpt = NULL; sp->del_rcpt = NULL; sp->add_header = NULL; sp->change_header = NULL; sp->replace_body = NULL; sp->quarantine = NULL; sp->debug = &spawner_stdout_debug; return sp; } /** \brief Close connection to milter (if any) and free any allocated memory @param sp Pointer to spawner structure */ void spawner_destroy(spawner* sp) { spawner_close(sp); free(sp->name); free(sp->spec); free(sp->sa); free(sp->replytext); free(sp); } /** \brief Set steps that are to be skipped by the communication between MTA and mail filter. Must be called before the connection to the mail filter is established using spawner_open() @param sp Pointer to spawner structure @param flags Flags, according to SMFIF_* @return SPAWNER_OK if called before spawner_open(), SPAWNER_ERROR_STATE otherwise. */ int spawner_set_protocol_flags(spawner* sp, u_int32_t flags) { if (!is_valid_state(sp->state,STATE_INIT)) return SPAWNER_ERROR_STATE; sp->error = 0; sp->flags = flags; return SPAWNER_OK; } /** \brief Set timeout values @param sp Pointer to spawner structure @param ct Timeout for connecting to a filter (Default: 300 seconds) @param st Timeout for sending a message to the filter (Default: 10 seconds) @param rt Timeout for reading reply from the filter (Default: 10 seconds) @param et Overall timeout between sending end-of-message to filter and waiting for the final acknowledgment. (Default: 300 seconds) */ void spawner_set_timeout(spawner* sp, unsigned int ct, unsigned int st, unsigned int rt, unsigned int et) { if (ct>0) sp->connect_timeout=ct; if (st>0) sp->send_timeout=st; if (rt>0) sp->recv_timeout=rt; if (et>0) sp->eom_timeout=et; } /** \brief Set the strict flag @param sp Pointer to spawner structure @param strict */ void spawner_set_strict_state(spawner* sp, int strict) { sp->strict_state = strict; } /** \brief Get value of strict flag @param sp Pointer to spawner structure @return 1 if sp is set to strict mode, 0 otherwise. */ int spawner_get_strict_state(spawner* sp) { return sp->strict_state; } /** \brief Set protocol flags @param sp Pointer to spawner structure @param flags Protocol flags @return SPAWNER_OK if called before spawner_open(), SPAWNER_ERROR_STATE otherwise. */ int spawner_set_flags(spawner* sp, u_int32_t flags) { if (!is_valid_state(sp->state,STATE_INIT)) return SPAWNER_ERROR_STATE; sp->flags = flags; return SPAWNER_OK; } /** \brief Get protocol flags @param sp Pointer to spawner structure @return Protocol flags */ u_int32_t spawner_get_flags(spawner *sp) { return sp->flags; } /** \brief Get actions that might be requested by mail filter @param sp Pointer to spawner structure @return Actions */ u_int32_t spawner_get_actions(spawner *sp) { return sp->actions; } /** \brief Set milter name @param sp Pointer to spawner structure @param name */ void spawner_set_name(spawner *sp, char *name) { free(sp->name); sp->name = NULL; if (name) sp->name = strdup(name); } /** \brief Get milter name @param sp Pointer to spawner structure @return Name that has been registered via spawner_set_name() or spawner_set_spec() */ char* spawner_get_name(spawner *sp) { return strdup(sp->name); } /** \brief Convert timestring into number of seconds @param s String, e.g. "60s", "3m" @return number of seconds */ int spawner_parsetime(char* s) { char* c = s; while (isspace(*c)) c++; int i = atoi(c); while (isdigit(*c)) c++; switch (*c) { case 'm': i*=60; break; case 's': break; default: i=0; break; } return i; } /** \brief Set specification for remote socket @param sp Pointer to spawner structure @param spec * Supported formats: - "inet: port @ host" - "inet6: port @ host" - "local: /path/to/socket" - "unix: /path/to/socket" * Examples (from the Sendmail Installation And Operation Guide) - filter1, S=local:/var/run/f1.sock, F=R - filter2, S=inet6:999\@localhost, F=T, T=S:1s;R:1s;E:5m - filter3, S=inet:3333\@localhost, T=C:2m @return SPAWNER_OK if called before spawner_open(), SPAWNER_ERROR_STATE otherwise. */ int spawner_set_spec(spawner *sp, char *spec) { char *tmpstr; char *s, *e, *f, *g; int res; struct addrinfo hints; struct addrinfo *lookupresult = NULL; struct addrinfo *r; if (!is_valid_state(sp->state,STATE_INIT)) return SPAWNER_ERROR_STATE; /* working copy */ tmpstr = strdup(spec); if (tmpstr == NULL) { sp->error = ENOMEM; res = SPAWNER_ERROR; goto exit; } s = tmpstr; do { while (isspace(*s)) s++; ( e = strchr(s, ',')) && (*e = 0); if (! sp->name) { /* Filter name in s..e */ sp->name=strdup(s); } else if (strncmp(s, "S=", 2) == 0) { f=s+2; /* Connection info in f..e */ if (strncmp(f, "local:", 6) == 0) { sp->af = AF_UNIX; sp->salen = sizeof(struct sockaddr_un); f += 6; } else if (strncmp(f, "unix:", 5) == 0) { sp->af = AF_UNIX; sp->salen = sizeof(struct sockaddr_un); f += 5; } else if (strncmp(f, "inet:", 5) == 0) { sp->af = AF_INET; sp->salen = sizeof(struct sockaddr_in); f += 5; } else if (strncmp(f, "inet6:", 6) == 0) { sp->af = AF_INET6; sp->salen = sizeof(struct sockaddr_in6); f += 6; } else { res = SPAWNER_ERROR_ARG; goto exit; } /* FIXME read port */ while (isspace(*f)) f++; if ((sp->sa = malloc(sp->salen)) == NULL) { sp->error = ENOMEM; res = SPAWNER_ERROR; goto exit; } if (sp->af == AF_UNIX) { ((struct sockaddr_un*)sp->sa)->sun_family = AF_UNIX; strncpy( ((struct sockaddr_un*)sp->sa)->sun_path, f, 108 ); } else { g=f; /* FIXME: Should symbolic service names be allowed? */ /* Check for port number*/ while (isdigit(*g)) g++; if (f == g) { res = SPAWNER_ERROR_ARG; goto exit; } while (isspace(*g)) g++; /* Check for trailing garbage before @ sign */ if (*g != '@') { res = SPAWNER_ERROR_ARG; goto exit; } *g='\0'; g++; while (isspace(*g)) g++; /* host points to host now. */ /* Hostname lookup */ hints.ai_flags=0; hints.ai_family=sp->af; hints.ai_socktype=SOCK_STREAM; hints.ai_protocol=0; /* FIXME: This needs decent error codes */ if (getaddrinfo(f, g, &hints, &lookupresult) != 0) { sp->error = EINVAL; /* FIXME: Find better error code */ res = SPAWNER_ERROR_IO; goto exit; } for (r=lookupresult; r->ai_next != NULL; r = r->ai_next) { if (r->ai_family == sp->af) break; } if (r->ai_family != sp->af) { sp->error = EINVAL; /* FIXME: Find better error code */ res = SPAWNER_ERROR_IO; goto exit; } memcpy(sp->sa, r->ai_addr, sp->salen); } } else if (strncmp(s, "F=", 2) == 0) { f=s+2; /* Flags in f..e */ if (*f == 'R') { /* reject */ } else if (*f == 't') { /* temporary error */ } else { /* error */ }; } else if (strncmp(s, "T=", 2) == 0) { f=s+2; /* Timeout flags in f..e */ do { while (isspace(*f)) f++; ( g = strchr(f, ';')) && (*g = 0); if (strncmp(f, "C:", 2)) { sp->connect_timeout = spawner_parsetime(f+2); } else if (strncmp(f, "S:", 2)) { sp->send_timeout = spawner_parsetime(f+2); } else if (strncmp(f, "R:", 2)) { sp->recv_timeout = spawner_parsetime(f+2); } else if (strncmp(f, "E:", 2)) { sp->eom_timeout = spawner_parsetime(f+2); } else { res = SPAWNER_ERROR_ARG; goto exit; } f=g+1; } while (g); } s=e+1; } while (e); exit: /*XXX*/ return SPAWNER_OK; } /** \brief Get spec string @param sp Pointer to spawner structure @return spec string */ char* spawner_get_spec(spawner *sp) { return strdup(sp->spec); } /** \brief Register callback function for the "add recipient" modification action @param sp Pointer to spawner structure @param add_rcpt Callback function @return SPAWNER_OK if called before spawner_open(), SPAWNER_ERROR_STATE otherwise. */ int spawner_callback_add_rcpt(spawner* sp, void (*add_rcpt)(spawner*, char*)) { if (!is_valid_state(sp->state,STATE_INIT)) return SPAWNER_ERROR_STATE; sp->add_rcpt = add_rcpt; if (add_rcpt != NULL) sp->actions |= SMFIF_ADDRCPT; else sp->actions &= ~SMFIF_ADDRCPT; return SPAWNER_OK; } /** \brief Register callback function for the "delete recipient" modification action @param sp Pointer to spawner structure @param del_rcpt Callback function @return SPAWNER_OK if called before spawner_open(), SPAWNER_ERROR_STATE otherwise. */ int spawner_callback_del_rcpt(spawner* sp, void (*del_rcpt)(spawner*, char*)) { if (!is_valid_state(sp->state,STATE_INIT)) return SPAWNER_ERROR_STATE; sp->del_rcpt = del_rcpt; if (del_rcpt != NULL) sp->actions |= SMFIF_DELRCPT; else sp->actions &= ~SMFIF_DELRCPT; return SPAWNER_OK; } /** \brief Register callback function for the "add header" modification action @param sp Pointer to spawner structure @param add_header Callback function @return SPAWNER_OK if called before spawner_open(), SPAWNER_ERROR_STATE otherwise. */ int spawner_callback_add_header(spawner* sp, void (*add_header)(spawner*, char*, char*)) { if (!is_valid_state(sp->state,STATE_INIT)) return SPAWNER_ERROR_STATE; sp->add_header = add_header; if (add_header != NULL) sp->actions |= SMFIF_ADDHDRS; else sp->actions &= ~SMFIF_ADDHDRS; return SPAWNER_OK; } /** \brief Register callback function for the "change header" modification action @param sp Pointer to spawner structure @param change_header Callback function @return SPAWNER_OK if called before spawner_open(), SPAWNER_ERROR_STATE otherwise. */ int spawner_callback_change_header(spawner* sp, void (*change_header)(spawner*, int, char*, char*)) { if (!is_valid_state(sp->state,STATE_INIT)) return SPAWNER_ERROR_STATE; sp->change_header = change_header; if (change_header != NULL) sp->actions |= SMFIF_CHGHDRS; else sp->actions &= ~SMFIF_CHGHDRS; return SPAWNER_OK; } /** \brief Register callback function for the "replace body" modification action @param sp Pointer to spawner structure @param replace_body Callback function @return SPAWNER_OK if called before spawner_open(), SPAWNER_ERROR_STATE otherwise. */ int spawner_callback_replace_body(spawner* sp, void (*replace_body)(spawner*, int, char*)) { if (!is_valid_state(sp->state,STATE_INIT)) return SPAWNER_ERROR_STATE; sp->replace_body = replace_body; if (replace_body != NULL) sp->actions |= SMFIF_CHGBODY; else sp->actions &= ~SMFIF_CHGBODY; return SPAWNER_OK; } /** \brief Register callback function for the "quarantine" modification action @param sp Pointer to spawner structure @param quarantine Callback function @return SPAWNER_OK if called before spawner_open(), SPAWNER_ERROR_STATE otherwise. */ int spawner_callback_quarantine(spawner* sp, void (*quarantine)(spawner*, char*)) { if (!is_valid_state(sp->state,STATE_INIT)) return SPAWNER_ERROR_STATE; sp->quarantine = quarantine; if (quarantine != NULL) sp->actions |= SMFIF_QUARANTINE; else sp->actions &= ~SMFIF_QUARANTINE; return SPAWNER_OK; } /** \brief Register callback function for debug messages @param sp Pointer to spawner structure @param debug_print Function that will be called by spawner_debug() */ void spawner_callback_debug(spawner* sp, void(*debug_print)(spawner*, int, char*)) { sp->debug = debug_print; } /** \brief Default debug function @param sp Pointer to spawner structure @param level @param buf buffer Prints debug info */ void spawner_stdout_debug(spawner *sp, int level, char *buf) { (void)sp; printf("<%i> ", level); printf(buf); return; } /** \brief Debug output function @param sp Pointer to spawner structure @param level @param format */ void spawner_debug(spawner *sp, int level, char *format, ...) { char buf[DEBUG_BUFSIZE]; va_list ap; va_start(ap, format); vsnprintf(buf, DEBUG_BUFSIZE, format, ap); if (sp->debug) sp->debug(sp, level, buf); va_end(ap); return; } /** \brief Determine a set of valid next states that have been negotiated on initial socket connection. @param sp Pointer to spawner structure @param state Valid steps for the next command, before considering sp->flags In spawner_open(), the MTA and the mail filter negotiate flags and actions. Flags determine which steps of the Milter protocola (CONNECT, HELO, MAIL, RCPT, HEADER, EOH, BODY, EOM) have to be skipped. */ void spawner_next_state(spawner* sp, unsigned int state) { unsigned int state_ack, state_nak; unsigned int states[] = { STATE_CONNECT, STATE_HELO, STATE_MAIL, STATE_RCPT, STATE_HEADER, STATE_EOH, STATE_BODY, STATE_EOM, 0}; unsigned char i, j; /* Commands states that the mail filter will/won't accept */ state_ack = state & (~(sp->flags) | STATE_EOM ); state_nak = state & sp->flags; /* For each command state the mail filter wants skipped ... */ for (i=0;states[i] != 0;i++) { if (states[i] & state_nak) { /* ... look for the first state after that ... */ for (j=i+1; states[j] != 0;j++) { if (!(states[j] & sp->flags)) { /* and add that to state_ack */ state_ack |= states[j]; break; } } } } /* FIXME ----------------------------v */ sp->state = state_ack; spawner_debug(sp, 0, "spawner_next_state(): Setting sp->state to 0x%.4x\n", sp->state); } /** \brief Open socket connection and negotiate protocol options @param sp Pointer to spawner structure @return error value (SPAWNER_OK on success) */ int spawner_open(spawner* sp) { int res = SPAWNER_OK; smfi_msg *msg = NULL; if (sp->state != STATE_INIT) return SPAWNER_ERROR_STATE; sp->error = 0; /* Create socket */ sp->socket = socket(sp->af, SOCK_STREAM, 0); if (sp->socket == -1) { sp->error = errno; res = SPAWNER_ERROR_IO; goto exit; } if (fcntl(sp->socket, F_SETFD, FD_CLOEXEC) == -1) { sp->error = errno; res = SPAWNER_ERROR_IO; goto exit; } /* Set timeouts */ time_t now = time(NULL); time_t connect_deadline = now + sp->connect_timeout + 1; time_t saved_alarm = now + alarm(sp->eom_timeout); /* Connect socket */ while (connect(sp->socket, sp->sa, sp->salen) == -1) { now = time(NULL); if ((errno != EINTR ) || (now > connect_deadline)) { sp->error = errno; res = SPAWNER_ERROR_IO; goto exit; } else { alarm(connect_deadline - now); } } now = time(NULL); if (now < saved_alarm) alarm(saved_alarm - now); else alarm(0); /* Send optneg packet: simple packet + 3x 32bit int */ msg = malloc(sizeof(smfi_msg)+12); if (msg == NULL) { sp->error = ENOMEM; res = SPAWNER_ERROR; goto exit; } msg->size = htonl(13); msg->cmd = SMFIC_OPTNEG; OPTNEG_VERSION(msg) = htonl(2); OPTNEG_ACTIONS(msg) = htonl(sp->actions); OPTNEG_FLAGS(msg) = htonl(sp->flags); if (write_msg(sp->socket, (smfi_msg*)msg, sp->send_timeout) != 0) { spawner_debug(sp, 0, "spawner_open(): Could not write OPTNEG packet\n"); res = SPAWNER_ERROR_IO; goto exit; } free(msg); msg = NULL; /* Find out what the mail filter wants to do */ if (read_msg(sp->socket, &msg, sp->recv_timeout) != 0) { spawner_debug(sp, 0, "spawner_open(): Could not read packet (OPTNEG expected).\n"); res = SPAWNER_ERROR_IO; goto exit; } if (msg->cmd != SMFIC_OPTNEG) { spawner_debug(sp, 0, "spawner_open(): Invalid packet (OPTNEG expected).\n"); res = SPAWNER_ERROR_PROTO; goto exit; } if (msg->size != htonl(sizeof(smfi_msg)+8)) { spawner_debug(sp, 0, "spawner_open(): Invalid packet size (OPTNEG expected).\n"); res = SPAWNER_ERROR_PROTO; goto exit; } spawner_debug(sp, 0, "spawner_open(): Got OPTNEG_FLAGS(msg)=0x%.4x, msg_actions=0x%.4x\n", ntohl(OPTNEG_FLAGS(msg)), ntohl(OPTNEG_ACTIONS(msg))); /* If the mail filter has "bigger" requirements than we are able/willing to satisfy (skipping protocol steps or modification actions), exit with an error */ unsigned int rflags = ntohl(OPTNEG_FLAGS(msg)); unsigned int ractions = ntohl(OPTNEG_ACTIONS(msg)); if ( ((rflags & sp->flags) != rflags) || ((ractions & sp->actions) != ractions) ) { res = SPAWNER_ERROR_PROTO; goto exit; } else { sp->flags = ntohl(OPTNEG_FLAGS(msg)); sp->actions = ntohl(OPTNEG_ACTIONS(msg)); spawner_debug(sp, 0, "spawner_open(): Setting sp->flags=0x%.4x, sp->actions=0x%.4x\n", sp->flags, sp->actions); } spawner_next_state(sp, STATE_CONNECT); exit: free(msg); if (res != SPAWNER_OK) { if (sp->socket != 0) (void) spawner_close(sp); } return res; } /** \brief Gracefully close the connection to the mail filter @param sp Pointer to spawner structure @return error value (SPAWNER_OK on success) */ int spawner_close(spawner* sp) { /* Can be called from any state -- ask no questions */ sp->error = 0; /* FIXME set state */ if (sp->socket != -1) { spawner_send_cmd_simple(sp, SMFIC_QUIT); close (sp->socket); sp->socket = 0; return SPAWNER_OK; } else { return SPAWNER_ERROR; } } /** \brief Send macros @param sp Pointer to spawner structure @param cmd Command with which the macro is associated @param macros list of strings, interprated as macro-name/macro-value pairs @return error value (SPAWNER_OK on success) */ int spawner_send_macro(spawner* sp, char cmd, char** macros) { smfi_msg* msg = NULL; int i, j; int res = SPAWNER_OK; sp->error = 0; /* State check: Does the macro context match any valid next state at this point? */ if ( !(((cmd == SMFIC_CONNECT) && is_valid_state(sp->state, STATE_CONNECT)) || ((cmd == SMFIC_HELO) && is_valid_state(sp->state, STATE_HELO)) || ((cmd == SMFIC_MAIL) && is_valid_state(sp->state, STATE_MAIL)) || ((cmd == SMFIC_RCPT) && is_valid_state(sp->state, STATE_RCPT)) )) return invalid_state(sp); /* Calculate total length of key/value strings (j) */ for (i=0, j=1; macros[i] != NULL ; i+=2) { j += strlen(macros[i]) + 1; j += strlen(macros[i+1]) + 1; } msg = malloc(sizeof(smfi_msg) + j); if (msg == NULL) { sp->error = ENOMEM; res = SPAWNER_ERROR; goto exit; } msg->size = htonl(j + 1); msg->cmd = SMFIC_MACRO; msg->data[0] = cmd; /* fill structure */ for (i=0, j=1; macros[i] != NULL ; i+=2) { strcpy(&msg->data[j], macros[i]); j += strlen(macros[i]) + 1; strcpy(&msg->data[j], macros[i+1]); j += strlen(macros[i+1]) + 1; } /* send it */ if (write_msg(sp->socket, msg, sp->send_timeout) != 0) { res = SPAWNER_ERROR_IO; goto exit; } exit: free(msg); return res; } /** \brief Send information about newly accepted SMTP connection @param sp Pointer to spawner structure @param hostname @param family Address family (AF_INET, AF_INET6, AF_UNIX, AF_UNSPEC) @param port @param address textual representation of the numeric address @return error value (SPAWNER_OK on success) */ int spawner_new_connection(spawner* sp, char* hostname, int family, u_int16_t port, char* address) { int h, a, size; int res; u_int16_t p; smfi_msg* msg = NULL; if (! is_valid_state(sp->state, STATE_CONNECT)) return invalid_state(sp); if (hostname == NULL) hostname=""; if (address == NULL) address=""; sp->error = 0; h = strlen(hostname) + 1; a = strlen(address) + 1; /* host + 1 byte "family" + 2 bytes (port) + address */ size = h + 3 + a; msg = malloc(sizeof(smfi_msg) + size); bzero(msg, sizeof(smfi_msg) + size); if (msg == NULL) { sp->error = ENOMEM; res = SPAWNER_ERROR; goto exit; } msg->size = htonl(size + 1); msg->cmd = SMFIC_CONNECT; strcpy(msg->data, hostname); switch(family) { case AF_UNIX: msg->data[h] = SMFIA_UNIX; break; case AF_INET: msg->data[h] = SMFIA_INET; break; case AF_INET6: msg->data[h] = SMFIA_INET6; break; case AF_UNSPEC: msg->data[h] = SMFIA_UNKNOWN; break; default: res = SPAWNER_ERROR; goto exit; } /* Fill in port and address field */ if ((family == AF_INET) || (family == AF_INET6)) { p = htons(port); memcpy(&(msg->data[h+1]), &p, sizeof(p)); strcpy(&(msg->data[h+3]), address); } else { msg->data[h+1] = msg->data[h+2] = 0; strcpy(&(msg->data[h+3]), ""); } if (write_msg(sp->socket, (smfi_msg*)msg, sp->send_timeout) != 0) { res = SPAWNER_ERROR_IO; goto exit; } res = spawner_recv_response(sp); if (res >= 0) spawner_next_state(sp, STATE_HELO); exit: free(msg); return res; } /** \brief Send HELO/EHLO string @param sp Pointer to spawner structure @param helostr Argument(s) to HELO/EHLO command @return error value (SPAWNER_OK on success) */ int spawner_helo(spawner* sp, char* helostr) { int h; int res; smfi_msg* msg = NULL; if (!is_valid_state(sp->state, STATE_HELO)) return invalid_state(sp); sp->error = 0; if (helostr == NULL) helostr=""; /* Strings need to be null-terminated */ h = strlen(helostr) + 1; msg = malloc(sizeof(smfi_msg) + h ); if (msg == NULL) { sp->error = ENOMEM; res = SPAWNER_ERROR; goto exit; } msg->size = htonl(h + 1); msg->cmd = SMFIC_HELO; strcpy(msg->data, helostr); if (write_msg(sp->socket, msg, sp->send_timeout) != 0) { res = SPAWNER_ERROR_IO; goto exit; } res = spawner_recv_response(sp); if (res >= 0) spawner_next_state(sp, STATE_MAIL); exit: free(msg); return res; } /** \brief Set envelope header @param sp Pointer to spawner structure @param sender Sender address, as given in "MAIL FROM:" command @param extra Extra arguments @return error value (SPAWNER_OK on success) */ int spawner_set_from(spawner* sp, char* sender, char* extra) { int res; if (!is_valid_state(sp->state, STATE_MAIL)) return invalid_state(sp); sp->error = 0; res = spawner_send_cmd_twostrings(sp, SMFIC_MAIL, sender, extra); res = spawner_recv_response(sp); if (res >= 0) spawner_next_state(sp, STATE_RCPT); return res; } /** \brief Add envelope recipient @param sp Pointer to spawner structure @param rcpt Recipient address, as given in "RCPT TO:" command @param extra Extra arguments @return error value (SPAWNER_OK on success) */ int spawner_add_rcpt(spawner* sp, char* rcpt, char* extra) { int res; if (!is_valid_state(sp->state, STATE_RCPT)) return invalid_state(sp); sp->error = 0; res = spawner_send_cmd_twostrings(sp, SMFIC_RCPT, rcpt, extra); res = spawner_recv_response(sp); if (res >= 0) spawner_next_state(sp, STATE_RCPT | STATE_HEADER); return res; } /** \brief Send header @param sp Pointer to spawner structure @param name Header name, not including ':' character @param value Header value @return error value (SPAWNER_OK on success) */ int spawner_send_header(spawner* sp, char* name, char* value) { int res; if (!is_valid_state(sp->state, STATE_HEADER)) return invalid_state(sp); /* Strip trailing newline */ if (value[strlen(value)-1] == '\n') value[strlen(value)-1] = '\0'; sp->error = 0; res = spawner_send_cmd_twostrings(sp, SMFIC_HEADER, name, value); res = spawner_recv_response(sp); if (res >= 0) spawner_next_state(sp, STATE_HEADER | STATE_EOH); return res; } /** \brief Set end of headers @param sp Pointer to spawner structure @return error value (SPAWNER_OK on success) */ int spawner_send_eoh(spawner* sp) { int res; if (!is_valid_state(sp->state, STATE_EOH)) return invalid_state(sp); sp->error = 0; res = spawner_send_cmd_simple(sp, SMFIC_EOH); res = spawner_recv_response(sp); if (res >= 0) spawner_next_state(sp, STATE_BODY | STATE_EOM); return res; } /** \brief Send body chunk @param sp Pointer to spawner structure @param size Chunk size @param buf Chunk @return error value (SPAWNER_OK on success) */ int spawner_send_body_chunk(spawner* sp, int size, char* buf) { int res; smfi_msg* msg = NULL; if (!is_valid_state(sp->state, STATE_BODY)) return invalid_state(sp); sp->error = 0; msg = malloc(sizeof(smfi_msg) + size); if (msg == NULL) { sp->error = ENOMEM; res = SPAWNER_ERROR; goto exit; } memcpy(msg->data, buf, size); msg->size = htonl(size + 1); msg->cmd = SMFIC_BODY; if (write_msg(sp->socket, (smfi_msg*)msg, sp->send_timeout) != 0) { res = SPAWNER_ERROR_IO; goto exit; } res = spawner_recv_response(sp); if (res >= 0) spawner_next_state(sp, STATE_BODY | STATE_EOM); exit: free(msg); return res; } /** \brief Set end of message @param sp Pointer to spawner structure @return error value (SPAWNER_OK on success) */ int spawner_send_eom(spawner* sp) { int res; if (!is_valid_state(sp->state, STATE_EOM)) return invalid_state(sp); sp->error = 0; res = spawner_send_cmd_simple(sp, SMFIC_BODYEOB); res = spawner_recv_eom_response(sp); if (res >= 0) spawner_next_state(sp, STATE_CONNECT); return res; } /** \brief Read mail body from file descriptor and send it to mail filter, including end-of-mail message @param sp Pointer to spawner structure @param fd file descriptor @return error value (SPAWNER_OK on success) */ int spawner_send_body_fd(spawner* sp, int fd) { int res; char buf[BODY_CHUNKSIZE]; int s; while ( (s=read(fd, buf, BODY_CHUNKSIZE)) != 0 ) { if (s == -1) { if (errno == EINTR) { continue; } else { res=SPAWNER_ERROR_IO; goto exit; } } res=spawner_send_body_chunk(sp, s, buf); if (res != SPAWNER_CONTINUE) goto exit; } res = spawner_send_eom(sp); exit: return res; } /** \brief read mail body from filehandle and send it to mail filter, including end-of-mail message @param sp Pointer to spawner structure @param fh filehandle @return error value (SPAWNER_OK on success) */ int spawner_send_body_fh(spawner* sp, FILE* fh) { int res; char buf[BODY_CHUNKSIZE]; int s; while ( !feof(fh) ) { s=fread(buf, 1, BODY_CHUNKSIZE, fh); if (s < BODY_CHUNKSIZE) { if (ferror(fh)) { res=SPAWNER_ERROR_IO; goto exit; } } res=spawner_send_body_chunk(sp, s, buf); if (res != SPAWNER_CONTINUE) goto exit; } res = spawner_send_eom(sp); exit: return res; } /** \brief Reset milter connection. @param sp Pointer to spawner structure @return error value (SPAWNER_OK on success) */ int spawner_reset(spawner* sp) { int res = SPAWNER_OK; /* No state check necessary */ sp->error = 0; /* No response expected */ res = spawner_send_cmd_simple(sp, SMFIC_ABORT); if (res >= 0) spawner_next_state(sp, STATE_HELO); return res; } /* Local Variables: c-file-style: "bsd" c-basic-offset: 8 indent-tabs-mode: nil End: */