/* This is a mail filter implementing a check for virtual users. It checks the "rcpt to" field and if it is not a local mail or virtual domain mail, it is rejected. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; extern "C" { #include "libmilter/mfapi.h" } static bool debug = false; static bool accept_all = false; static void n_debug (const char *ctl, ...) { if (debug){ va_list list; va_start (list,ctl); vfprintf (stderr,ctl,list); va_end (list); } } #if 0 struct mlfiPriv{ string fname; string connectfrom; string helofrom; unsigned long clientip; // IP number of the SMTP client FILE *fp; mlfiPriv(){ clientip=0; } }; #define MLFIPRIV ((struct mlfiPriv *) smfi_getpriv(ctx)) #endif /* Sendmail is pretty much sending anything entered in rcpt to without any normalisation. We can get quotes and <> there. We just remove those character. */ static void removecrap(const char *src, char *dst) { while (*src != '\0'){ char car = *src++; if (car != '<' && car != '>' && car != '"'){ *dst++ = car; } } *dst = '\0'; } /* Transform the GECOS in a mail address by replacing space with dots. */ static void vmilter_formatgecos(const char *orig, string &mail_gecos) { char tmp[strlen(orig)+1]; char *dst = tmp; while (*orig != '\0'){ if (isspace(*orig)){ *dst++ = '.'; orig++; while (isspace(*orig)) orig++; }else{ *dst++ = *orig++; } } *dst = '\0'; mail_gecos = tmp; } static int vmilter_splitline (const char *line, string words[9]) { int nbword = 0; const char *start = line; while (*line != '\0' && *line != '\n' && nbword < 9){ if (*line == ':'){ words[nbword] = string(start,(int)(line-start)); line++; nbword++; start = line; }else{ line++; } } return nbword; } /* Check if there is a given record in a file (passwd or aliases) Password files and aliases do not have the same layout, but lines are starting with name followed by a colon. Return -1 if the file does not exist 0 if the file exist but there is no match 1 if there is a match */ static int mlfi_checkrec ( const string &file, const string &user, bool match_gecos) { int ret = -1; FILE *fin = fopen (file.c_str(),"r"); if (fin != NULL){ ret = 0; // Ok, this is the vdomain, does the user exist char buf[2000]; while (fgets(buf,sizeof(buf)-1,fin)!=NULL){ string words[9]; vmilter_splitline (buf,words); string gecos; if (match_gecos) vmilter_formatgecos(words[4].c_str(),gecos); if (strcasecmp(words[0].c_str(),user.c_str())==0 || strcasecmp(gecos.c_str(),user.c_str())==0){ ret = 1; break; } } fclose (fin); } return ret; } static sfsistat mlfi_envrcpt( SMFICTX *ctx, char **argv) { sfsistat ret = SMFIS_CONTINUE; for (int i=0; argv[i] != NULL; i++){ const char *arg = argv[i]; char adr[strlen(arg)+1]; removecrap (arg,adr); if (strncasecmp(adr,"NOTIFY=",7)==0 || strncasecmp(adr,"ORCPT=",6)==0){ n_debug ("Ignore DSN stuff: %s\n",adr); continue; } const char *pt = strchr(adr,'@'); n_debug ("Testing address %s\n",adr); if (pt != NULL){ string domain(pt+1); string user (adr,pt-adr); // Is this a vdomain FILE *fin = fopen ("/etc/conf.linuxconf","r"); if (fin == NULL){ syslog (LOG_ERR,"can't open /etc/conf.linuxconf, weird (%m)"); }else{ set fallbacks; // List of all vdomain having a fallback set match_gecos; // vdomain accepting email to full user name /* We have a domain, but this may be either a vdomain or an alias for a vdomain. We must walk /etc/conf.linuxconf to learn about this. While doing so, we collect all vdomain having a fallback definition. Since we don't know yet which vdomain is targetted (domain may be an alias for a vdomain) we have to collect all fallbacks in a set and perform the test at the end of the loop. Look for a line of the following format vdomain_other.A_DOMAIN alias and vdomain_fallback.DOMAIN .... */ { char buf[2000]; while (fgets(buf,sizeof(buf)-1,fin)!=NULL){ if (strncmp(buf,"vdomain_other.",14)==0){ char v1[1000],v2[1000]; if (sscanf (buf,"%s %s",v1,v2)==2 && strcasecmp(domain.c_str(),v2)==0){ domain = v1+14; } }else if (strncmp(buf,"vdomain_fallback.",17)==0){ // Record all the vdomain having a fallback char v1[2000]; if (sscanf (buf,"%s",v1)==1){ fallbacks.insert(v1+17); } }else if (strncmp(buf,"vdomain_gecos.",14)==0){ // Record all the vdomain supporting full user name char v1[2000]; if (sscanf (buf,"%s",v1)==1){ match_gecos.insert(v1+14); } } } } fclose (fin); n_debug ("Domain scan: %s -> %s fallback=%d\n" ,adr,domain.c_str(),fallbacks.count(domain)); // If there is a fallback, we accept anything // So we perform the test only if there is no fallback if (fallbacks.count(domain)==0){ // Now we try to open the password file for this domain // if it exist. At this point we are not sure it is // a vdomain. string pfile = string ("/etc/vmail/passwd.") + domain; bool checkgecos = match_gecos.count(domain)>0; int pwcheck = mlfi_checkrec (pfile,user,checkgecos); n_debug ("Check in passwd file %s -> %s (checkgecos=%d)\n" ,user.c_str() ,pwcheck ? "Found" : "Not found" ,checkgecos); if (pwcheck == 0){ string alfile = string ("/etc/vmail/aliases.") + domain; int alcheck = mlfi_checkrec(alfile,user,false); n_debug ("Check in aliases file %s -> %s\n" ,user.c_str() ,pwcheck ? "Found" : "Not found"); if (alcheck == 1) pwcheck = 1; } if (pwcheck==0){ if (!accept_all){ smfi_setreply(ctx, "550", "5.1.1", "User unknown"); ret = SMFIS_REJECT; } break; } } } } } return accept_all ? SMFIS_CONTINUE : ret; } #if 0 static sfsistat mlfi_close(SMFICTX *ctx) { mlfiPriv *priv = MLFIPRIV; delete priv; smfi_setpriv(ctx, NULL); return SMFIS_CONTINUE; } #endif struct smfiDesc smfilter = { "vdomainmilter", /* filter name */ SMFI_VERSION, /* version code -- do not change */ SMFIF_ADDHDRS, /* flags */ NULL, /* connection info filter */ NULL, /* SMTP HELO command filter */ NULL, /* envelope sender filter */ mlfi_envrcpt, /* envelope recipient filter */ NULL, /* header filter */ NULL, /* end of header */ NULL, /* body block filter */ NULL, /* end of message */ NULL, /* message aborted */ NULL, /* connection cleanup */ }; static void usage(const char *prog) { fprintf(stderr, "Usage: %s -d -p socket-addr\n" ,prog); } int main(int argc, char *argv[]) { bool setconn = false; int c; const char *args = "adp:h"; extern char *optarg; /* Process command line options */ while ((c = getopt(argc, argv, args)) != -1){ switch (c){ case 'a': accept_all = true; break; case 'd': debug = true; break; case 'p': if (optarg == NULL || *optarg == '\0'){ (void) fprintf(stderr, "Illegal conn: %s\n",optarg); exit(EX_USAGE); } if (smfi_setconn(optarg) == MI_FAILURE){ (void) fprintf(stderr,"smfi_setconn failed\n"); exit(EX_SOFTWARE); } /* ** If we're using a local socket, make sure it ** doesn't already exist. Don't ever run this ** code as root!! */ if (strncasecmp(optarg, "unix:", 5) == 0) unlink(optarg + 5); else if (strncasecmp(optarg, "local:", 6) == 0) unlink(optarg + 6); setconn = true; break; case 'h': default: usage(argv[0]); exit(EX_USAGE); } } if (!setconn){ fprintf(stderr, "%s: Missing required -p argument\n", argv[0]); usage(argv[0]); exit(EX_USAGE); } if (smfi_register(smfilter) == MI_FAILURE){ fprintf(stderr, "smfi_register failed\n"); exit(EX_UNAVAILABLE); } return smfi_main(); }