''' smtpd.py - SMTP daemon service for sagator. (c) 2003-2019 Jan ONDREJ (SAL) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. ''' from aglib import * import stats, resource __all__ = ['smtpd'] class smtps(smtp): ''' SMTP server class ''' def __init__(self, scanners, conn, addr): self.SCANNERS = scanners self.f = conn.makefile('rwb', self.bufsize) self.write = conn.sendall self.readline = self.f.readline self.RECV = RECV_SMTP self.stats = stats.statistics() self.srv = smtpc() self.write(self.srv.readline()) mail.__init__() mail.addr = ( addr[0].encode(), addr[1], mail.getnamebyaddr(addr[0]).encode() ) self.connect_from = addr self.last_cmd = None # last command, like EHLO while True: if self.recv(): break def send_ok(self, r=b"Ok"): self.write(b"250 "+r+b"\r\n") def quit_connect(self, cmd=b'QUIT'): debug.echo(5, "SMTPS: Closing connect to server") self.RECV=RECV_QUIT if cmd: self.srv.conn.sendall(cmd+b"\r\n") self.srv.close() return self.recv() def check_reply(self, line, rw=True): '''Check for SMTP reply. Return True, if 2XX or 3XX returned.''' debug.echo(6, "SMTPC<: ", line) self.srv.conn.sendall(line) # send reply while 1: l=self.srv.readline() if not l: debug.echo(3, "SMTPS: Connection closed after:", line) return False if rw: if not (l.startswith(b'250-STARTTLS') \ and self.last_cmd and self.last_cmd.group(1).upper()==b'EHLO'): debug.echo(7, 'SMTPC>: ', l) self.write(l) else: debug.echo(7, 'SMTPC>: Filtered STARTTLS: ', l) if self.reg_smtp_reply.search(l): break if self.reg_smtp_reply_ok.search(l): return True debug.echo(4, "SMTPS: ", l.rstrip(), ' --- ', line) return False def recv(self): try: line=self.readline() except IOError as err: (ec,es) = err.args debug.echo(0, "smtps(): ", es) return 1 if not line: debug.echo(9, "smtps(): Empty line") return 1 if self.RECV: # receiving data or header if self.RECV==RECV_QUIT: # waiting for quit? if self.reg_quit.search(line): # response for QUIT is not required, just close it #self.write(b"221 Bye\r\n") self.f.close() # connection shutdown follows from parent function return 1 else: self.write(b"502 SMTP communication stopped by Sagator\r\n") return 0 elif (line==b'.\r\n') | (line==b'.\n'): # end of DATA part mail.close() debug.echo(3, "SMTPS: BODY DONE, size: ", len(mail.data), " B") self.RECV = RECV_SMTP v, level, virname = checkvir(self.SCANNERS) if v!=S_TEMPFAIL: debug.echo(2, "STATS: %s seconds, %s bytes, status: %s" % (self.stats.end(), len(mail.data), tostr(virname))) if (v==S_OK) | (v==S_FORCE_SEND): self.check_reply(b'DATA\r\n', rw=False) debug.echo(5, "SMTPS: Sending data") self.srv.conn.sendall(mail.xheader) self.srv.conn.sendall(mail.data) debug.echo(1, "SMTPS: OK: 250 Ok") self.srv.conn.sendall(line) # send . self.write(self.srv.readline()) self.stats.update(len(mail.data)) mail.__init__() # reinit elif v==S_REJECT: debug.echo(1, "SMTPS: REJECT: ","550 Content rejected - ", tostr(virname)) self.write(b"550 Content rejected - %s\r\n" % virname) self.stats.update(len(mail.data), 1) return self.quit_connect() elif v==S_DROP: debug.echo(1, "SMTPS: DROP: 250 mail dropped - ", tostr(virname)) self.write(b"250 mail dropped - %s\r\n" % virname) self.stats.update(len(mail.data), 1) return self.quit_connect() elif v==S_CUSTOM: debug.echo(1, "SMTPS: CUSTOM: %s %s" % tostr(globals.REPLY)) self.write(b"%s %s\r\n" % globals.REPLY) self.stats.update(len(mail.data), 1) return self.quit_connect() else: # S_TEMPFAIL debug.echo(1, "SMTPS: TEMPFAIL: 451 ", tostr(virname)) self.write(b"451 %s\r\n" % virname) self.stats.update(tempfail=1) # update fail statistics return self.quit_connect() else: mail.df.write(line) else: mail.comm = mail.comm+line rcptto = self.reg_rcptto.search(line) mailfrom = self.reg_mailfrom.search(line) self.last_cmd = self.reg_cmd.search(line) if rcptto: try: recipient_addr = parseaddr(tostr(rcptto.group(1)))[1] except: recipient_addr = b'' if globals.recipient_policy: mail.policy_request = { 'client_address': self.connect_from[0], 'client_name': self.connect_from[1], 'helo_name': self.reg_helo_ehlo.search(mail.comm).group(1), 'sender': mail.sender, 'recipient': recipient_addr } try: mail.policy_request['client_address'] = \ self.reg_xforward_addr.search(mail.comm).group(1) mail.policy_request['client_name'] = \ self.reg_xforward_name.search(mail.comm).group(1) except AttributeError: pass policy_reply = checkpolicy(globals.recipient_policy,True) self.stats.policy_update() if policy_reply[0]!=b'2': self.write(policy_reply+b"\r\n") return 0 if self.check_reply(line): debug.echo(2, 'SMTPS: ', line) if recipient_addr: mail.recip.append(recipient_addr) return 0 elif mailfrom: if self.check_reply(line): debug.echo(2, 'SMTPS: ', line) try: mail.sender = parseaddr(tostr(mailfrom.group(1)))[1] except: mail.sender = mailfrom.group(1) elif self.reg_rset.search(line): if self.check_reply(line): debug.echo(2, 'SMTPS: ', line.rstrip(), ", Resetting mail class") mail.__init__() # reinit mail class for scnr in self.SCANNERS: scnr.reinit() # reinit scanners elif self.reg_data.search(line): debug.echo(5, 'SMTPS: ', line) self.RECV = RECV_BODY self.write(b"354 End data with .\r\n") elif self.reg_quit.search(line): debug.echo(5, 'SMTPS: ', line) self.check_reply(line) # 221 Bye return 1 elif self.reg_xforward.search(line): debug.echo(2, 'SMTPS: ', line) self.check_reply(line) else: debug.echo(6, 'SMTPS: ', line) self.check_reply(line) return 0 class smtpd(service): ''' SMTP daemon service. This service can be used to start sagator as separate filtering SMTP daemon. Is is useful for postfix and any other SMTP daemon, which can use these filters. Usage: smtpd(scanners, host, port, prefork=2) Where: scanners is an array of scanners (see README.scanners for more info) host is a an ip address to bind port is a port to bind prefork is a number, which defines preforked process count. Set this parameter to actual processor count + 1 or leave it's default (2). For multicore servers you can use core_count() function to use autodetection. Example: smtpd(SCANNERS, '127.0.0.1', 27) ''' name='smtpd()' def accept(self,connects=0): # accept conn,addr = self.s.accept() socket_settimeout(conn,120) # reinit scanners for scnr in self.SCANNERS: scnr.reinit() # generate ID self.time2=time.strftime("%Y%m%d-%H%M%S",time.localtime(time.time())) if self.time2!=self.time1: self.time1,self.timec=self.time2,1 globals.gen_id(self.time2, self.timec) debug.echo(1, "Connection from: ", addr[0], " at ", \ time.strftime("%c", time.localtime())) debug.echo(1, "Process id: %s" % globals.id) smtps(self.SCANNERS, conn, addr) debug.echo(1, "%s: Closing connection." % self.name) # catch shutdown errors try: conn.shutdown(socket.SHUT_RDWR) conn.close() except socket.error: pass