~wilunix/mailman/lmtp

« back to all changes in this revision

Viewing changes to mailman/queue/lmtp.py

  • Committer: William Mead
  • Date: 2008-07-17 15:00:11 UTC
  • Revision ID: wam22@quant.staff.uscs.susx.ac.uk-20080717150011-kd2o03gdktp6s688
rev7: Complete LMTP channel, Fully uses enhanced error codes, LHLO command is required before MAIL command

Show diffs side-by-side

added added

removed removed

Lines of Context:
39
39
import smtpd
40
40
import logging
41
41
import asyncore
 
42
import asynchat
 
43
import socket
42
44
 
43
45
from email.utils import parseaddr
44
46
 
58
60
    'bounces',  'confirm',  'join',         'leave',
59
61
    'owner',    'request',  'subscribe',    'unsubscribe',
60
62
    )
61
 
 
 
63
    
 
64
EMPTYSTRING = ''
 
65
NEWLINE = '\n'
62
66
DASH    = '-'
63
67
CRLF    = '\r\n'
 
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
69
73
 
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'
 
75
EERR_200 = '2.0.0'
 
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'
76
82
 
77
83
 
78
84
# XXX Blech
79
 
smtpd.__version__ = 'Python LMTP queue runner 1.0'
 
85
__version__ = 'Python LMTP queue runner 1.1'
80
86
 
81
87
 
82
88
 
108
114
 
109
115
 
110
116
 
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
 
121
    
 
122
    COMMAND = 0
 
123
    DATA = 1
 
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
 
128
    LHLO = False
113
129
 
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
 
133
        self._conn = conn
 
134
        self._addr = addr
 
135
        self._line = []
 
136
        self._state = self.COMMAND
 
137
        self._greeting = 0
 
138
        self._mailfrom = None
 
139
        self._rcpttos = []
 
140
        self._data = ''
 
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__)
 
145
 
 
146
 
 
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')
 
150
        return
 
151
 
 
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')
 
155
        return
118
156
 
119
157
    def smtp_LHLO(self, arg):
120
 
        """The LMTP greeting, used instead of HELO/EHLO."""
121
 
        Channel.smtp_HELO(self, arg)
122
 
 
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."""
126
159
        if not arg:
127
 
            self.push(EERR_554+'. Syntax: lhlo hostname')
128
 
            return
129
 
        if self._SMTPChannel__greeting:
130
 
            self.push(EERR_551+'. Duplicate LHLO')
131
 
        else:
132
 
            self._SMTPChannel__greeting = arg
133
 
            self.push('250 %s' % self._SMTPChannel__fqdn)
 
160
            self.push('501 '+EERR_554+'. Syntax: lhlo hostname')
 
161
            return
 
162
        if self._greeting:
 
163
            self.push('503 '+EERR_551+'. Duplicate LHLO')
 
164
        else:
 
165
            self.LHLO = True
 
166
            self._greeting = arg
 
167
            self.push('250 '+EERR_200+' '+self._fqdn)
 
168
            self.push('250 '+EERR_200+' PIPELINING')
 
169
            self.push('250 '+EERR_200+' ENHANCEDSTATUSCODES')
 
170
 
 
171
    def smtp_MAIL(self, arg):
 
172
        if self.LHLO == False:
 
173
            self.push('503 '+EERR_551+'. Need LHLO command')
 
174
            return
 
175
        address = self._getaddr('FROM:', arg) if arg else None
 
176
        if not address:
 
177
            self.push('501 '+EERR_554+'. Syntax: MAIL FROM:<address>')
 
178
            return
 
179
        if self._mailfrom:
 
180
            self.push('503 '+EERR_551+'. Nested MAIL command')
 
181
            return
 
182
        self._mailfrom = address
 
183
        self.push('250 '+EERR_200+' Ok')
 
184
 
 
185
 
 
186
    def smtp_RCPT(self, arg):
 
187
        if not self._mailfrom:
 
188
            self.push('503 '+EERR_551+'. Need MAIL command')
 
189
            return
 
190
        address = self._getaddr('TO:', arg) if arg else None
 
191
        if not address:
 
192
            self.push('501 '+EERR_554+'. Syntax: RCPT TO:<address>')
 
193
            return
 
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)
 
197
            return
 
198
        # Call rcpttocheck to check if list address has syntax errors
 
199
        if self.rcpttocheck(address) == 'EERR_513':
 
200
            self.push('550 '+EERR_513)
 
201
            return
 
202
        # get subaddress
 
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)
 
209
                return   
 
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')
 
214
                    return
 
215
        self._rcpttos.append(address)
 
216
        self.push('250 '+EERR_200+' Ok') 
 
217
 
 
218
    def smtp_DATA(self, arg):
 
219
        if not self._rcpttos:
 
220
            self.push('503 '+EERR_551+'. Need RCPT command')
 
221
            return
 
222
        if arg:
 
223
            self.push('501 '+EERR_554+' Syntax: DATA')
 
224
            return
 
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>')
 
228
 
 
229
    def smtp_RSET(self, arg):
 
230
        if arg:
 
231
            self.push('501 Syntax: RSET')
 
232
            return
 
233
        # Resets the sender, recipients, and data, but not the greeting
 
234
        self._mailfrom = None
 
235
        self._rcpttos = []
 
236
        self._data = ''
 
237
        self._state = self.COMMAND
 
238
        self.push('250 '+EERR_200+' Reset Ok')
 
239
 
 
240
    def smtp_NOOP(self, arg):
 
241
        if arg:
 
242
            self.push('501 '+EERR_554+'. Syntax: NOOP')
 
243
        else:
 
244
            self.push('250 '+EERR_200+' Ok')
 
245
 
 
246
    def smtp_QUIT(self, arg):
 
247
        # args is ignored
 
248
        self.push('221 '+EERR_200+' Goodbye')
 
249
        self.close_when_done()
 
250
 
 
251
 
 
252
    # Overrides base class for convenience
 
253
    def push(self, msg):
 
254
        asynchat.async_chat.push(self, msg + '\r\n')
 
255
 
 
256
    # Implementation of base class abstract method
 
257
    def collect_incoming_data(self, data):
 
258
        self._line.append(data)
 
259
 
 
260
    # factored
 
261
    def _getaddr(self, keyword, arg):
 
262
        address = None
 
263
        keylen = len(keyword)
 
264
        if arg[:keylen].upper() == keyword:
 
265
            address = arg[keylen:].strip()
 
266
            if not address:
 
267
                pass
 
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]
 
272
        return address
 
273
 
 
274
    # Implementation of base class abstract method
 
275
    def found_terminator(self):
 
276
        line = EMPTYSTRING.join(self._line)
 
277
        self._line = []
 
278
        if self._state == self.COMMAND:
 
279
            if not line:
 
280
                self.push('500 '+EERR_551+'. Bad syntax')
 
281
                return
 
282
            method = None
 
283
            i = line.find(' ')
 
284
            if i < 0:
 
285
                command = line.upper()
 
286
                arg = None
 
287
            else:
 
288
                command = line[:i].upper()
 
289
                arg = line[i+1:].strip()
 
290
            method = getattr(self, 'smtp_' + command, None)
 
291
            if not method:
 
292
                self.push('500 '+EERR_551+'. Command "%s" not implemented' % command)
 
293
                return
 
294
            method(arg)
 
295
            return
 
296
        else:
 
297
            if self._state != self.DATA:
 
298
                self.push('451 '+EERR_450+'. Internal confusion')
 
299
                return
 
300
            # Remove extraneous carriage returns and de-transparency according
 
301
            # to RFC 821, Section 4.5.2.
 
302
            data = []
 
303
            for text in line.split('\r\n'):
 
304
                if text and text[0] == '.':
 
305
                    data.append(text[1:])
 
306
                else:
 
307
                    data.append(text)
 
308
            self._data = NEWLINE.join(data)
 
309
            status = self._server.process_message(self._peer,
 
310
                                                   self._mailfrom,
 
311
                                                   self._rcpttos,
 
312
                                                   self._data)
 
313
            self._rcpttos = []
 
314
            self._mailfrom = None
 
315
            self._state = self.COMMAND
 
316
            self.set_terminator('\r\n')
 
317
            if not status:
 
318
                self.push('250 '+EERR_200+' Ok')
 
319
            else:
 
320
                self.push(status)
 
321
 
134
322
 
135
323
    # lists gets all the lists
136
324
    def lists(self):
188
376
            return
189
377
        else:
190
378
            return 'EERR_572'
191
 
            
192
 
 
193
 
    def smtp_RCPT(self, arg):
194
 
        if not self._SMTPChannel__mailfrom:
195
 
            self.push(EERR_551+'. Need MAIL command')
196
 
            return
197
 
        address = self._SMTPChannel__getaddr('TO:', arg) if arg else None
198
 
        if not address:
199
 
            self.push(EERR_554+'. Syntax: RCPT TO:<address>')
200
 
            return
201
 
        # Call rcpttocheck to check if list address is a known address.
202
 
        if self.rcpttocheck(address) == 'EERR_511':
203
 
            self.push(EERR_511)
204
 
            return
205
 
        # Call rcpttocheck to check if list address has syntax errors
206
 
        if self.rcpttocheck(address) == 'EERR_513':
207
 
            self.push(EERR_513)
208
 
            return
209
 
        # get subaddress
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':
215
 
                self.push(EERR_572)
216
 
                return   
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')
221
 
                    return
222
 
        self._SMTPChannel__rcpttos.append(address)
223
 
        self.push('250 Ok') 
224
379
 
225
380
 
226
381
class LMTPRunner(Runner, smtpd.SMTPServer):
249
404
            # message, reject it right away; it's probably spam. 
250
405
            msg = email.message_from_string(data, Message)
251
406
            if msg.defects:
252
 
                return ERR_501
 
407
                return ('501 '+EERR_554+'. Message has defects')
253
408
            msg['X-MailFrom'] = mailfrom
254
409
        except Exception, e:
255
410
            elog.exception('LMTP message parsing')
256
411
            config.db.abort()
257
 
            return CRLF.join([ERR_451 for to in rcpttos])
 
412
            return CRLF.join(['451 '+EERR_450+'. Requested action aborted: error in processing' for to in rcpttos])
258
413
        # RFC 2033 requires us to return a status code for every recipient.
259
414
        status = []
260
415
        # Now for each address in the recipients, parse the address to first
269
424
                           to, listname, subaddress, domain)
270
425
                listname += '@' + domain
271
426
                if listname not in listnames:
272
 
                    status.append(EERR_511)
 
427
                    status.append('550 '+EERR_511)
273
428
                    continue
274
429
                # The recipient is a valid mailing list; see if it's a valid
275
430
                # sub-address, and if so, enqueue it.
301
456
                     queue = Switchboard(config.CMDQUEUE_DIR)
302
457
                else:
303
458
                    elog.error('Unknown sub-address: %s', subaddress)
304
 
                    status.append(EERR_511)
 
459
                    status.append('550 '+EERR_511)
305
460
                    continue
306
461
                # If we found a valid subaddress, enqueue the message and add
307
462
                # a success status for this recipient.
308
463
                if queue is not None:
309
464
                    queue.enqueue(msg, msgdata)
310
 
                    status.append('250 Ok')
 
465
                    status.append('250 '+EERR_200+' Ok Message enqueued for '+to)
311
466
            except Exception, e:
312
467
                elog.exception('Queue detection: %s', msg['message-id'])
313
468
                config.db.abort()
314
 
                status.append(EERR_513)
 
469
                status.append('550 '+EERR_513)
315
470
        # All done; returning this big status string should give the expected
316
471
        # response to the LMTP client.
317
472
        return CRLF.join(status)