/* * sma -- Sendmail log analyser * * Copyright (c) 2000 - 2003 Jarkko Turkulainen. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY JARKKO TURKULAINEN ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL JARKKO TURKULAINEN BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $Date: 2003/04/27 15:43:33 $ */ #include "sma.h" #include /* Delivery status flags */ #define STATUS_SENT 1 #define STATUS_DEF 2 #define STATUS_USER 3 #define STATUS_QUEUE 4 #define STATUS_HOST 5 #define STATUS_SERV 6 #define STATUS_OTHER 7 /* Message ID flags: */ #define NOT_DELIVERED 0 #define FIRST_DELIVERY 1 #define DELIVERED 2 /* Message ID checking mode */ #define KEEP 0 #define REMOVE 1 /* Action flags */ #define ACTION_FROM 0 #define ACTION_TO 1 #define ACTION_SYSERR 2 #define ACTION_RULESET 3 #define ACTION_DAEMON 4 #define ACTION_DATAB 5 #define ACTION_DSN 6 #define ACTION_USER 7 #define ACTION_UNKNOWN 100 /* ACTION_SYSERR flags */ #define OTHER 0 #define HOPCOUNT 1 #define LOOP 2 #ifdef USE_REGEXP int check_filter(const char *, regex_t *, const char *); #else int check_filter(const char *, const char *); #endif /* * Do parsing for each input file */ void parse(FILE *fp, const char *file) { /* input buffer */ char line[2048]; /* plenty of pointers for line splitting: */ static const char *env_rec[128]; char *head[64]; char *info[64]; /* collecting */ char *action, *dsn, *idptr; char *messageid = NULL; const char *tmphost; int logformat, hcount, icount; int status = 0, idsize = 0, gf, pf = 0, actn = 0, sunos = 0; int actflag, year, mon, day, hh, min, sec; struct msgid *tmsgid = NULL; /* general purpose: */ const char *p = NULL; const char *s = NULL; int i, k, lcount; /* status and ruleset pointers */ const char *rulestr = NULL; const char *relstr = NULL; const char *statstr = NULL; /* temporary pointer to host struct: */ struct host *hptr; /* Start read file line by line: */ lcount = 0; while (fgets(line, 2048, fp)) { lcount++; /* null-terminate: */ line[strlen(line)-1] = '\0'; memset(head, 0, 64); memset(info, 0, 64); /* split line initially with ',': */ info[0] = line; for (i = 1, k = 0; line[k] != '\0'; ) { if (line[k] == ',') { line[k] = '\0'; k++; if (line[k] == ' ') k++; info[i] = &line[k]; i++; } else k++; } icount = i; /* split first info with ' ': */ head[0] = info[0]; for (i = 1, k = 0; info[0][k] != '\0'; ) { if (info[0][k] == ' ') { info[0][k] = '\0'; k++; while(info[0][k] == ' ') k++; head[i] = &info[0][k]; i++; } else k++; } hcount = i; /* * Initial sanity check: * Try to figure out the input file format * Currently supported are syslog and NT * * Sendmail for NT * Assume that the second field is "sendmail" .. * This may be wrong, don't know NT version enough. */ idptr = NULL; action = NULL; actn = 0; dsn = NULL; if (head[5] && !strncmp("sendmail", head[2], 7)) { logformat = 1; if (sscanf(head[1], "%d:%d:%d", &hh, &min, &sec) != 3) { if (!qflag) fprintf(stderr, "%s: not NT log, skipping line %d, " "file %s\n", pname, lcount, file); continue; } if (sscanf(head[0], "%d/%d/%d",&mon,&day,&year) != 3) { if (!qflag) fprintf(stderr, "%s: not NT log, skipping line %d, " "file %s\n", pname, lcount, file); continue; } /* We assume things fixed with NT log.. */ if (hcount >= 6) { idptr = head[4]; action = head[5]; actn = 5; dsn = head[6]; mon--; } /* * Normal syslog */ } else { logformat = 0; if (!head[5] || (Lchar && strncmp(Lchar, head[4], strlen(Lchar)))) { if (!qflag) fprintf(stderr, "%s: not syslog, skipping line %d, " "file %s\n", pname, lcount, file); continue; } if (sscanf(head[2], "%d:%d:%d", &hh, &min, &sec) != 3) { if (!qflag) fprintf(stderr, "%s: not syslog, skipping line %d, " "file %s\n", pname, lcount, file); continue; } day = atoi(head[1]); mon = conv_mon(head[0]); /* SunOS 5.x with ID generation enabled: */ if (!strncmp("[ID", head[5], 3)) { sunos = 3; } else sunos = 0; /* * Now, we run through the first info-vector (head) * and try to find useful keywords. If found, copy * the pointers as idptr, action and dsn. Action * flags are later set according to these keywords. */ for (k = 6+sunos; k < hcount; k++ ) { /* "normal" delivery lines */ if (!strncmp("daemon", head[k], 6) || !strncmp("ruleset", head[k], 7) || !strncmp("to=", head[k], 3) || !strncmp("from=", head[k], 5) || !strncmp("SYSERR", head[k], 6) || !strncmp("database", head[k], 8)) { actn = k; idptr = head[k-1]; action = head[k]; break; /* DSN, and other strange stuff: */ } else if (!strncmp("DSN", head[k], 3) || !strncmp("User", head[k], 4) || !strncmp("postmaster", head[k],10) || !strncmp("return", head[k], 6)) { idptr = head[k-2]; action = head[k-1]; dsn = head[k]; break; } } } /* * Permitted actions are defined here. * * We test the action, dsn, idptr and other fields for * keywords and set the flag actflag according to a * defined action. These actions are later performed * with a switch(actflag) statement. * */ actflag = ACTION_UNKNOWN; if (idptr && !strncmp("ruleset", idptr, 7)) { actflag = ACTION_RULESET; } else if (action && !strncmp("ruleset", action, 7)) { actflag = ACTION_RULESET; } else if (action && !strncmp("from=", action, 5)) { actflag = ACTION_FROM; } else if (action && !strncmp("to=", action, 3)) { actflag = ACTION_TO; } else if (action && !strncmp("SYSERR", action, 6)) { actflag = ACTION_SYSERR; } else if (action && !strncmp("daemon", action, 6)) { actflag = ACTION_DAEMON; } else if (action && !strncmp("database", action, 8)) { actflag = ACTION_DATAB; } else if (dsn && (!strncmp("User", dsn, 4))) { actflag = ACTION_USER; } else if (dsn) { actflag = ACTION_DSN; } else continue; /* * Initialize the host structure * If the hostname is not given, use syslog or * (in case of NT) default string */ if (Hchar) tmphost = Hchar; else if (logformat) tmphost = HOSTNAME; else tmphost = head[3]; hptr = init_host(tmphost); /* * Rejected before SMTP "MAIL FROM" * This should be under switch(actflag) but isn't really * a delivery, so we test it here.. */ if (!strncmp("ruleset", idptr, 7)) { hptr->rule++; /* Find the reject-field: */ for (k = 1; k < icount; k++ ) { if (!strncmp("relay=", info[k], 6)) relstr = get_relay_name(info[k]+6); else if (!strncmp("reject=", info[k], 7)) rulestr = info[k]+7; } if (!rulestr) continue; if (!relstr) relstr = BOUNCE_HOST; if (rsnum) update_ruleset(hptr, rulestr, relstr); continue; } /* Take the current time: */ hptr->cdtime = (time_t)conv_time(mon, day, hh, min, sec); /* Check if we are allowed to proceed: */ if (sstime && (hptr->cdtime < sstime)) continue; if (eetime && (hptr->cdtime > eetime)) continue; /* is the host brand new? */ if (hptr->fhost == 0) { /* Make the first time as ftime, cdtime and ltime: */ hptr->fhost = 1; hptr->ftime = hptr->cdtime; hptr->ltime = hptr->ftime; } /* Check ftime and ltime: */ if (hptr->cdtime > hptr->ltime) hptr->ltime = hptr->cdtime; if (hptr->cdtime < hptr->ftime) hptr->ftime = hptr->cdtime; /* Calculate weekday: */ hptr->lday = (localtime(&hptr->cdtime))->tm_wday; /* Debug */ if (vflag && (actflag == ACTION_DSN || actflag == ACTION_FROM || actflag == ACTION_TO || actflag == ACTION_RULESET || actflag == ACTION_USER)) { fprintf(stderr, "\n<< DEBUG >> file %s, line %d\n", file, lcount); fprintf(stderr, " msg id=%s\n", idptr); } /* * Switch the possible actions * DSN stuff * These include DSN, postmaster notify, return to sender... */ switch(actflag) { case ACTION_DSN: if (vflag) fprintf(stderr, " DSN: %s\n", idptr); /* Find the message ID */ tmsgid = get_msgid(hptr, idptr); if (tmsgid == NULL) continue; /* Remove it */ remove_msgid(hptr, idptr); /* This is a completely new mail */ p = bechar; s = BOUNCE_HOST; /* * Sender Envelope and Relay filtering */ #ifdef USE_REGEXP if (check_filter(sef, &csef, p) && check_filter(srf, &csrf, s)) { #else if (check_filter(sef, p) && check_filter(srf, s)) { #endif /* Message ID */ update_msgid(hptr, p, s, action, hh, day, 1, 200, ""); /* Nope, we are filtered: */ } else continue; break; /* User unkown: */ case ACTION_USER: if (vflag) fprintf(stderr, " User unkown: %s\n", idptr); update_omsgid(hptr, idptr, 0); break; /* "from" envelope: */ case ACTION_FROM: p = NULL; s = NULL; /* check omsgidtab */ if (check_omsgid(hptr, idptr)) continue; /* fetch the name: */ p = get_name(action+5); if (vflag) fprintf(stderr, " from=%s\n", p); /* follow on: */ for (k = 1; k < icount; k++) { /* size of the message: */ if (!strncmp("size", info[k], 4)) { hptr->lsize = atoi(info[k]+5); /* msgid */ } else if (!strncmp("msgid", info[k], 5)) { messageid = info[k]+6; /* input relay */ } else if (!strncmp("relay", info[k], 5)) { s = get_relay_name(info[k]+6); /* no of recipients to follow */ } else if (!strncmp("nrcpts", info[k], 6)) idsize = atoi(info[k]+7); } /* Check relay */ if (!s) s = BOUNCE_HOST; if (vflag) fprintf(stderr, " relay=%s\n", s); /* Initialize messageid */ if (!messageid) messageid = ""; /* * Sender Envelope and Relay filtering */ #ifdef USE_REGEXP if (check_filter(sef, &csef, p) && check_filter(srf, &csrf, s)) { #else if (check_filter(sef, p) && check_filter(srf, s)) { #endif update_msgid(hptr, p, s, idptr, hh, day, idsize, hptr->lsize, messageid); /* Nope, we are filtered: */ } else continue; break; /* "to" envelpe: */ case ACTION_TO: p = NULL; s = NULL; /* Check msgidtab */ tmsgid = get_msgid(hptr, idptr); if (tmsgid == NULL) /* Bogus address or (from=) side filter */ continue; hptr->lsize = tmsgid->size; /* Check delivery flag: */ if (tmsgid->flag == NOT_DELIVERED) /* This is a fresh one: */ tmsgid->flag = FIRST_DELIVERY; /* fetch name and make entry: */ i = 0; memset(env_rec, 0, (size_t)sizeof(char) * 128); p = get_name(action+3); env_rec[i] = p; for (k = 1; k < icount; k++) { if (!strncmp("delay", info[k], 5) || !strncmp("ctladdr", info[k], 7)) break; else /* more recipients: */ env_rec[++i] = get_name(info[k]); } /* now for the rest of the cases: */ for (; k < icount; k++ ) { if (!strncmp("stat", info[k], 4)) { statstr = info[k]+5; if (!strncmp("Sent", info[k]+5, 4) || !strncmp("sent", info[k]+5, 4)) status = STATUS_SENT; else if (!strncmp("Deferred", info[k]+5, 8)) status = STATUS_DEF; else if (!strncmp("User", info[k]+5, 4)) status = STATUS_USER; else if (!strncmp("Host", info[k]+5, 4)) status = STATUS_HOST; else if (!strncmp("queued", info[k]+5, 6)) status = STATUS_QUEUE; else if (!strncmp("Service", info[k]+5, 7)) status = STATUS_SERV; else status = STATUS_OTHER; } else if (!strncmp("relay", info[k], 5)) { /* output relay */ s = get_relay_name(info[k]+6); } } /* If no relay host, make as bouncehost: */ if (!s) s = BOUNCE_HOST; if (vflag) fprintf(stderr, " relay=%s\n", s); i = 0; gf = 0; while (env_rec[i]) { if (vflag) fprintf(stderr, " to=%s\n", env_rec[i]); /* * Recipient Envelope and Relay filtering */ #ifdef USE_REGEXP if (check_filter(ref, &cref, env_rec[i]) && check_filter(rrf, &crrf, s)) { gf = 1; pf = 1; #else if (check_filter(ref, env_rec[i]) && check_filter(rrf, s)) { gf = 1; pf = 1; #endif } else pf = 0; if (pf) { /* * Update output side stats if * the format is ASCII or HTML: */ if (Oflag != FORMAT_CLOG) { if (status == STATUS_SENT) { update_out(hptr, env_rec[i], hptr->lsize); if (epnum) update_envpair(hptr, tmsgid->sender, env_rec[i], hptr->lsize); } else { update_out(hptr, env_rec[i], 0); if (epnum) update_envpair(hptr, tmsgid->sender, env_rec[i], 0); } hptr->onum++; /* Custom log: */ } else if (!clsflag || (clsflag && (status == STATUS_SENT))) { printclog( hptr->cdtime, tmphost, (int)hptr->lsize, tmsgid->msgid, tmsgid->sender, tmsgid->relay, env_rec[i], s, (status == STATUS_SENT) ? 1 : 0); } /* Checkout msgid */ check_msgid(hptr, idptr, KEEP); } i++; } /* Remove msgid if not ASCII or HTML: */ if (Oflag == FORMAT_CLOG) { if (gf) { if (status == STATUS_SENT) check_msgid(hptr, idptr, REMOVE); } else /* Remove useless message ID: */ remove_msgid(hptr, idptr); continue; } /* * Update global stats only once per delivery */ if (gf) { /* Update output relay: */ hptr->ronum++; if (status == STATUS_SENT) { update_rout(hptr, s, hptr->lsize); /* update relay pairs */ if (rpnum) update_relpair(hptr, tmsgid->relay, s, hptr->lsize); } else { update_rout(hptr, s, 0); if (rpnum) update_relpair(hptr, tmsgid->relay, s, 0); } /* Update the global stats: */ hptr->gonum++; hptr->ohh[hh]++; hptr->odd[hptr->lday]++; if (status == STATUS_SENT) hptr->osize += hptr->lsize; /* Check status flag: */ if (status == STATUS_SENT) { hptr->sent++; if (stnum) update_status(hptr, "Sent"); } else { if (stnum) update_status(hptr, statstr); switch(status) { case STATUS_DEF: hptr->defe++; break; case STATUS_HOST: hptr->hunk++; break; case STATUS_USER: hptr->uunk++; break; case STATUS_QUEUE: hptr->queu++; break; case STATUS_SERV: hptr->service++; break; case STATUS_OTHER: hptr->other++; break; } } /* * If this is a first delivery, update * input side statistics: */ if (tmsgid->flag == FIRST_DELIVERY) { /* Add envelope: */ hptr->inum++; update_in(hptr, tmsgid->sender, hptr->lsize); /* Add relay: */ hptr->rinum++; update_rin(hptr, tmsgid->relay, hptr->lsize); /* Update statistics: */ hptr->ihh[tmsgid->hh]++; hptr->idd[hptr->lday]++; hptr->size += hptr->lsize; hptr->isize += hptr->lsize; tmsgid->flag = DELIVERED; } /* * If the message was sent, remove useless * message ID from the chain */ if (status == STATUS_SENT) check_msgid(hptr, idptr, REMOVE); } else /* Remove useless message ID: */ remove_msgid(hptr, idptr); break; /* SYSERR: */ case ACTION_SYSERR: if (vflag) { fprintf(stderr, "\n<< DEBUG >> file %s, line %d\n", file, lcount); fprintf(stderr, " SYSERR"); } for (i = 0, k = actn; k < hcount; k++) { /* Too many hops: */ if (!strncmp("Too", head[k], 3)) { if (!head[k+2]) continue; if (!strncmp("many", head[k+1], 4) && !strncmp("hops", head[k+2], 4)) i = HOPCOUNT; /* Local configuration error: */ } else if (!strncmp("config", head[k], 6)) { if (!head[k+1]) continue; if (!strncmp("error:", head[k+1], 6)) i = LOOP; } /* Other SYSERR (i = 0)*/ } switch(i) { case HOPCOUNT: if (vflag) fprintf(stderr, " (hop count)\n"); hptr->hopc++; break; case LOOP: if (vflag) fprintf(stderr, " (mail loop)\n"); hptr->lcerror++; break; case OTHER: if (vflag) fprintf(stderr, " (other)\n"); hptr->oserror++; break; } break; /* ruleset based rejection: */ case ACTION_RULESET: if (vflag) fprintf(stderr, " ruleset: %s\n", idptr); update_omsgid(hptr, idptr, 0); hptr->rule++; /* Find the reject-field: */ for (k = 1; k < icount; k++ ) { if (!strncmp("relay=", info[k], 6)) relstr = get_relay_name(info[k]+6); else if (!strncmp("reject=", info[k], 7)) rulestr = info[k]+7; } if (!rulestr) continue; if (!relstr) relstr = BOUNCE_HOST; if (rsnum) update_ruleset(hptr, rulestr, relstr); break; /* sendmail daemon started: */ case ACTION_DAEMON: if (vflag) { fprintf(stderr, "\n<< DEBUG >> file %s, line %d\n", file, lcount); fprintf(stderr, " daemon restart\n"); } hptr->dstart++; break; /* alias database rebuild: */ case ACTION_DATAB: if (vflag) { fprintf(stderr, "\n<< DEBUG >> file %s, line %d\n", file, lcount); fprintf(stderr, " alias table rebuilt\n"); } hptr->alias++; break; default: continue; } } } /* * get_name - strip sender/recipient name/domain. */ const char * get_name(char *name) { char *tmp, *tmp2; int i; /* downcase everything if requested */ if (dcaddrflag) { for ( i=0; name[i]; i++) { name[i] = tolower(name[i]); } } /* null-sender? */ if (!strncmp("<>", name, 2)) return("<>"); /* find the separating '@'-char: */ while (*name == '<' || *name == '"' || *name == '\'') name++; tmp = tmp2 = name; while (*name++ != '@' && *name) ; if (!*name) name = tmp; /* find the first char of the return pointer: */ if (dflag) tmp = name; else { while (name != tmp2) { if ((*name == '<') || (*name == '"') || (*name == '\'') || (*name == ' ')) { name++; break; } else name--; } tmp = name; } /* cut the string: */ while (*name) { if ((*name == '>') || (*name == '"') || (*name == '\'') || (*name == ' ')) { *name = '\0'; break; } else name++; } return tmp; } /* get relay name: */ const char * get_relay_name(char *name) { char *tmp; tmp = name; /* * Search for space or (in case of identd) * '@'-char. Make the return string point * to next char. */ while (*name) { if (*name == '@') { tmp = name + 1; break; } name++; } if (!*name) name = tmp; /* * Do not take the IP-address in brackets */ while (*name) { if ((*name == ' ') || (*name == ',')) { *name = '\0'; break; } else name++; } if (*(name-1) == '.') *(name-1) = '\0'; return tmp; } #ifdef USE_REGEXP int check_filter(const char *s, regex_t *rgp, const char *text) { if (!s) return(1); else if (*s == '!') { if (!regexec(rgp, text, 0, NULL, 0)) return(0); else return(1); } else if (!regexec(rgp, text, 0, NULL, 0)) return(1); else return(0); } #else int check_filter(const char *s, const char *text) { if (!s) return(1); else if (*s == '!') { if (strstr(text, s+1)) return(0); else return(1); } else if (strstr(text, s)) return(1); else return(0); } #endif /* * Printing routine for Custom Log format */ void printclog(time_t sma_time, const char *host, int size, const char *id, const char *from, const char *frelay, const char *to, const char *trelay, int status) { /* time struct: */ struct tm *tmptime; /* Month tab */ const char *montab[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; /* Temporary pointer */ const char *frm; /* default format: */ if (!cfchar) { fprintf(ofp, "%d %s %d %s %s %s %s %d\n", (int)sma_time, host, size, from, frelay, to, trelay, status); /* We have the format string */ } else { /* Break down the time struct: */ tmptime = localtime(&sma_time); for (frm = cfchar; *frm != '\0'; frm++) switch (*frm) { /* * If the char after '%'-char is something we know * print it, otherwise print literally */ case '%': if (*(frm+1)) switch (*(frm+1)) { case 'd': fprintf(ofp,"%.2d", tmptime->tm_mday); frm++; break; case 'D': fprintf(ofp,"%s", stripn(asctime(tmptime))); frm++; break; case 'f': fprintf(ofp,"%s", from); frm++; break; case 'F': fprintf(ofp, "%s", frelay); frm++; break; case 'h': fprintf(ofp,"%.2d", tmptime->tm_hour); frm++; break; case 'H': fprintf(ofp,"%s", host); frm++; break; case 'i': fprintf(ofp,"%s", id); frm++; break; case 'm': fprintf(ofp,"%.2d", tmptime->tm_mon+1); frm++; break; case 'n': fprintf(ofp,"%.2d", tmptime->tm_min); frm++; break; case 'M': fprintf(ofp,"%s", montab[tmptime->tm_mon]); frm++; break; case 's': fprintf(ofp,"%.2d", tmptime->tm_sec); frm++; break; case 'S': fprintf(ofp,"%d", status); frm++; break; case 't': fprintf(ofp,"%s", to); frm++; break; case 'T': fprintf(ofp,"%s", trelay); frm++; break; case 'U': fprintf(ofp,"%d", (int)sma_time); frm++; break; case 'z': fprintf(ofp,"%d", size); frm++; break; case 'y': fprintf(ofp,"%d", tmptime->tm_year + 1900); frm++; break; case '%': fputc('%', ofp); frm++; break; default: fputc('%', ofp); break; } break; /* backslashed special characters: */ case '\\': if (*(frm+1)) switch (*(frm+1)) { case 'n': fputc('\n', ofp); frm++; break; case 't': fputc('\t', ofp); frm++; break; case '\\': fputc('\\', ofp); frm++; break; default: fputc(*(frm+1), ofp); frm++; break; } break; /* * If not flag or any other special char, copy the * format string character to stream: */ default: fputc(*frm, ofp); break; } /* End line always with newline: */ fputc('\n', ofp); /* Flush output stream */ fflush(ofp); } }