"Fossies" - the Fresh Open Source Software Archive 
Member "getmail-5.16/getmailcore/destinations.py" (31 Oct 2021, 44147 Bytes) of package /linux/misc/getmail-5.16.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style:
standard) with prefixed line numbers.
Alternatively you can here
view or
download the uninterpreted source code file.
For more information about "destinations.py" see the
Fossies "Dox" file reference documentation and the latest
Fossies "Diffs" side-by-side code changes report:
5.15_vs_5.16.
1 #!/usr/bin/env python2
2 '''Classes implementing destinations (files, directories, or programs getmail
3 can deliver mail to).
4
5 Currently implemented:
6
7 Maildir
8 Mboxrd
9 MDA_qmaillocal (deliver though qmail-local as external MDA)
10 MDA_external (deliver through an arbitrary external MDA)
11 MultiSorter (deliver to a selection of maildirs/mbox files based on matching
12 recipient address patterns)
13 '''
14
15 __all__ = [
16 'DeliverySkeleton',
17 'Maildir',
18 'Mboxrd',
19 'MDA_qmaillocal',
20 'MDA_external',
21 'MultiDestinationBase',
22 'MultiDestination',
23 'MultiSorterBase',
24 'MultiSorter',
25 ]
26
27 import os
28 import re
29 import tempfile
30 import types
31 import email.Utils
32
33 import pwd
34
35 from getmailcore.exceptions import *
36 from getmailcore.utilities import *
37 from getmailcore.baseclasses import *
38
39 #######################################
40 class DeliverySkeleton(ConfigurableBase):
41 '''Base class for implementing message-delivery classes.
42
43 Sub-classes should provide the following data attributes and methods:
44
45 _confitems - a tuple of dictionaries representing the parameters the class
46 takes. Each dictionary should contain the following key,
47 value pairs:
48 - name - parameter name
49 - type - a type function to compare the parameter value
50 against (i.e. str, int, bool)
51 - default - optional default value. If not preseent, the
52 parameter is required.
53
54 __str__(self) - return a simple string representing the class instance.
55
56 showconf(self) - log a message representing the instance and configuration
57 from self._confstring().
58
59 initialize(self) - process instantiation parameters from self.conf.
60 Raise getmailConfigurationError on errors. Do any
61 other validation necessary, and set self.__initialized
62 when done.
63
64 retriever_info(self, retriever) - extract information from retriever and
65 store it for use in message deliveries.
66
67 _deliver_message(self, msg, delivered_to, received) - accept the message
68 and deliver it, returning a string describing the
69 result.
70
71 See the Maildir class for a good, simple example.
72 '''
73 def __init__(self, **args):
74 ConfigurableBase.__init__(self, **args)
75 try:
76 self.initialize()
77 except KeyError, o:
78 raise getmailConfigurationError(
79 'missing required configuration parameter %s' % o
80 )
81 self.received_from = None
82 self.received_with = None
83 self.received_by = None
84 self.retriever = None
85 self.log.trace('done\n')
86
87 def retriever_info(self, retriever):
88 self.log.trace()
89 self.received_from = retriever.received_from
90 self.received_with = retriever.received_with
91 self.received_by = retriever.received_by
92 self.retriever = retriever
93
94 def deliver_message(self, msg, delivered_to=True, received=True):
95 self.log.trace()
96 msg.received_from = self.received_from
97 msg.received_with = self.received_with
98 msg.received_by = self.received_by
99 return self._deliver_message(msg, delivered_to, received)
100
101 #######################################
102 class Maildir(DeliverySkeleton, ForkingBase):
103 '''Maildir destination.
104
105 Parameters:
106
107 path - path to maildir, which will be expanded for leading '~/' or
108 '~USER/', as well as environment variables.
109 '''
110 _confitems = (
111 ConfInstance(name='configparser', required=False),
112 ConfMaildirPath(name='path'),
113 ConfString(name='user', required=False, default=None),
114 ConfString(name='filemode', required=False, default='0600'),
115 )
116
117 def initialize(self):
118 self.log.trace()
119 self.hostname = localhostname()
120 self.dcount = 0
121 try:
122 self.conf['filemode'] = int(self.conf['filemode'], 8)
123 except ValueError, o:
124 raise getmailConfigurationError('filemode %s not valid: %s'
125 % (self.conf['filemode'], o))
126
127 def __str__(self):
128 self.log.trace()
129 return 'Maildir %s' % self.conf['path']
130
131 def showconf(self):
132 self.log.info('Maildir(%s)\n' % self._confstring())
133
134 def __deliver_message_maildir(self, uid, gid, msg, delivered_to, received,
135 stdout, stderr):
136 '''Delivery method run in separate child process.
137 '''
138 try:
139 if os.name == 'posix':
140 if uid:
141 change_uidgid(None, uid, gid)
142 if os.geteuid() == 0:
143 raise getmailConfigurationError(
144 'refuse to deliver mail as root'
145 )
146 if os.getegid() == 0:
147 raise getmailConfigurationError(
148 'refuse to deliver mail as GID 0'
149 )
150 f = deliver_maildir(
151 self.conf['path'], msg.flatten(delivered_to, received),
152 self.hostname, self.dcount, self.conf['filemode']
153 )
154 stdout.write(f)
155 stdout.flush()
156 os.fsync(stdout.fileno())
157 os._exit(0)
158 except StandardError, o:
159 # Child process; any error must cause us to exit nonzero for parent
160 # to detect it
161 stderr.write('maildir delivery process failed (%s)' % o)
162 stderr.flush()
163 os.fsync(stderr.fileno())
164 os._exit(127)
165
166 def _deliver_message(self, msg, delivered_to, received):
167 self.log.trace()
168 uid = None
169 gid = None
170 user = self.conf['user']
171 if os.name == 'posix':
172 if user and uid_of_user(user) != os.geteuid():
173 # Config specifies delivery as user other than current UID
174 uid = uid_of_user(user)
175 gid = gid_of_uid(uid)
176 if uid == 0:
177 raise getmailConfigurationError(
178 'refuse to deliver mail as root'
179 )
180 if gid == 0:
181 raise getmailConfigurationError(
182 'refuse to deliver mail as GID 0'
183 )
184 self._prepare_child()
185 stdout = tempfile.TemporaryFile()
186 stderr = tempfile.TemporaryFile()
187 childpid = os.fork()
188
189 if not childpid:
190 # Child
191 self.__deliver_message_maildir(uid, gid, msg, delivered_to,
192 received, stdout, stderr)
193 self.log.debug('spawned child %d\n' % childpid)
194
195 # Parent
196 exitcode = self._wait_for_child(childpid)
197
198 stdout.seek(0)
199 stderr.seek(0)
200 out = stdout.read().strip()
201 err = stderr.read().strip()
202
203 self.log.debug('maildir delivery process %d exited %d\n'
204 % (childpid, exitcode))
205
206 if exitcode or err:
207 raise getmailDeliveryError('maildir delivery %d error (%d, %s)'
208 % (childpid, exitcode, err))
209
210 self.dcount += 1
211 self.log.debug('maildir file %s' % out)
212 return self
213
214 #######################################
215 class Mboxrd(DeliverySkeleton, ForkingBase):
216 '''mboxrd destination with fcntl-style locking.
217
218 Parameters:
219
220 path - path to mboxrd file, which will be expanded for leading '~/'
221 or '~USER/', as well as environment variables.
222
223 Note the differences between various subtypes of mbox format (mboxrd, mboxo,
224 mboxcl, mboxcl2) and differences in locking; see the following for details:
225 http://qmail.org/man/man5/mbox.html
226 http://groups.google.com/groups?selm=4ivk9s%24bok%40hustle.rahul.net
227 '''
228 _confitems = (
229 ConfInstance(name='configparser', required=False),
230 ConfMboxPath(name='path'),
231 ConfString(name='locktype', required=False, default='lockf'),
232 ConfString(name='user', required=False, default=None),
233 )
234
235 def initialize(self):
236 self.log.trace()
237 if self.conf['locktype'] not in ('lockf', 'flock'):
238 raise getmailConfigurationError('unknown mbox lock type: %s'
239 % self.conf['locktype'])
240
241 def __str__(self):
242 self.log.trace()
243 return 'Mboxrd %s' % self.conf['path']
244
245 def showconf(self):
246 self.log.info('Mboxrd(%s)\n' % self._confstring())
247
248 def __deliver_message_mbox(self, uid, gid, msg, delivered_to, received,
249 stdout, stderr):
250 '''Delivery method run in separate child process.
251 '''
252 try:
253 if os.name == 'posix':
254 if uid:
255 change_uidgid(None, uid, gid)
256 if os.geteuid() == 0:
257 raise getmailConfigurationError(
258 'refuse to deliver mail as root'
259 )
260 if os.getegid() == 0:
261 raise getmailConfigurationError(
262 'refuse to deliver mail as GID 0'
263 )
264
265 if not os.path.exists(self.conf['path']):
266 raise getmailDeliveryError('mboxrd does not exist (%s)'
267 % self.conf['path'])
268 if not os.path.isfile(self.conf['path']):
269 raise getmailDeliveryError('not an mboxrd file (%s)'
270 % self.conf['path'])
271
272 # Open mbox file, refusing to create it if it doesn't exist
273 fd = os.open(self.conf['path'], os.O_RDWR)
274 status_old = os.fstat(fd)
275 f = os.fdopen(fd, 'r+b')
276 lock_file(f, self.conf['locktype'])
277 # Check if it _is_ an mbox file. mbox files must start with "From "
278 # in their first line, or are 0-length files.
279 f.seek(0, 0)
280 first_line = f.readline()
281 if first_line and not first_line.startswith('From '):
282 # Not an mbox file; abort here
283 unlock_file(f, self.conf['locktype'])
284 raise getmailDeliveryError('not an mboxrd file (%s)'
285 % self.conf['path'])
286 # Seek to end
287 f.seek(0, 2)
288 try:
289 # Write out message plus blank line with native EOL
290 f.write(msg.flatten(delivered_to, received, include_from=True,
291 mangle_from=True) + os.linesep)
292 f.flush()
293 os.fsync(fd)
294 status_new = os.fstat(fd)
295 # Reset atime
296 try:
297 os.utime(self.conf['path'], (status_old.st_atime,
298 status_new.st_mtime))
299 except OSError, o:
300 # Not root or owner; readers will not be able to reliably
301 # detect new mail. But you shouldn't be delivering to
302 # other peoples' mboxes unless you're root, anyways.
303 stdout.write('failed to updated mtime/atime of mbox')
304 stdout.flush()
305 os.fsync(stdout.fileno())
306
307 unlock_file(f, self.conf['locktype'])
308
309 except IOError, o:
310 try:
311 if not f.closed:
312 # If the file was opened and we know how long it was,
313 # try to truncate it back to that length
314 # If it's already closed, or the error occurred at
315 # close(), then there's not much we can do.
316 f.truncate(status_old.st_size)
317 except KeyboardInterrupt:
318 raise
319 except StandardError:
320 pass
321 raise getmailDeliveryError(
322 'failure writing message to mbox file "%s" (%s)'
323 % (self.conf['path'], o)
324 )
325
326 os._exit(0)
327
328 except StandardError, o:
329 # Child process; any error must cause us to exit nonzero for parent
330 # to detect it
331 stderr.write('mbox delivery process failed (%s)' % o)
332 stderr.flush()
333 os.fsync(stderr.fileno())
334 os._exit(127)
335
336 def _deliver_message(self, msg, delivered_to, received):
337 self.log.trace()
338 uid = None
339 gid = None
340 # Get user & group of mbox file
341 st_mbox = os.stat(self.conf['path'])
342 user = self.conf['user']
343 if os.name == 'posix':
344 if user and uid_of_user(user) != os.geteuid():
345 # Config specifies delivery as user other than current UID
346 uid = uid_of_user(user)
347 gid = gid_of_uid(uid)
348 if uid == 0:
349 raise getmailConfigurationError(
350 'refuse to deliver mail as root'
351 )
352 if gid == 0:
353 raise getmailConfigurationError(
354 'refuse to deliver mail as GID 0'
355 )
356 self._prepare_child()
357 stdout = tempfile.TemporaryFile()
358 stderr = tempfile.TemporaryFile()
359 childpid = os.fork()
360
361 if not childpid:
362 # Child
363 self.__deliver_message_mbox(uid, gid, msg, delivered_to, received,
364 stdout, stderr)
365 self.log.debug('spawned child %d\n' % childpid)
366
367 # Parent
368 exitcode = self._wait_for_child(childpid)
369
370 stdout.seek(0)
371 stderr.seek(0)
372 out = stdout.read().strip()
373 err = stderr.read().strip()
374
375 self.log.debug('mboxrd delivery process %d exited %d\n'
376 % (childpid, exitcode))
377
378 if exitcode or err:
379 raise getmailDeliveryError('mboxrd delivery %d error (%d, %s)'
380 % (childpid, exitcode, err))
381
382 if out:
383 self.log.debug('mbox delivery: %s' % out)
384
385 return self
386
387 #######################################
388 class MDA_qmaillocal(DeliverySkeleton, ForkingBase):
389 '''qmail-local MDA destination.
390
391 Passes the message to qmail-local for delivery. qmail-local is invoked as:
392
393 qmail-local -nN user homedir local dash ext domain sender defaultdelivery
394
395 Parameters (all optional):
396
397 qmaillocal - complete path to the qmail-local binary. Defaults
398 to "/var/qmail/bin/qmail-local".
399
400 user - username supplied to qmail-local as the "user" argument. Defaults
401 to the login name of the current effective user ID. If supplied,
402 getmail will also change the effective UID to that of the user
403 before running qmail-local.
404
405 group - If supplied, getmail will change the effective GID to that of the
406 named group before running qmail-local.
407
408 homedir - complete path to the directory supplied to qmail-local as the
409 "homedir" argument. Defaults to the home directory of the current
410 effective user ID.
411
412 localdomain - supplied to qmail-local as the "domain" argument. Defaults
413 to localhostname().
414
415 defaultdelivery - supplied to qmail-local as the "defaultdelivery"
416 argument. Defaults to "./Maildir/".
417
418 conf-break - supplied to qmail-local as the "dash" argument and used to
419 calculate ext from local. Defaults to "-".
420
421 localpart_translate - a string representing a Python 2-tuple of strings
422 (i.e. "('foo', 'bar')"). If supplied, the retrieved message
423 recipient address will have any leading instance of "foo" replaced
424 with "bar" before being broken into "local" and "ext" for qmail-
425 local (according to the values of "conf-break" and "user"). This
426 can be used to add or remove a prefix of the address.
427
428 strip_delivered_to - if set, existing Delivered-To: header fields will be
429 removed from the message before processing by qmail-local. This may
430 be necessary to prevent qmail-local falsely detecting a looping
431 message if (for instance) the system retrieving messages otherwise
432 believes it has the same domain name as the POP server.
433 Inappropriate use, however, may cause message loops.
434
435 allow_root_commands (boolean, optional) - if set, external commands are
436 allowed when running as root. The default is not to allow such
437 behaviour.
438
439 For example, if getmail is run as user "exampledotorg", which has virtual
440 domain "example.org" delegated to it with a virtualdomains entry of
441 "example.org:exampledotorg", and messages are retrieved with envelope
442 recipients like "trimtext-localpart@example.org", the messages could be
443 properly passed to qmail-local with a localpart_translate value of
444 "('trimtext-', '')" (and perhaps a defaultdelivery value of
445 "./Maildirs/postmaster/" or similar).
446 '''
447
448 _confitems = (
449 ConfInstance(name='configparser', required=False),
450 ConfFile(name='qmaillocal', required=False,
451 default='/var/qmail/bin/qmail-local'),
452 ConfString(name='user', required=False,
453 default=pwd.getpwuid(os.geteuid()).pw_name),
454 ConfString(name='group', required=False, default=None),
455 ConfDirectory(name='homedir', required=False,
456 default=pwd.getpwuid(os.geteuid()).pw_dir),
457 ConfString(name='localdomain', required=False, default=localhostname()),
458 ConfString(name='defaultdelivery', required=False,
459 default='./Maildir/'),
460 ConfString(name='conf-break', required=False, default='-'),
461 ConfTupleOfStrings(name='localpart_translate', required=False,
462 default="('', '')"),
463 ConfBool(name='strip_delivered_to', required=False, default=False),
464 ConfBool(name='allow_root_commands', required=False, default=False),
465 )
466
467 def initialize(self):
468 self.log.trace()
469
470 def __str__(self):
471 self.log.trace()
472 return 'MDA_qmaillocal %s' % self._confstring()
473
474 def showconf(self):
475 self.log.info('MDA_qmaillocal(%s)\n' % self._confstring())
476
477 def _deliver_qmaillocal(self, msg, msginfo, delivered_to, received, stdout,
478 stderr):
479 try:
480 args = (
481 self.conf['qmaillocal'], self.conf['qmaillocal'],
482 '--', self.conf['user'], self.conf['homedir'],
483 msginfo['local'], msginfo['dash'], msginfo['ext'],
484 self.conf['localdomain'], msginfo['sender'],
485 self.conf['defaultdelivery']
486 )
487 self.log.debug('about to execl() with args %s\n' % str(args))
488 # Modify message
489 if self.conf['strip_delivered_to']:
490 msg.remove_header('delivered-to')
491 # Also don't insert a Delivered-To: header.
492 delivered_to = None
493 # Write out message
494 msgfile = tempfile.TemporaryFile()
495 msgfile.write(msg.flatten(delivered_to, received))
496 msgfile.flush()
497 os.fsync(msgfile.fileno())
498 # Rewind
499 msgfile.seek(0)
500 # Set stdin to read from this file
501 os.dup2(msgfile.fileno(), 0)
502 # Set stdout and stderr to write to files
503 os.dup2(stdout.fileno(), 1)
504 os.dup2(stderr.fileno(), 2)
505 change_usergroup(self.log, self.conf['user'], self.conf['group'])
506 # At least some security...
507 if ((os.geteuid() == 0 or os.getegid() == 0)
508 and not self.conf['allow_root_commands']):
509 raise getmailConfigurationError(
510 'refuse to invoke external commands as root '
511 'or GID 0 by default'
512 )
513
514 os.execl(*args)
515 except StandardError, o:
516 # Child process; any error must cause us to exit nonzero for parent
517 # to detect it
518 stderr.write('exec of qmail-local failed (%s)' % o)
519 stderr.flush()
520 os.fsync(stderr.fileno())
521 os._exit(127)
522
523 def _deliver_message(self, msg, delivered_to, received):
524 self.log.trace()
525 self._prepare_child()
526 if msg.recipient == None:
527 raise getmailConfigurationError(
528 'MDA_qmaillocal destination requires a message source that '
529 'preserves the message envelope'
530 )
531 msginfo = {
532 'sender' : msg.sender,
533 'local' : '@'.join(msg.recipient.lower().split('@')[:-1])
534 }
535
536 self.log.debug('recipient: extracted local-part "%s"\n'
537 % msginfo['local'])
538 xlate_from, xlate_to = self.conf['localpart_translate']
539 if xlate_from or xlate_to:
540 if msginfo['local'].startswith(xlate_from):
541 self.log.debug('recipient: translating "%s" to "%s"\n'
542 % (xlate_from, xlate_to))
543 msginfo['local'] = xlate_to + msginfo['local'][len(xlate_from):]
544 else:
545 self.log.debug('recipient: does not start with xlate_from '
546 '"%s"\n' % xlate_from)
547 self.log.debug('recipient: translated local-part "%s"\n'
548 % msginfo['local'])
549 if self.conf['conf-break'] in msginfo['local']:
550 msginfo['dash'] = self.conf['conf-break']
551 msginfo['ext'] = self.conf['conf-break'].join(
552 msginfo['local'].split(self.conf['conf-break'])[1:]
553 )
554 else:
555 msginfo['dash'] = ''
556 msginfo['ext'] = ''
557 self.log.debug('recipient: set dash to "%s", ext to "%s"\n'
558 % (msginfo['dash'], msginfo['ext']))
559
560 stdout = tempfile.TemporaryFile()
561 stderr = tempfile.TemporaryFile()
562 childpid = os.fork()
563
564 if not childpid:
565 # Child
566 self._deliver_qmaillocal(msg, msginfo, delivered_to, received,
567 stdout, stderr)
568 self.log.debug('spawned child %d\n' % childpid)
569
570 # Parent
571 exitcode = self._wait_for_child(childpid)
572
573 stdout.seek(0)
574 stderr.seek(0)
575 out = stdout.read().strip()
576 err = stderr.read().strip()
577
578 self.log.debug('qmail-local %d exited %d\n' % (childpid, exitcode))
579
580 if exitcode == 111:
581 raise getmailDeliveryError('qmail-local %d temporary error (%s)'
582 % (childpid, err))
583 elif exitcode:
584 raise getmailDeliveryError('qmail-local %d error (%d, %s)'
585 % (childpid, exitcode, err))
586
587 if out and err:
588 info = '%s:%s' % (out, err)
589 else:
590 info = out or err
591
592 return 'MDA_qmaillocal (%s)' % info
593
594 #######################################
595 class MDA_external(DeliverySkeleton, ForkingBase):
596 '''Arbitrary external MDA destination.
597
598 Parameters:
599
600 path - path to the external MDA binary.
601
602 unixfrom - (boolean) whether to include a Unix From_ line at the beginning
603 of the message. Defaults to False.
604
605 arguments - a valid Python tuple of strings to be passed as arguments to
606 the command. The following replacements are available if
607 supported by the retriever:
608
609 %(sender) - envelope return path
610 %(recipient) - recipient address
611 %(domain) - domain-part of recipient address
612 %(local) - local-part of recipient address
613 %(mailbox) - for IMAP retrievers, the name of the
614 server-side mailbox/folder the message was retrieved
615 from. Will be empty for POP.
616
617 Warning: the text of these replacements is taken from the
618 message and is therefore under the control of a potential
619 attacker. DO NOT PASS THESE VALUES TO A SHELL -- they may
620 contain unsafe shell metacharacters or other hostile
621 constructions.
622
623 example:
624
625 path = /path/to/mymda
626 arguments = ('--demime', '-f%(sender)', '--', '%(recipient)')
627
628 user (string, optional) - if provided, the external command will be run as
629 the specified user. This requires that the main getmail process
630 have permission to change the effective user ID.
631
632 group (string, optional) - if provided, the external command will be run
633 with the specified group ID. This requires that the main getmail
634 process have permission to change the effective group ID.
635
636 allow_root_commands (boolean, optional) - if set, external commands are
637 allowed when running as root. The default is not to allow such
638 behaviour.
639
640 ignore_stderr (boolean, optional) - if set, getmail will not consider the
641 program writing to stderr to be an error. The default is False.
642 '''
643 _confitems = (
644 ConfInstance(name='configparser', required=False),
645 ConfFile(name='path'),
646 ConfTupleOfStrings(name='arguments', required=False, default="()"),
647 ConfString(name='user', required=False, default=None),
648 ConfString(name='group', required=False, default=None),
649 ConfBool(name='allow_root_commands', required=False, default=False),
650 ConfBool(name='unixfrom', required=False, default=False),
651 ConfBool(name='ignore_stderr', required=False, default=False),
652 )
653
654 def initialize(self):
655 self.log.trace()
656 self.conf['command'] = os.path.basename(self.conf['path'])
657 if not os.access(self.conf['path'], os.X_OK):
658 raise getmailConfigurationError('%s not executable'
659 % self.conf['path'])
660 if type(self.conf['arguments']) != tuple:
661 raise getmailConfigurationError(
662 'incorrect arguments format; see documentation (%s)'
663 % self.conf['arguments']
664 )
665
666 def __str__(self):
667 self.log.trace()
668 return 'MDA_external %s (%s)' % (self.conf['command'],
669 self._confstring())
670
671 def showconf(self):
672 self.log.info('MDA_external(%s)\n' % self._confstring())
673
674 def _deliver_command(self, msg, msginfo, delivered_to, received,
675 stdout, stderr):
676 try:
677 # Write out message with native EOL convention
678 msgfile = tempfile.TemporaryFile()
679 msgfile.write(msg.flatten(delivered_to, received,
680 include_from=self.conf['unixfrom']))
681 msgfile.flush()
682 os.fsync(msgfile.fileno())
683 # Rewind
684 msgfile.seek(0)
685 # Set stdin to read from this file
686 os.dup2(msgfile.fileno(), 0)
687 # Set stdout and stderr to write to files
688 os.dup2(stdout.fileno(), 1)
689 os.dup2(stderr.fileno(), 2)
690 change_usergroup(self.log, self.conf['user'], self.conf['group'])
691 # At least some security...
692 if ((os.geteuid() == 0 or os.getegid() == 0)
693 and not self.conf['allow_root_commands']):
694 raise getmailConfigurationError(
695 'refuse to invoke external commands as root '
696 'or GID 0 by default'
697 )
698 args = [self.conf['path'], self.conf['path']]
699 msginfo['mailbox'] = (self.retriever.mailbox_selected
700 or '').encode('utf-8')
701 for arg in self.conf['arguments']:
702 arg = expand_user_vars(arg)
703 for (key, value) in msginfo.items():
704 arg = arg.replace('%%(%s)' % key, value)
705 args.append(arg)
706 self.log.debug('about to execl() with args %s\n' % str(args))
707 os.execl(*args)
708 except StandardError, o:
709 # Child process; any error must cause us to exit nonzero for parent
710 # to detect it
711 stderr.write('exec of command %s failed (%s)'
712 % (self.conf['command'], o))
713 stderr.flush()
714 os.fsync(stderr.fileno())
715 os._exit(127)
716
717 def _deliver_message(self, msg, delivered_to, received):
718 self.log.trace()
719 self._prepare_child()
720 msginfo = {}
721 msginfo['sender'] = msg.sender
722 if msg.recipient != None:
723 msginfo['recipient'] = msg.recipient
724 msginfo['domain'] = msg.recipient.lower().split('@')[-1]
725 msginfo['local'] = '@'.join(msg.recipient.split('@')[:-1])
726 self.log.debug('msginfo "%s"\n' % msginfo)
727
728 stdout = tempfile.TemporaryFile()
729 stderr = tempfile.TemporaryFile()
730 childpid = os.fork()
731
732 if not childpid:
733 # Child
734 self._deliver_command(msg, msginfo, delivered_to, received,
735 stdout, stderr)
736 self.log.debug('spawned child %d\n' % childpid)
737
738 # Parent
739 exitcode = self._wait_for_child(childpid)
740
741 stdout.seek(0)
742 stderr.seek(0)
743 out = stdout.read().strip()
744 err = stderr.read().strip()
745
746 self.log.debug('command %s %d exited %d\n'
747 % (self.conf['command'], childpid, exitcode))
748
749 if exitcode:
750 raise getmailDeliveryError(
751 'command %s %d error (%d, %s)'
752 % (self.conf['command'], childpid, exitcode, err)
753 )
754 elif err:
755 if not self.conf['ignore_stderr']:
756 raise getmailDeliveryError(
757 'command %s %d wrote to stderr: %s'
758 % (self.conf['command'], childpid, err)
759 )
760 #else:
761 # User said to ignore stderr, just log it.
762 self.log.info('command %s: %s' % (self, err))
763
764 return 'MDA_external command %s (%s)' % (self.conf['command'], out)
765
766 #######################################
767 class MultiDestinationBase(DeliverySkeleton):
768 '''Base class for destinations which hand messages off to other
769 destinations.
770
771 Sub-classes must provide the following attributes and methods:
772
773 conf - standard ConfigurableBase configuration dictionary
774
775 log - getmailcore.logging.Logger() instance
776
777 In addition, sub-classes must populate the following list provided by
778 this base class:
779
780 _destinations - a list of all destination objects messages could be
781 handed to by this class.
782 '''
783
784 def _get_destination(self, path):
785 p = expand_user_vars(path)
786 if p.startswith('[') and p.endswith(']'):
787 destsectionname = p[1:-1]
788 if not destsectionname in self.conf['configparser'].sections():
789 raise getmailConfigurationError(
790 'destination specifies section name %s which does not exist'
791 % path
792 )
793 # Construct destination instance
794 self.log.debug(' getting destination for %s\n' % path)
795 destination_type = self.conf['configparser'].get(destsectionname,
796 'type')
797 self.log.debug(' type="%s"\n' % destination_type)
798 destination_func = globals().get(destination_type, None)
799 if not callable(destination_func):
800 raise getmailConfigurationError(
801 'configuration file section %s specifies incorrect '
802 'destination type (%s)'
803 % (destsectionname, destination_type)
804 )
805 destination_args = {'configparser' : self.conf['configparser']}
806 for (name, value) in self.conf['configparser'].items(destsectionname):
807 if name in ('type', 'configparser'):
808 continue
809 self.log.debug(' parameter %s="%s"\n' % (name, value))
810 destination_args[name] = value
811 self.log.debug(' instantiating destination %s with args %s\n'
812 % (destination_type, destination_args))
813 dest = destination_func(**destination_args)
814 elif (p.startswith('/') or p.startswith('.')) and p.endswith('/'):
815 dest = Maildir(path=p)
816 elif (p.startswith('/') or p.startswith('.')):
817 dest = Mboxrd(path=p)
818 else:
819 raise getmailConfigurationError(
820 'specified destination %s not of recognized type' % p
821 )
822 return dest
823
824 def initialize(self):
825 self.log.trace()
826 self._destinations = []
827
828 def retriever_info(self, retriever):
829 '''Override base class to pass this to the encapsulated destinations.
830 '''
831 self.log.trace()
832 DeliverySkeleton.retriever_info(self, retriever)
833 # Pass down to all destinations
834 for destination in self._destinations:
835 destination.retriever_info(retriever)
836
837 #######################################
838 class MultiDestination(MultiDestinationBase):
839 '''Send messages to one or more other destination objects unconditionally.
840
841 Parameters:
842
843 destinations - a tuple of strings, each specifying a destination that
844 messages should be delivered to. These strings will be expanded
845 for leading "~/" or "~user/" and environment variables,
846 then interpreted as maildir/mbox/other-destination-section.
847 '''
848 _confitems = (
849 ConfInstance(name='configparser', required=False),
850 ConfTupleOfStrings(name='destinations'),
851 )
852
853 def initialize(self):
854 self.log.trace()
855 MultiDestinationBase.initialize(self)
856 dests = [expand_user_vars(item) for item in self.conf['destinations']]
857 for item in dests:
858 try:
859 dest = self._get_destination(item)
860 except getmailConfigurationError, o:
861 raise getmailConfigurationError('%s destination error %s'
862 % (item, o))
863 self._destinations.append(dest)
864 if not self._destinations:
865 raise getmailConfigurationError('no destinations specified')
866
867 def _confstring(self):
868 '''Override the base class implementation.
869 '''
870 self.log.trace()
871 confstring = ''
872 for dest in self._destinations:
873 if confstring:
874 confstring += ', '
875 confstring += '%s' % dest
876 return confstring
877
878 def __str__(self):
879 self.log.trace()
880 return 'MultiDestination (%s)' % self._confstring()
881
882 def showconf(self):
883 self.log.info('MultiDestination(%s)\n' % self._confstring())
884
885 def _deliver_message(self, msg, delivered_to, received):
886 self.log.trace()
887 for dest in self._destinations:
888 dest.deliver_message(msg, delivered_to, received)
889 return self
890
891 #######################################
892 class MultiSorterBase(MultiDestinationBase):
893 '''Base class for multiple destinations with address matching.
894 '''
895
896 def initialize(self):
897 self.log.trace()
898 MultiDestinationBase.initialize(self)
899 self.default = self._get_destination(self.conf['default'])
900 self._destinations.append(self.default)
901 self.targets = []
902 try:
903 _locals = self.conf['locals']
904 # Special case for convenience if user supplied one base 2-tuple
905 if (len(_locals) == 2 and type(_locals[0]) == str
906 and type(_locals[1]) == str):
907 _locals = (_locals, )
908 for item in _locals:
909 if not (type(item) == tuple and len(item) == 2
910 and type(item[0]) == str and type(item[1]) == str):
911 raise getmailConfigurationError(
912 'invalid syntax for locals; see documentation'
913 )
914 for (pattern, path) in _locals:
915 try:
916 dest = self._get_destination(path)
917 except getmailConfigurationError, o:
918 raise getmailConfigurationError(
919 'pattern %s destination error %s' % (pattern, o)
920 )
921 self.targets.append((re.compile(pattern, re.IGNORECASE), dest))
922 self._destinations.append(dest)
923 except re.error, o:
924 raise getmailConfigurationError('invalid regular expression %s' % o)
925
926 def _confstring(self):
927 '''
928 Override the base class implementation; locals isn't readable that way.
929 '''
930 self.log.trace()
931 confstring = 'default=%s' % self.default
932 for (pattern, destination) in self.targets:
933 confstring += ', %s->%s' % (pattern.pattern, destination)
934 return confstring
935
936 #######################################
937 class MultiSorter(MultiSorterBase):
938 '''Multiple destination with envelope recipient address matching.
939
940 Parameters:
941
942 default - the default destination. Messages not matching any
943 "local" patterns (see below) will be delivered here.
944
945 locals - an optional tuple of items, each being a 2-tuple of quoted
946 strings. Each quoted string pair is a regular expression and a
947 maildir/mbox/other destination. In the general case, an email
948 address is a valid regular expression. Each pair is on a separate
949 line; the second and subsequent lines need to have leading
950 whitespace to be considered a continuation of the "locals"
951 configuration. If the recipient address matches a given pattern, it
952 will be delivered to the corresponding destination. A destination
953 is assumed to be a maildir if it starts with a dot or slash and ends
954 with a slash. A destination is assumed to be an mboxrd file if it
955 starts with a dot or a slash and does not end with a slash. A
956 destination may also be specified by section name, i.e.
957 "[othersectionname]". Multiple patterns may match a given recipient
958 address; the message will be delivered to /all/ destinations with
959 matching patterns. Patterns are matched case-insensitively.
960
961 example:
962
963 default = /home/kellyw/Mail/postmaster/
964 locals = (
965 ("jason@example.org", "/home/jasonk/Maildir/"),
966 ("sales@example.org", "/home/karlyk/Mail/sales"),
967 ("abuse@(example.org|example.net)", "/home/kellyw/Mail/abuse/"),
968 ("^(jeff|jefferey)(\.s(mith)?)?@.*$", "[jeff-mail-delivery]"),
969 ("^.*@(mail.)?rapinder.example.org$", "/home/rapinder/Maildir/")
970 )
971
972 In it's simplest form, locals is merely a list of pairs of email
973 addresses and corresponding maildir/mbox paths. Don't worry
974 about the details of regular expressions if you aren't familiar
975 with them.
976 '''
977 _confitems = (
978 ConfInstance(name='configparser', required=False),
979 ConfString(name='default'),
980 ConfTupleOfTupleOfStrings(name='locals', required=False, default="()"),
981 )
982
983 def __str__(self):
984 self.log.trace()
985 return 'MultiSorter (%s)' % self._confstring()
986
987 def showconf(self):
988 self.log.info('MultiSorter(%s)\n' % self._confstring())
989
990 def _deliver_message(self, msg, delivered_to, received):
991 self.log.trace()
992 matched = []
993 if msg.recipient == None and self.targets:
994 raise getmailConfigurationError(
995 'MultiSorter recipient matching requires a retriever (message '
996 'source) that preserves the message envelope'
997 )
998 for (pattern, dest) in self.targets:
999 self.log.debug('checking recipient %s against pattern %s\n'
1000 % (msg.recipient, pattern.pattern))
1001 if pattern.search(msg.recipient):
1002 self.log.debug('recipient %s matched target %s\n'
1003 % (msg.recipient, dest))
1004 dest.deliver_message(msg, delivered_to, received)
1005 matched.append(str(dest))
1006 if not matched:
1007 if self.targets:
1008 self.log.debug('recipient %s not matched; using default %s\n'
1009 % (msg.recipient, self.default))
1010 else:
1011 self.log.debug('using default %s\n' % self.default)
1012 return 'MultiSorter (default %s)' % self.default.deliver_message(
1013 msg, delivered_to, received
1014 )
1015 return 'MultiSorter (%s)' % matched
1016
1017 #######################################
1018 class MultiGuesser(MultiSorterBase):
1019 '''Multiple destination with header field address matching.
1020
1021 Parameters:
1022
1023 default - see MultiSorter for definition.
1024
1025 locals - see MultiSorter for definition.
1026
1027 '''
1028 _confitems = (
1029 ConfInstance(name='configparser', required=False),
1030 ConfString(name='default'),
1031 ConfTupleOfTupleOfStrings(name='locals', required=False, default="()"),
1032 )
1033
1034 def __str__(self):
1035 self.log.trace()
1036 return 'MultiGuesser (%s)' % self._confstring()
1037
1038 def showconf(self):
1039 self.log.info('MultiGuesser(%s)\n' % self._confstring())
1040
1041 def _deliver_message(self, msg, delivered_to, received):
1042 self.log.trace()
1043 matched = []
1044 header_addrs = []
1045 fieldnames = (
1046 ('delivered-to', ),
1047 ('envelope-to', ),
1048 ('x-envelope-to', ),
1049 ('apparently-to', ),
1050 ('resent-to', 'resent-cc', 'resent-bcc'),
1051 ('to', 'cc', 'bcc'),
1052 )
1053 for fields in fieldnames:
1054 for field in fields:
1055 self.log.debug(
1056 'looking for addresses in %s header fields\n' % field
1057 )
1058 header_addrs.extend(
1059 [addr for (name, addr) in email.Utils.getaddresses(
1060 msg.get_all(field, [])
1061 ) if addr]
1062 )
1063 if header_addrs:
1064 # Got some addresses, quit here
1065 self.log.debug('found total of %d addresses (%s)\n'
1066 % (len(header_addrs), header_addrs))
1067 break
1068 else:
1069 self.log.debug('no addresses found, continuing\n')
1070
1071 for (pattern, dest) in self.targets:
1072 for addr in header_addrs:
1073 self.log.debug('checking address %s against pattern %s\n'
1074 % (addr, pattern.pattern))
1075 if pattern.search(addr):
1076 self.log.debug('address %s matched target %s\n'
1077 % (addr, dest))
1078 dest.deliver_message(msg, delivered_to, received)
1079 matched.append(str(dest))
1080 # Only deliver once to each destination; this one matched,
1081 # so we don't need to check any remaining addresses against
1082 # this pattern
1083 break
1084 if not matched:
1085 if self.targets:
1086 self.log.debug('no addresses matched; using default %s\n'
1087 % self.default)
1088 else:
1089 self.log.debug('using default %s\n' % self.default)
1090 return 'MultiGuesser (default %s)' % self.default.deliver_message(
1091 msg, delivered_to, received
1092 )
1093 return 'MultiGuesser (%s)' % matched