58
60
'bounces', 'confirm', 'join', 'leave',
59
61
'owner', 'request', 'subscribe', 'unsubscribe',
68
# These error codes are now obsolete. The LMTP protocol now uses enhanced error codes
64
69
ERR_451 = '451 Requested action aborted: error in processing'
65
70
ERR_501 = '501 Message has defects'
66
71
ERR_502 = '502 Error: command HELO not implemented'
67
# Error 550 no longer in use, has been replaced by enhanced error codes
68
72
# ERR_550 = config.LMTP_ERR_550
70
74
# Enhanced error codes
71
EERR_511 = '550 5.1.1 Bad destination mailbox address'
72
EERR_513 = '501 5.1.3 Bad destination list address syntax'
73
EERR_551 = '503 5.5.1 Invalid command'
74
EERR_554 = '501 5.5.4 Invalid command arguments'
75
EERR_572 = '550 5.7.2 The sender is not authorized to send a message to the intended mailing list'
76
EERR_450 = '4.5.0 Other or undefined protocol status'
77
EERR_511 = '5.1.1 Bad destination mailbox address'
78
EERR_513 = '5.1.3 Bad destination list address syntax'
79
EERR_551 = '5.5.1 Invalid command'
80
EERR_554 = '5.5.4 Invalid command arguments'
81
EERR_572 = '5.7.2 The sender is not authorized to send a message to the intended mailing list'
79
smtpd.__version__ = 'Python LMTP queue runner 1.0'
85
__version__ = 'Python LMTP queue runner 1.1'
111
class Channel(smtpd.SMTPChannel):
117
class Channel(asynchat.async_chat):
112
118
"""An LMTP channel."""
119
# The LMTP channel is not dependent on the SMTP channel in Python smtpd,
120
# It is a complete working LMTP channel, based on smtpd in Python
124
# The LHLO boolean determines if the LHLO command has been used or not during a session
125
# False = LHLO has not been used
126
# True = LHLO has been used
127
# RFC 2033 requires the client to say lhlo to the server before mail can be sent
114
130
def __init__(self, server, conn, addr):
115
smtpd.SMTPChannel.__init__(self, server, conn, addr)
116
# Stash this here since the subclass uses private attributes. :(
131
asynchat.async_chat.__init__(self, conn)
117
132
self._server = server
136
self._state = self.COMMAND
138
self._mailfrom = None
141
self._fqdn = socket.getfqdn()
142
self._peer = conn.getpeername()
143
self.set_terminator('\r\n')
144
self.push('220 '+EERR_200+' '+self._fqdn+' '+__version__)
147
# smtp_HELO pushs an error if the HELO command is used
148
def smtp_HELO(self, arg):
149
self.push('501 '+EERR_551+'. Use: LHLO command')
152
# smtp_EHLO pushs an error if the EHLO command is used
153
def smtp_EHLO(self, arg):
154
self.push('501 '+EERR_551+'. Use: LHLO command')
119
157
def smtp_LHLO(self, arg):
120
"""The LMTP greeting, used instead of HELO/EHLO."""
121
Channel.smtp_HELO(self, arg)
123
# smtp_HELO overridden to give LHLO errors instead of HELO errors
124
def smtp_HELO(self, arg):
125
158
"""HELO is not a valid LMTP command."""
127
self.push(EERR_554+'. Syntax: lhlo hostname')
129
if self._SMTPChannel__greeting:
130
self.push(EERR_551+'. Duplicate LHLO')
132
self._SMTPChannel__greeting = arg
133
self.push('250 %s' % self._SMTPChannel__fqdn)
160
self.push('501 '+EERR_554+'. Syntax: lhlo hostname')
163
self.push('503 '+EERR_551+'. Duplicate LHLO')
167
self.push('250 '+EERR_200+' '+self._fqdn)
168
self.push('250 '+EERR_200+' PIPELINING')
169
self.push('250 '+EERR_200+' ENHANCEDSTATUSCODES')
171
def smtp_MAIL(self, arg):
172
if self.LHLO == False:
173
self.push('503 '+EERR_551+'. Need LHLO command')
175
address = self._getaddr('FROM:', arg) if arg else None
177
self.push('501 '+EERR_554+'. Syntax: MAIL FROM:<address>')
180
self.push('503 '+EERR_551+'. Nested MAIL command')
182
self._mailfrom = address
183
self.push('250 '+EERR_200+' Ok')
186
def smtp_RCPT(self, arg):
187
if not self._mailfrom:
188
self.push('503 '+EERR_551+'. Need MAIL command')
190
address = self._getaddr('TO:', arg) if arg else None
192
self.push('501 '+EERR_554+'. Syntax: RCPT TO:<address>')
194
# Call rcpttocheck to check if list address is a known address.
195
if self.rcpttocheck(address) == 'EERR_511':
196
self.push('550 '+EERR_511)
198
# Call rcpttocheck to check if list address has syntax errors
199
if self.rcpttocheck(address) == 'EERR_513':
200
self.push('550 '+EERR_513)
203
listname = self.listname(address)
204
subaddress = self.subaddress(address)
205
# Check if sender is authorised to post to list
206
if not subaddress in SUBADDRESS_NAMES:
207
if self.listmembercheck(self._mailfrom, address) == 'EERR_572':
208
self.push('550 '+EERR_572)
210
if subaddress in SUBADDRESS_NAMES:
211
if self.listmembercheck(self._mailfrom, listname) == 'EERR_572':
212
if subaddress == 'leave' or subaddress == 'unsubscribe':
213
self.push('550 '+EERR_572+', the subaddresses -leave and -unsubscribe can not be used by unauthorised senders')
215
self._rcpttos.append(address)
216
self.push('250 '+EERR_200+' Ok')
218
def smtp_DATA(self, arg):
219
if not self._rcpttos:
220
self.push('503 '+EERR_551+'. Need RCPT command')
223
self.push('501 '+EERR_554+' Syntax: DATA')
225
self._state = self.DATA
226
self.set_terminator('\r\n.\r\n')
227
self.push('354 '+EERR_200+' End data with <CR><LF>.<CR><LF>')
229
def smtp_RSET(self, arg):
231
self.push('501 Syntax: RSET')
233
# Resets the sender, recipients, and data, but not the greeting
234
self._mailfrom = None
237
self._state = self.COMMAND
238
self.push('250 '+EERR_200+' Reset Ok')
240
def smtp_NOOP(self, arg):
242
self.push('501 '+EERR_554+'. Syntax: NOOP')
244
self.push('250 '+EERR_200+' Ok')
246
def smtp_QUIT(self, arg):
248
self.push('221 '+EERR_200+' Goodbye')
249
self.close_when_done()
252
# Overrides base class for convenience
254
asynchat.async_chat.push(self, msg + '\r\n')
256
# Implementation of base class abstract method
257
def collect_incoming_data(self, data):
258
self._line.append(data)
261
def _getaddr(self, keyword, arg):
263
keylen = len(keyword)
264
if arg[:keylen].upper() == keyword:
265
address = arg[keylen:].strip()
268
elif address[0] == '<' and address[-1] == '>' and address != '<>':
269
# Addresses can be in the form <person@dom.com> but watch out
270
# for null address, e.g. <>
271
address = address[1:-1]
274
# Implementation of base class abstract method
275
def found_terminator(self):
276
line = EMPTYSTRING.join(self._line)
278
if self._state == self.COMMAND:
280
self.push('500 '+EERR_551+'. Bad syntax')
285
command = line.upper()
288
command = line[:i].upper()
289
arg = line[i+1:].strip()
290
method = getattr(self, 'smtp_' + command, None)
292
self.push('500 '+EERR_551+'. Command "%s" not implemented' % command)
297
if self._state != self.DATA:
298
self.push('451 '+EERR_450+'. Internal confusion')
300
# Remove extraneous carriage returns and de-transparency according
301
# to RFC 821, Section 4.5.2.
303
for text in line.split('\r\n'):
304
if text and text[0] == '.':
305
data.append(text[1:])
308
self._data = NEWLINE.join(data)
309
status = self._server.process_message(self._peer,
314
self._mailfrom = None
315
self._state = self.COMMAND
316
self.set_terminator('\r\n')
318
self.push('250 '+EERR_200+' Ok')
135
323
# lists gets all the lists
190
378
return 'EERR_572'
193
def smtp_RCPT(self, arg):
194
if not self._SMTPChannel__mailfrom:
195
self.push(EERR_551+'. Need MAIL command')
197
address = self._SMTPChannel__getaddr('TO:', arg) if arg else None
199
self.push(EERR_554+'. Syntax: RCPT TO:<address>')
201
# Call rcpttocheck to check if list address is a known address.
202
if self.rcpttocheck(address) == 'EERR_511':
205
# Call rcpttocheck to check if list address has syntax errors
206
if self.rcpttocheck(address) == 'EERR_513':
210
listname = self.listname(address)
211
subaddress = self.subaddress(address)
212
# Check if sender is authorised to post to list
213
if not subaddress in SUBADDRESS_NAMES:
214
if self.listmembercheck(self._SMTPChannel__mailfrom, address) == 'EERR_572':
217
if subaddress in SUBADDRESS_NAMES:
218
if self.listmembercheck(self._SMTPChannel__mailfrom, listname) == 'EERR_572':
219
if subaddress == 'leave' or subaddress == 'unsubscribe':
220
self.push(EERR_572+', the subaddresses -leave and -unsubscribe can not be used by unauthorised senders')
222
self._SMTPChannel__rcpttos.append(address)
226
381
class LMTPRunner(Runner, smtpd.SMTPServer):