/* * lmtp2nntp.c * * The lmtp2nntp program reads mail as a LMTP server and posts it to one or * more newsgroups using NNTP. It delivers the message immediately or fails. * * The OSSP Project, Cable & Wireless Deutschland GmbH * Thomas Lotterer, * */ #include #include #include #include #include #include /* third party */ #include "str.h" #include "argz.h" /* own headers */ #include "lmtp.h" #include "nntp.h" #include "sa.h" #include "msg.h" #include "trace.h" #define _VERSION_C_AS_HEADER_ #include "version.c" #undef _VERSION_C_AS_HEADER_ #ifndef FALSE #define FALSE (1 != 1) #endif #ifndef TRUE #define TRUE (!FALSE) #endif #define ERR_EXECUTION -1 #define ERR_DELIVERY -2 #define MESSAGE_MAXLEN 8*1024*1024 #define STDSTRLEN 128 #define MAXNEWSSERVICES 3 extern void lmtp_debug_dumplmtp(lmtp_t *lmtp); static lmtp_rc_t lmtp_cb_lhlo (lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx); static lmtp_rc_t lmtp_cb_mail (lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx); static lmtp_rc_t lmtp_cb_rcpt (lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx); static lmtp_rc_t lmtp_cb_data (lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx); static lmtp_rc_t lmtp_cb_noop (lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx); static lmtp_rc_t lmtp_cb_rset (lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx); static lmtp_rc_t lmtp_cb_quit (lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx); static int helo_rfc0821domain(char *msg, char **domain); static int helo_rfc1035domain(char *msg, char **domain); struct session { int lhlo_seen; char *lhlo_domain; }; static void initsession(struct session *session); static void resetsession(struct session *session); struct ns { char *h; /* host */ char *p; /* port */ sa_t *sa; int s; /* socket */ nntp_t *nntp; }; typedef struct { int option_verbose; int option_tracing; int option_groupmode; int option_deliverymode; char *option_deliverymodefakestatus; char *option_deliverymodefakedsn; int nsc; struct ns ns[MAXNEWSSERVICES]; char *azGroupargs; size_t asGroupargs; struct session session; msg_t *msg; } lmtp2nntp_t; enum { GROUPMODE_ARG, GROUPMODE_ENVELOPE, GROUPMODE_HEADER }; enum { DELIVERYMODE_FAKE, DELIVERYMODE_POST, DELIVERYMODE_FEED }; /* * print usage information */ static void usage(char *command) { fprintf(stderr, "USAGE: %s [-p protocol] [-l logtarget] " "[-h host[:port]] [-m mode] [-t] [-v] newsgroup [newsgroup ...]\n", command); return; } int main(int argc, char **argv) { int rc = 0; lmtp_t *lmtp; lmtp_io_t lmtp_io; lmtp2nntp_t *ctx; int i; /* general purpose scratch int, index ... */ char *progname; char *cpHost; char *cpPort; sa_t *sa; #if 0 /* begin NNTP posting test */ { nntp_post(nntp, "..."); nntp_destroy(nntp); sock_destroy(s); exit(0); } #endif progname = argv[0]; /* create application context */ if ((ctx = (lmtp2nntp_t *)malloc(sizeof(lmtp2nntp_t))) == NULL) exit(ERR_EXECUTION); ctx->option_verbose = FALSE; ctx->option_tracing = FALSE; ctx->option_groupmode = GROUPMODE_ARG; ctx->option_deliverymode = DELIVERYMODE_FAKE; ctx->option_deliverymodefakestatus = "553"; /* Requested action not taken: mailbox name not allowed */ ctx->option_deliverymodefakedsn = "5.7.1"; /* Delivery not authorized, message refused */ ctx->nsc = 0; for (i=0; i < MAXNEWSSERVICES; i++) { ctx->ns[i].h = ""; ctx->ns[i].s = -1; ctx->ns[i].nntp = NULL; } ctx->azGroupargs = NULL; ctx->asGroupargs = 0; initsession(&ctx->session); ctx->msg = NULL; #if 1 { char buf[1000]; int bufused = 0; int tracefile; for (i=0; ioption_deliverymode = DELIVERYMODE_POST; else if (strcasecmp(optarg, "feed") == 0) ctx->option_deliverymode = DELIVERYMODE_FEED; else { if (strlen(optarg) != 9) { fprintf(stderr, "%s:Error: Invalid format or length \"%s\" to option -d\n", progname, optarg); exit(ERR_EXECUTION); } if (optarg[3] != '/') { fprintf(stderr, "%s:Error: Invalid format or missing slash \"%s\" to option -d\n", progname, optarg); exit(ERR_EXECUTION); } optarg[3] = '\0'; ctx->option_deliverymodefakestatus = &optarg[0]; ctx->option_deliverymodefakedsn = &optarg[4]; if ( strlen(ctx->option_deliverymodefakestatus) != 3 || !isdigit(ctx->option_deliverymodefakestatus[0]) || !isdigit(ctx->option_deliverymodefakestatus[1]) || !isdigit(ctx->option_deliverymodefakestatus[2])) { fprintf(stderr, "%s:Error: Invalid status in format \"%s\" to option -d\n", progname, optarg); exit(ERR_EXECUTION); } if ( (strlen(ctx->option_deliverymodefakedsn) != 5) || !isdigit(ctx->option_deliverymodefakedsn[0]) || (ctx->option_deliverymodefakedsn[1] != '.') || !isdigit(ctx->option_deliverymodefakedsn[2]) || (ctx->option_deliverymodefakedsn[3] != '.') || !isdigit(ctx->option_deliverymodefakedsn[4]) || (ctx->option_deliverymodefakedsn[0] != ctx->option_deliverymodefakestatus[0])) { fprintf(stderr, "%s:Error: Invalid dsn in format \"%s\" to option -d\n", progname, optarg); exit(ERR_EXECUTION); } } break; case 'g': /* -g groupmode */ if (strcasecmp(optarg, "arg") == 0) ctx->option_groupmode = GROUPMODE_ARG; else if (strcasecmp(optarg, "envelope") == 0) ctx->option_groupmode = GROUPMODE_ENVELOPE; else if (strcasecmp(optarg, "header") == 0) ctx->option_groupmode = GROUPMODE_HEADER; else { fprintf(stderr, "%s:Error: Invalid mode \"%s\" to option -g\n", progname, optarg); exit(ERR_EXECUTION); } break; case 'h': /* -h host */ if (ctx->nsc >= MAXNEWSSERVICES) { fprintf(stderr, "%s:Error: Too many services (%d) using option -h\n", progname, ctx->nsc); exit(ERR_EXECUTION); } /* parse host[:port] string into host and port */ cpHost = strdup(optarg); if ((cpPort = strrchr(cpHost, ':')) != NULL) { *cpPort++ = '\0'; cpPort = strdup(cpPort); } else cpPort = strdup("nntp"); ctx->ns[ctx->nsc].h = cpHost; ctx->ns[ctx->nsc].p = cpPort; if ((sa = sa_create(SA_IP, "tcp", ctx->ns[ctx->nsc].h, ctx->ns[ctx->nsc].p)) == NULL) { fprintf(stderr, "%s:Error: creating TCP socket address failed for \"%s:%s\": %s\n", progname, ctx->ns[ctx->nsc].h, ctx->ns[ctx->nsc].p, strerror(errno)); exit(ERR_EXECUTION); } if ((ctx->ns[ctx->nsc].s = socket(sa->sa_buf->sa_family, SOCK_STREAM, sa->sa_proto)) == -1) { fprintf(stderr, "%s:Error: Creating TCP socket failed for \"%s:%s\": %s\n", progname, ctx->ns[ctx->nsc].h, ctx->ns[ctx->nsc].p, strerror(errno)); exit(ERR_EXECUTION); } ctx->ns[ctx->nsc].sa = sa; //FIXME sa_destroy(sa); ctx->ns[ctx->nsc].nntp = NULL; ctx->nsc++; break; case 't': // -t (tracing) ctx->option_tracing = TRUE; trace_read (-1, optarg, 0); trace_write(-1, optarg, 0); break; case 'v': // -v (verbose) ctx->option_verbose = TRUE; break; case 'V': // -V (version) fprintf(stdout, "%s\n", lmtp2nntp_version.v_gnu); exit(0); break; case '?': default: usage(progname); exit(ERR_EXECUTION); } } /* remaining arguments are Groupargs */ for (i = optind; i < argc; i++) { //fprintf(stderr, "DEBUG: argv[i] = ***%s***\n", argv[i]); argz_add(&ctx->azGroupargs, &ctx->asGroupargs, argv[i]); } /* initialize LMTP context */ lmtp_io.select = NULL; lmtp_io.read = trace_read; lmtp_io.write = trace_write; if ((lmtp = lmtp_create(STDIN_FILENO, STDOUT_FILENO, ctx->option_tracing != TRUE ? NULL : &lmtp_io)) == NULL) { fprintf(stderr, "%s:Error: Unable to initialize LMTP library\n", progname); exit(ERR_EXECUTION); } lmtp_register(lmtp, "LHLO", lmtp_cb_lhlo, ctx, NULL, NULL); lmtp_register(lmtp, "MAIL", lmtp_cb_mail, ctx, NULL, NULL); lmtp_register(lmtp, "RCPT", lmtp_cb_rcpt, ctx, NULL, NULL); lmtp_register(lmtp, "DATA", lmtp_cb_data, ctx, NULL, NULL); lmtp_register(lmtp, "NOOP", lmtp_cb_noop, ctx, NULL, NULL); lmtp_register(lmtp, "RSET", lmtp_cb_rset, ctx, NULL, NULL); lmtp_register(lmtp, "QUIT", lmtp_cb_quit, ctx, NULL, NULL); /* loop for LMTP protocol */ lmtp_loop(lmtp); return rc; } static void resetsession(struct session *session) { //FIXME what about non-graceful aborts? if (session->lhlo_domain != NULL) free(session->lhlo_domain); initsession(session); return; } static void initsession(struct session *session) { session->lhlo_seen = FALSE; session->lhlo_domain = NULL; return; } static lmtp_rc_t lmtp_cb_lhlo(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *_ctx) { /* * RFC821 [excerpt] 4.1. SMTP COMMANDS * 4.1.1. COMMAND SEMANTICS, HELO * This command and an OK reply to it confirm that both the sender-SMTP * and the receiver-SMTP are in the initial state, that is, there is no * transaction in progress and all state tables and buffers are cleared. * * The first command in a session must be the HELO command. The HELO * command may be used later in a session as well. If the HELO command * argument is not acceptable a 501 failure reply must be returned and * the receiver-SMTP must stay in the same state. * * If the transaction beginning command argument is not acceptable a 501 * failure reply must be returned and the receiver-SMTP must stay in the * same state. If the commands in a transaction are out of order a 503 * failure reply must be returned and the receiver-SMTP must stay in the * same state. * * HELO */ lmtp2nntp_t *ctx = (lmtp2nntp_t *)_ctx; lmtp_res_t res; nntp_rc_t rc; char str[STDSTRLEN]; int bOk; int i; nntp_io_t nntp_io; nntp_io.select = NULL; nntp_io.read = trace_read; nntp_io.write = trace_write; if (ctx->session.lhlo_seen == TRUE) { res.statuscode = "503"; res.dsncode = "5.0.0"; res.statusmsg = "Duplicate LHLO."; lmtp_response(lmtp, &res); return LMTP_OK; } if (! ( helo_rfc0821domain(req->msg, &ctx->session.lhlo_domain) || helo_rfc1035domain(req->msg, &ctx->session.lhlo_domain) )) { res.statuscode = "501"; res.dsncode = "5.0.0"; res.statusmsg = "Please identify yourself. Domain must match RFC0821/RFC1035."; lmtp_response(lmtp, &res); return LMTP_OK; } if (ctx->nsc == 0) { res.statuscode = "501"; res.dsncode = "5.0.0"; res.statusmsg = "No valid NNTP Services specified."; lmtp_response(lmtp, &res); return LMTP_OK; } i = 0; do { bOk = TRUE; if (connect(ctx->ns[i].s, ctx->ns[i].sa->sa_buf, ctx->ns[i].sa->sa_len) < 0) { //fprintf(stderr, "DEBUG: connect failed: %s\n", strerror(errno)); bOk = FALSE; } if (bOk && ((ctx->ns[i].nntp = nntp_create(ctx->ns[i].s, ctx->ns[i].s, ctx->option_tracing != TRUE ? NULL : &nntp_io)) == NULL)) { //fprintf(stderr, "DEBUG: nntp_create failed: %s\n", strerror(errno)); bOk = FALSE; } if (bOk && ((rc = nntp_init(ctx->ns[i].nntp)) != NNTP_OK)) { //fprintf(stderr, "DEBUG: nntp_init failed: %s\n", nntp_error(rc)); bOk = FALSE; } if (bOk) i++; else { if (i < --ctx->nsc) { memcpy(&ctx->ns[i], &ctx->ns[i+1], (ctx->nsc - i ) * sizeof(struct ns)); } } } while (i < ctx->nsc); /* RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS * "421 Service not available, closing transmission channel [This * may be a reply to any command if the service knows it must shut down]" * * RFC1893 2. Status Codes, 3.5 Network and Routing Status * 4.X.X Persistent Transient Failure * X.4.1 No answer from host */ if (ctx->nsc == 0) { res.statuscode = "421"; res.dsncode = "4.4.1"; res.statusmsg = "No connection to any NNTP Service."; //FIXME add error strings from above DEBUGs lmtp_response(lmtp, &res); return LMTP_OK; } ctx->session.lhlo_seen = TRUE; str_format(str, sizeof(str), "FIXME.dev.de.cw.net" /* RFC2821 4.1.1.1 */ " Hello %s, pleased to meet you.\n" "ENHANCEDSTATUSCODES\n" /* RFC2034 */ "DSN\n" /* RFC1894 */ "PIPELINING\n" /* RFC1854 */ "8BITMIME", /* RFC1652 */ ctx->session.lhlo_domain); res.statuscode = "250"; res.dsncode = NULL; /* DSN not used for greeting */ res.statusmsg = str; lmtp_response(lmtp, &res); return LMTP_OK; } static int helo_rfc0821domain(char *msg, char **domain) { int rc; rc = str_parse(msg, "^.+ (" /* ## ## The mega Perl regular expression below is generated ## with the following Perl program. This is only possible ## because the given grammar is Chomsky-3 (right or left ## linear grammar, but noth both). ## # BNF grammar for according to RFC 821: # ::= one, two, or three digits representing a decimal integer value in the range 0 through 255 # ::= any one of the 52 alphabetic characters A through Z in upper case and a through z in lower case # ::= any one of the ten digits 0 through 9 # ::= | | "-" # ::= | # ::= | # ::= "." "." "." # ::= | # ::= # ::= | "#" | "[" "]" # ::= | "." # # corresponding Perl regular expression ($domain) $snum = "(?:[0-9]|[0-9]{2}|[0-1][0-9]{2}|2[0-4][0-9]|25[0-5])"; $d = "[0-9]"; $a = "[A-Za-z]"; $let_dig_hyp = "(?:$a|$d|-)"; $let_dig = "(?:$a|$d)"; $ldh_str = "${let_dig_hyp}+"; $dotnum = "$snum\\.$snum\\.$snum\\.$snum"; $number = "$d+"; $name = "$a$ldh_str$let_dig"; $element = "(?:$name|#$number|\\[$dotnum\\])"; $domain = "(?:$element\.)*$element"; # # translate into C string block suitable for passing to the Perl # Compatible Regular Expressions (PCRE) based string library Str. my $cregex = $domain; $cregex =~ s|\\|\\\\|sg; $cregex =~ s|(.{70})|"$1"\n|sg; $cregex =~ s|\n([^\n]+)$|\n"$1"|s; #FIXME this fails when last #FIXME line matches linelength exacly print "$cregex\n"; */ "(?:(?:[A-Za-z](?:[A-Za-z]|[0-9]|-)+(?:[A-Za-z]|[0-9])|#[0-9]+|\\[(?:[0" "-9]|[0-9]{2}|[0-1][0-9]{2}|2[0-4][0-9]|25[0-5])\\.(?:[0-9]|[0-9]{2}|[0" "-1][0-9]{2}|2[0-4][0-9]|25[0-5])\\.(?:[0-9]|[0-9]{2}|[0-1][0-9]{2}|2[0" "-4][0-9]|25[0-5])\\.(?:[0-9]|[0-9]{2}|[0-1][0-9]{2}|2[0-4][0-9]|25[0-5" "])\\]).)*(?:[A-Za-z](?:[A-Za-z]|[0-9]|-)+(?:[A-Za-z]|[0-9])|#[0-9]+|\\" "[(?:[0-9]|[0-9]{2}|[0-1][0-9]{2}|2[0-4][0-9]|25[0-5])\\.(?:[0-9]|[0-9]" "{2}|[0-1][0-9]{2}|2[0-4][0-9]|25[0-5])\\.(?:[0-9]|[0-9]{2}|[0-1][0-9]{" "2}|2[0-4][0-9]|25[0-5])\\.(?:[0-9]|[0-9]{2}|[0-1][0-9]{2}|2[0-4][0-9]|" "25[0-5])\\])" ")$", domain); return rc; } static int helo_rfc1035domain(char *msg, char **domain) { int rc; rc = str_parse(msg, "^.+ (" /* ## ## The mega Perl regular expression below is generated ## with the following Perl program. This is only possible ## because the given grammar is Chomsky-3 (right or left ## linear grammar, but noth both). ## # BNF grammar for according to RFC1035: # ::= any one of the 52 alphabetic characters A through Z in upper case and a through z in lower case # ::= any one of the ten digits 0 through 9 # ::= | # ::= | "-" # ::= | #