3 SMTP proxy to ask account password in between sending mail.
4 Useful if one do not want to store passwords in /etc/exim/passwd.client .
5 Also it always uses SSL on outbound connections.
7 Copyright (C) 2012 Kirill Smelkov <kirr@navytux.spb.ru>
9 This program is free software: you can Use, Study, Modify and Redistribute
10 it under the terms of the GNU General Public License version 2, or any
11 later version. This program is distributed WITHOUT ANY WARRANTY. See
12 COPYING file for full License terms.
16 For this proxy to work
18 1) turn Exim's send-to-self on (see Exim FAQ Q0206 about why this is
21 --- a/exim4/conf.d/router/200_exim4-config_primary
22 +++ b/exim4/conf.d/router/200_exim4-config_primary
23 @@ -73,9 +73,10 @@ nonlocal:
24 # domains, you'll need to copy the dnslookup_relay_to_domains router
25 # here so that mail to relay_domains is handled separately.
28 debug_print = "R: smarthost for $local_part@$domain"
31 domains = ! +local_domains
32 transport = remote_smtp_smarthost
33 route_list = * DCsmarthost byname
37 2) point Exim smarthost to this program's incoming port, e.g.:
39 smarthost -> localhost::2525
42 3) and run the proxy, e.g.
44 ksmtpproxy :2525 smtp.yandex.ru
47 then Exim will send all outgoing mail through this proxy.
54 - - - - - - - - - - - | - - - -
56 Exim -----> SMTPServer ---> sendmail() -----> smarthost
59 - - - - - - - - - - - - - - - -
68 from sys
import stderr
69 from subprocess
import Popen
, PIPE
70 import prctl
# for prctl(PR_SET_DUMPABLE, 0)
71 import traceback
# to show details, when something goes wrong
73 localhost
= 'localhost'
76 smtpport
= 465 # default to smtps
77 ca_certs
= "/etc/ssl/certs/ca-certificates.crt" # default to system-wide ca-certs
82 # NOTE since we are protecting ourself from poking from gdb, it is
83 # (relatively) ok to store passwords in memory. An if it is not - we have
84 # bigger problems, because ssh-agent uses the same protection.
86 passwd_invalid
= False # special object marking previous password was invalid
89 # password entry was cancelled
90 class PasswdCancel(Exception):
93 # what's the password for an account?
94 def password4(account
):
95 password
= passwd
.get(account
)
96 if password
is not None and password
is not passwd_invalid
:
97 return passwd
[account
]
99 text
= 'Password for %s' % account
100 if password
is passwd_invalid
:
101 text
+= ' (prev auth attempt failed)'
103 p
= Popen(['/usr/bin/ssh-askpass', text
], stdout
=PIPE
)
104 password
, _
= p
.communicate()
105 # x11-ssh-askpass prints password with trailing \n
106 password
= password
.rstrip('\n')
112 passwd
[account
] = password
116 # like smtplib.SMTP_SSL but ALWAYS checks peer (ssl.CERT_REQUIRED)
117 class SMTP_SSL_STRICT(smtplib
.SMTP_SSL
):
119 def _get_socket(self
, host
, port
, timeout
):
120 if self
.debuglevel
> 0:
121 print >>stderr
, 'connect: %s via ssl' % ((host
, port
),)
122 sk
= socket
.create_connection((host
, port
), timeout
)
123 sk_ssl
= ssl
.wrap_socket(sk
,
125 cert_reqs
=ssl
.CERT_REQUIRED
# ! *always* check peer
126 # NOTE we don't use self.keyfile
127 # NOTE we don't use self.certfile
129 self
.file = smtplib
.SSLFakeFile(sk_ssl
)
133 def sendmail(fromaddr
, toaddrs
, msg
):
134 srv
= SMTP_SSL_STRICT()
135 srv
.set_debuglevel(debug
)
137 srv
.connect(smtphost
, smtpport
)
141 # print brief summary of peer cert
142 peer
, port
= sk_ssl
.getpeername()
143 cipher
, sslver
, bits
= sk_ssl
.cipher()
144 peercert
= sk_ssl
.getpeercert()
147 for rdn
in peercert
['subject']:
148 # each RDN is a sequence of name/value pairs
150 # XXX what to do for duplicate names?
153 print '\t via %s:%s %s,%s,%i' % (peer
,port
, cipher
,sslver
,bits
)
154 print '\t %s/%s/%s/%s/%s (%s)' % (
155 subj
.get('countryName', '?'),
156 subj
.get('stateOrProvinceName', '?'),
157 subj
.get('localityName', '?'),
158 subj
.get('organizationName', '?'),
159 subj
.get('organizationalUnitName', '?'),
160 subj
.get('commonName', '?'))
163 # print whole peer cert
164 print >>stderr
, 'Full certificate info:'
165 print >>stderr
, pprint
.pformat(peercert
)
167 # usual smtp repertoire
168 srv
.login(fromaddr
, password4(fromaddr
))
169 srv
.sendmail(fromaddr
, toaddrs
, msg
)
175 # SMTPServer accepts incoming mail and handles it over to our sendmail()
176 class SMTPServer(smtpd
.SMTPServer
):
178 # we've got incoming message - relay it to sendmail()
179 def process_message(self
, peer
, mailfrom
, rcpttos
, data
):
180 print '\t* %s\t-> %s ...' % (mailfrom
, ', '.join(rcpttos
))
182 print >>stderr
, '---------- MESSAGE BEGIN ----------'
183 print >>stderr
, 'peer:\t', peer
184 print >>stderr
, 'from:\t', mailfrom
185 print >>stderr
, 'tos:\t', rcpttos
186 print >>stderr
, 'data:\n'
188 print >>stderr
, '----------- MESSAGE END -----------'
193 sendmail(mailfrom
, rcpttos
, data
)
195 # password entry was cancelled
196 except PasswdCancel
, e
:
197 print '\t password cancel'
198 # 450 = "Requested mail action not taken:
199 # mailbox unavailable [E.g., mailbox busy]"
200 return '450 User cancelled password entry'
202 # SSL error - something wrong is going on!
203 except ssl
.SSLError
, e
:
204 print '\t !SSL %r' % e
205 return '450 SSL connection to smarthost not ok: %r' % e
207 # on auth error, retry asking for password
208 except smtplib
.SMTPAuthenticationError
, e
:
209 print '\t %s %s' % (e
.smtp_code
, e
.smtp_error
)
210 passwd
[mailfrom
] = passwd_invalid
211 continue # goto retry;
213 # proxy back any other SMTP errors
214 except smtplib
.SMTPResponseException
, e
:
215 emsg
= '%s %s' % (e
.smtp_code
, e
.smtp_error
)
217 return '\t %s' % emsg
221 traceback
.print_exc()
223 # 451 = "Requested action aborted: error in processing"
229 return '250 Ok, thanks for using ksmtpproxy'
233 # don't let gdb attach and read our memory, disable /proc/me/mem
234 # from being readable, /proc/me/fd/* from being peekfd'able, etc...
235 # (we store passwords in memory!)
236 prctl
.set_dumpable(0)
238 # XXX likewise maybe don't allow our memory to be swapped?
239 # but locking MCL_CURRENT works only for root (on 3.3-rc1)
240 #mlockall(MCL_CURRENT | MCL_FUTURE)
242 proxy
= SMTPServer((localhost
, localport
), (None,None))
244 smtpd
.DEBUGSTREAM
= stderr
# NOTE it's per module, not per object
248 except KeyboardInterrupt:
252 ########################################
256 from getopt
import getopt
, GetoptError
258 progname
= os
.path
.basename(sys
.argv
[0])
262 print "Try `%s --help' for more information." % progname
266 print 'Usage: %s [OPTIONS] localhost smarthost' % progname
271 """Proxy SMTP from localhost to smarthost, asking passwords in between
273 localhost: e.g. localhost:2525 or :2525
274 smarthost: e.g. smtp.yandex.ru or smtp.yandex.ru:465
276 NOTE: smarthost is always connected via SSL
278 --ca-certs=FILE file with CA certificates to use
279 (default %(ca_certs)s)
280 -d, --debug show debug output on stderr
281 -h, --help display this help and exit
285 # localhost:2525 -> localhost, 2525
286 def parse_addrport(addrspec
):
290 if not ':' in addrspec
:
293 addr
, port
= addrspec
.split(':', 1)
300 global localhost
,localport
, smtphost
,smtpport
, debug
, ca_certs
304 opts
, args
= getopt(sys
.argv
[1:], 'dh', ['ca-certs=', 'debug', 'help'])
305 except GetoptError
, e
:
306 exithelp('%s: %s' % (progname
, e
.msg
))
309 if o
in [ '--ca-certs']:
312 if o
in ['-d', '--debug']:
316 if o
in ['-h', '--help']:
326 host
, port
= parse_addrport(args
[0])
328 exithelp('E: can\'t parse localhost')
330 localhost
= host
or localhost
331 localport
= port
or localport
332 if not (localhost
and localport
):
333 exithelp('E: localhost not fully specified')
337 host
, port
= parse_addrport(args
[1])
339 exithelp('E: can\'t parse smarthost')
341 smtphost
= host
or smtphost
342 smtpport
= port
or smtpport
343 if not (smtphost
and smtpport
):
344 exithelp('E: smarthost not fully specified')
351 if __name__
== '__main__':