-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathlmtp-proxy
executable file
·178 lines (149 loc) · 5.84 KB
/
lmtp-proxy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/bin/env python
#
# A simple LMTP delivery proxy
#
# 2017 Andreas Thienemann <andreas@bawue.net>
# Licensed under GPLv3+
# https://github.com/ixs/lmtp_proxy
#
import argparse
import asyncore
import grp
from lmtpd import LMTPServer
import os
import pwd
import signal
import smtplib
import syslog
import sys
import yaml
class LMTPProxy(LMTPServer):
def __init__(self, address, owner=-1, group=-1, permissions=None):
self.config = None
self.options = {}
# In case we get a unix socket, we need to remove the file first
if address[0] == '/':
try:
os.unlink(address)
except OSError:
if os.path.exists(address):
raise
address_text = address
else:
address = tuple(address)
address_text = '{}:{}'.format(*address)
LMTPServer.__init__(self, address)
# Make sure the file has the necessary permissions
if address[0] == '/':
if owner is not None:
os.chown(address, pwd.getpwnam(owner).pw_uid, -1)
if group is not None:
os.chown(address, -1, grp.getgrnam(owner).gr_gid)
if permissions is not None:
os.chmod(address, permissions)
self.log('Listening for incoming connection on {}'.format(address_text))
def log(self, message):
if not self.options.get('daemonize', False):
print(message)
syslog.syslog(syslog.LOG_INFO, message)
def lookup_user_backend(self, user):
backend = self.config['users'].get(user, self.config['config'].get('fallback_backend', None))
if backend is None:
raise RuntimeError('No fallback backend configured')
return backend
def lmtp_deliver(self, backend, mailfrom, rcptto, data):
if backend == 'reject':
self.log('LMTP message from <{}> to <{}> temporarily rejected'.format(mailfrom, rcptto))
return '450 LMTP user {} rejected. Please try again.'.format(rcptto)
backend_data = self.config['backends'].get(backend, None)
if backend_data is None:
self.log('No backend configuration found for backend {}'.format(backend))
return '451 No backend configuration found for backend {}'.format(backend)
if 'socket' in backend_data:
server = smtplib.LMTP(backend_data['socket'])
address_text = backend_data['socket']
else:
server = smtplib.LMTP(backend_data['host'], backend_data['port'])
address_text = '{}:{}'.format(backend_data['host'], backend_data['port'])
if 'user' in backend_data and 'password' in backend_data:
server.login(backend_data['user'], backend_data['password'])
try:
server.sendmail(mailfrom, rcptto, data)
server.quit()
self.log('LMTP message from <{}> to <{}> delivered to {} ({})'.format(mailfrom, rcptto, backend, address_text))
return None
except smtplib.SMTPRecipientsRefused as e:
server.quit()
code = dict(e[0]).values()[0][0]
msg = (dict(e[0]).values()[0][1]).split("\n")
output = []
i = 0
for line in msg:
if (i + 1) == len(msg):
sep = " "
else:
sep = "-"
output.append("{}{}{}".format(code, sep, line))
i += 1
self.log('LMTP message from <{}> to <{}> not delivered to {} ({}). {}'.format(mailfrom, rcptto, backend, address_text, "\n".join(output)))
return "\r\n".join(output)
except:
server.quit()
error = " ".join(map(str, sys.exc_info()[1]))
self.log('LMTP message from <{}> to <{}> not delivered to {} ({}): {}'.format(mailfrom, rcptto, backend, address_text, error))
return error
def process_message(self, peer, mailfrom, rcptto, data):
if self.config['config'].get('ignoreDomain', False):
user = rcptto.split('@')[0]
else:
user = rcptto
backend = self.lookup_user_backend(user)
self.log('Incoming message from <{}> to <{}>, forwarding to {} backend'.format(mailfrom, rcptto, backend))
return self.lmtp_deliver(backend, mailfrom, rcptto, data)
def reload_user_list(self, signum, frame):
try:
with open(self.options['config']) as f:
self.config = yaml.safe_load(f)
self.log('Userlist reloaded. {} Entries found'.format(len(self.config.get('users', {}))))
except:
pass
def parse_cmdline():
parser = argparse.ArgumentParser(
description="""An LMTP Proxy server. Accepts mail via LMTP on an incoming
socket and can forward to different lmtp sockets depending on the
destination address.""")
parser.add_argument('-c', '--config', help="Configuration file location", nargs="?", default="/etc/lmtp-proxy.conf")
parser.add_argument('-D', '--daemonize', help="Daemonize program. If unspecified, run in foreground", action="store_true", default=False)
return parser.parse_args()
def parse_config(file):
with open(file) as f:
config = yaml.safe_load(f)
return config
def main():
options = parse_cmdline()
config = parse_config(options.config)
tmp = {'daemonize': options.daemonize, 'config': options.config }
syslog.openlog(ident="lmtp-proxy", logoption=syslog.LOG_PID, facility=syslog.LOG_MAIL)
syslog.syslog(syslog.LOG_INFO, 'Started using configuration file {}'.format(options.config))
address = config['config'].get('socket', False)
owner = config['config'].get('owner', None)
group = config['config'].get('group', None)
permissions = config['config'].get('permissions', None)
pid = config['config'].get('pid', '/var/run/lmtp-proxy.pid')
server = LMTPProxy(address, owner, group, permissions)
server.config = config
server.options = tmp
signal.signal(signal.SIGUSR1, server.reload_user_list)
try:
asyncore.loop()
except KeyboardInterrupt:
asyncore.ExitNow()
if __name__ == "__main__":
options = parse_cmdline()
config = parse_config(options.config)
pid = config['config'].get('pid', '/var/run/lmtp-proxy.pid')
if options.daemonize:
from daemonize import Daemonize
daemon = Daemonize(app="lmtp-proxy", pid=pid, action=main)
daemon.start()
main()