This file is indexed.

/usr/share/pyshared/zope/sendmail/queue.py is in python-zope.sendmail 3.7.4-2.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Queue processor thread

This module contains the queue processor thread.

$Id: queue.py 107776 2010-01-07 10:37:46Z kobold $
"""
__docformat__ = 'restructuredtext'

import atexit
import ConfigParser
import logging
import os
import smtplib
import stat
import threading
import time

from zope.sendmail.maildir import Maildir
from zope.sendmail.mailer import SMTPMailer

import sys
if sys.platform == 'win32':
    import win32file
    _os_link = lambda src, dst: win32file.CreateHardLink(dst, src, None)
else:
    _os_link = os.link

# The longest time sending a file is expected to take.  Longer than this and
# the send attempt will be assumed to have failed.  This means that sending
# very large files or using very slow mail servers could result in duplicate
# messages sent.
MAX_SEND_TIME = 60*60*3

# The below diagram depicts the operations performed while sending a message in
# the ``run`` method of ``QueueProcessorThread``.  This sequence of operations
# will be performed for each file in the maildir each time the thread "wakes
# up" to send messages.
#
# Any error conditions not depected on the diagram will provoke the catch-all
# exception logging of the ``run`` method.
#
# In the diagram the "message file" is the file in the maildir's "cur" directory
# that contains the message and "tmp file" is a hard link to the message file
# created in the maildir's "tmp" directory.
#
#           ( start trying to deliver a message )
#                            |
#                            |
#                            V
#            +-----( get tmp file mtime )
#            |               |
#            |               | file exists
#            |               V
#            |         ( check age )-----------------------------+
#   tmp file |               |                       file is new |
#   does not |               | file is old                       |
#   exist    |               |                                   |
#            |      ( unlink tmp file )-----------------------+  |
#            |               |                      file does |  |
#            |               | file unlinked        not exist |  |
#            |               V                                |  |
#            +---->( touch message file )------------------+  |  |
#                            |                   file does |  |  |
#                            |                   not exist |  |  |
#                            V                             |  |  |
#            ( link message file to tmp file )----------+  |  |  |
#                            |                 tmp file |  |  |  |
#                            |           already exists |  |  |  |
#                            |                          |  |  |  |
#                            V                          V  V  V  V
#                     ( send message )             ( skip this message )
#                            |
#                            V
#                 ( unlink message file )---------+
#                            |                    |
#                            | file unlinked      | file no longer exists
#                            |                    |
#                            |  +-----------------+
#                            |  |
#                            |  V
#                  ( unlink tmp file )------------+
#                            |                    |
#                            | file unlinked      | file no longer exists
#                            V                    |
#                  ( message delivered )<---------+

class QueueProcessorThread(threading.Thread):
    """This thread is started at configuration time from the
    `mail:queuedDelivery` directive handler if processorThread is True.
    """

    log = logging.getLogger("QueueProcessorThread")
    _stopped = False
    interval = 3.0   # process queue every X second

    def __init__(self, interval=3.0):
        threading.Thread.__init__(self)
        self.interval = interval
        self._lock = threading.Lock()
        self.setDaemon(True)

    def setMaildir(self, maildir):
        """Set the maildir.

        This method is used just to provide a `maildir` stubs ."""
        self.maildir = maildir

    def setQueuePath(self, path):
        self.maildir = Maildir(path, True)

    def setMailer(self, mailer):
        self.mailer = mailer

    def _parseMessage(self, message):
        """Extract fromaddr and toaddrs from the first two lines of
        the `message`.

        Returns a fromaddr string, a toaddrs tuple and the message
        string.
        """

        fromaddr = ""
        toaddrs = ()
        rest = ""

        try:
            first, second, rest = message.split('\n', 2)
        except ValueError:
            return fromaddr, toaddrs, message

        if first.startswith("X-Zope-From: "):
            i = len("X-Zope-From: ")
            fromaddr = first[i:]

        if second.startswith("X-Zope-To: "):
            i = len("X-Zope-To: ")
            toaddrs = tuple(second[i:].split(", "))

        return fromaddr, toaddrs, rest

    def run(self, forever=True):
        atexit.register(self.stop)
        while not self._stopped:
            for filename in self.maildir:
                # if we are asked to stop while sending messages, do so
                if self._stopped:
                    break

                fromaddr = ''
                toaddrs = ()
                head, tail = os.path.split(filename)
                tmp_filename = os.path.join(head, '.sending-' + tail)
                rejected_filename = os.path.join(head, '.rejected-' + tail)
                try:
                    # perform a series of operations in an attempt to ensure
                    # that no two threads/processes send this message
                    # simultaneously as well as attempting to not generate
                    # spurious failure messages in the log; a diagram that
                    # represents these operations is included in a
                    # comment above this class
                    try:
                        # find the age of the tmp file (if it exists)
                        age = None
                        mtime = os.stat(tmp_filename)[stat.ST_MTIME]
                        age = time.time() - mtime
                    except OSError, e:
                        if e.errno == 2: # file does not exist
                            # the tmp file could not be stated because it
                            # doesn't exist, that's fine, keep going
                            pass
                        else:
                            # the tmp file could not be stated for some reason
                            # other than not existing; we'll report the error
                            raise

                    # if the tmp file exists, check it's age
                    if age is not None:
                        try:
                            if age > MAX_SEND_TIME:
                                # the tmp file is "too old"; this suggests
                                # that during an attemt to send it, the
                                # process died; remove the tmp file so we
                                # can try again
                                os.unlink(tmp_filename)
                            else:
                                # the tmp file is "new", so someone else may
                                # be sending this message, try again later
                                continue
                            # if we get here, the file existed, but was too
                            # old, so it was unlinked
                        except OSError, e:
                            if e.errno == 2: # file does not exist
                                # it looks like someone else removed the tmp
                                # file, that's fine, we'll try to deliver the
                                # message again later
                                continue

                    # now we know that the tmp file doesn't exist, we need to
                    # "touch" the message before we create the tmp file so the
                    # mtime will reflect the fact that the file is being
                    # processed (there is a race here, but it's OK for two or
                    # more processes to touch the file "simultaneously")
                    try:
                        os.utime(filename, None)
                    except OSError, e:
                        if e.errno == 2: # file does not exist
                            # someone removed the message before we could
                            # touch it, no need to complain, we'll just keep
                            # going
                            continue

                    # creating this hard link will fail if another process is
                    # also sending this message
                    try:
                        #os.link(filename, tmp_filename)
                        _os_link(filename, tmp_filename)
                    except OSError, e:
                        if e.errno == 17: # file exists, *nix
                            # it looks like someone else is sending this
                            # message too; we'll try again later
                            continue
                    except Exception, e:
                        if e[0] == 183 and e[1] == 'CreateHardLink':
                            # file exists, win32
                            continue

                    # read message file and send contents
                    file = open(filename)
                    message = file.read()
                    file.close()
                    fromaddr, toaddrs, message = self._parseMessage(message)
                    # The next block is the only one that is sensitive to
                    # interruptions.  Everywhere else, if this daemon thread
                    # stops, we should be able to correctly handle a restart.
                    # In this block, if we send the message, but we are
                    # stopped before we unlink the file, we will resend the
                    # message when we are restarted.  We limit the likelihood
                    # of this somewhat by using a lock to link the two
                    # operations.  When the process gets an interrupt, it
                    # will call the atexit that we registered (``stop``
                    # below).  This will try to get the same lock before it
                    # lets go.  Because this can cause the daemon thread to
                    # continue (that is, to not act like a daemon thread), we
                    # still use the _stopped flag to communicate.
                    self._lock.acquire()
                    try:
                        try:
                            self.mailer.send(fromaddr, toaddrs, message)
                        except smtplib.SMTPResponseException, e:
                            if 500 <= e.smtp_code <= 599:
                                # permanent error, ditch the message
                                self.log.error(
                                    "Discarding email from %s to %s due to"
                                    " a permanent error: %s",
                                    fromaddr, ", ".join(toaddrs), str(e))
                                #os.link(filename, rejected_filename)
                                _os_link(filename, rejected_filename)
                            else:
                                # Log an error and retry later
                                raise

                        try:
                            os.unlink(filename)
                        except OSError, e:
                            if e.errno == 2: # file does not exist
                                # someone else unlinked the file; oh well
                                pass
                            else:
                                # something bad happend, log it
                                raise
                    finally:
                        self._lock.release()
                    try:
                        os.unlink(tmp_filename)
                    except OSError, e:
                        if e.errno == 2: # file does not exist
                            # someone else unlinked the file; oh well
                            pass
                        else:
                            # something bad happend, log it
                            raise

                    # TODO: maybe log the Message-Id of the message sent
                    self.log.info("Mail from %s to %s sent.",
                                  fromaddr, ", ".join(toaddrs))
                    # Blanket except because we don't want
                    # this thread to ever die
                except:
                    if fromaddr != '' or toaddrs != ():
                        self.log.error(
                            "Error while sending mail from %s to %s.",
                            fromaddr, ", ".join(toaddrs), exc_info=True)
                    else:
                        self.log.error(
                            "Error while sending mail : %s ",
                            filename, exc_info=True)
            else:
                if forever:
                    time.sleep(self.interval)

            # A testing plug
            if not forever:
                break

    def stop(self):
        self._stopped = True
        self._lock.acquire()
        self._lock.release()


def boolean(s):
    s = str(s).lower()
    return s.startswith("t") or s.startswith("y") or s.startswith("1")


def string_or_none(s):
    if s == 'None':
        return None
    return s


class ConsoleApp(object):
    """Allows running of Queue Processor from the console."""

    _usage = """%(script_name)s [OPTIONS] path/to/maildir

    OPTIONS:

        --daemon            Run in daemon mode, periodically checking queue
                            and sending messages.  Default is to send all
                            messages in queue once and exit.

        --interval <#secs>  How often to check queue when in daemon mode.
                            Default is 3 seconds.

        --hostname          Name of smtp host to use for delivery.  Default is
                            localhost.

        --port              Which port on smtp server to deliver mail to.
                            Default is 25.

        --username          Username to use to log in to smtp server.  Default
                            is none.

        --password          Password to use to log in to smtp server.  Must be
                            specified if username is specified.

        --force-tls         Do not connect if TLS is not available.  Not
                            enabled by default.

        --no-tls            Do not use TLS even if is available.  Not enabled
                            by default.

        --config <inifile>  Get configuration from specificed ini file; it must
                            contain a section [app:zope-sendmail].

    """

    _error = False
    daemon = False
    interval = 3
    hostname = 'localhost'
    port = 25
    username = None
    password = None
    force_tls = False
    no_tls = False
    queue_path = None

    def __init__(self, argv=sys.argv, verbose=True):
        self.script_name = argv[0]
        self.verbose = verbose
        self._process_args(argv[1:])
        self.mailer = SMTPMailer(self.hostname, self.port, self.username,
            self.password, self.no_tls, self.force_tls)

    def main(self):
        if self._error:
            return
        queue = QueueProcessorThread(self.interval)
        queue.setMailer(self.mailer)
        queue.setQueuePath(self.queue_path)
        queue.run(forever=self.daemon)

    def _process_args(self, args):
        got_queue_path = False
        while args:
            arg = args.pop(0)
            if arg == "--daemon":
                self.daemon = True
            elif arg == "--interval":
                try:
                    self.interval = float(args.pop(0))
                except:
                    self._error_usage()
            elif arg == "--hostname":
                if not args:
                    self._error_usage()
                self.hostname = args.pop(0)
            elif arg == "--port":
                try:
                    self.port = int(args.pop(0))
                except:
                    self._error_usage()
            elif arg == "--username":
                if not args:
                    self._error_usage()
                self.username = args.pop(0)
            elif arg == "--password":
                if not args:
                    self._error_usage()
                self.password = args.pop(0)
            elif arg == "--force-tls":
                self.force_tls = True
            elif arg == "--no-tls":
                self.no_tls = True
            elif arg == "--config":
                if not args:
                    self._error_usage()
                self._load_config(args.pop(0))
            elif arg.startswith("-") or got_queue_path:
                self._error_usage()
            else:
                self.queue_path = arg
                got_queue_path = True
        if not self.queue_path:
            self._error_usage()
        if (self.username or self.password) and \
           not (self.username and self.password):
            if self.verbose:
                print >>sys.stderr, "Must use username and password together."
            self._error = True
        if self.force_tls and self.no_tls:
            if self.verbose:
                print >>sys.stderr, \
                    "--force-tls and --no-tls are mutually exclusive."
            self._error = True

    def _load_config(self, path):
        section = "app:zope-sendmail"
        names = [
            "interval",
            "hostname",
            "port",
            "username",
            "password",
            "force_tls",
            "no_tls",
            "queue_path",
        ]
        defaults = dict([(name, str(getattr(self, name))) for name in names])
        config = ConfigParser.ConfigParser(defaults)
        config.read(path)
        self.interval = float(config.get(section, "interval"))
        self.hostname = config.get(section, "hostname")
        self.port = int(config.get(section, "port"))
        self.username = string_or_none(config.get(section, "username"))
        self.password = string_or_none(config.get(section, "password"))
        self.force_tls = boolean(config.get(section, "force_tls"))
        self.no_tls = boolean(config.get(section, "no_tls"))
        self.queue_path = string_or_none(config.get(section, "queue_path"))

    def _error_usage(self):
        if self.verbose:
            print >>sys.stderr, self._usage % {"script_name": self.script_name,}
        self._error = True


def run():
    logging.basicConfig()
    app = ConsoleApp()
    app.main()


if __name__ == "__main__":
    run_console()