source: trunk/email2trac.py.in @ 404

Last change on this file since 404 was 404, checked in by bas, 14 years ago

email2trac.py.in:

  • used the SaraDict? class so we can use class attributes as dict values, eg:

b[k] = v is the same as b.k = v

  • some more logging code added




  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 59.3 KB
Line 
1#!@PYTHON@
2# Copyright (C) 2002
3#
4# This file is part of the email2trac utils
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
19#
20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
22"""
23email2trac.py -- Email tickets to Trac.
24
25A simple MTA filter to create Trac tickets from inbound emails.
26
27Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28Copyright 2005, Edgewall Software
29
30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
33
34The scripts reads emails from stdin and inserts directly into a Trac database.
35
36How to use
37----------
38 * See https://subtrac.sara.nl/oss/email2trac/
39
40 * Create an config file:
41    [DEFAULT]                        # REQUIRED
42    project      : /data/trac/test   # REQUIRED
43    debug        : 1                 # OPTIONAL, if set print some DEBUG info
44
45    [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
46    project      : /data/trac/jouvin # use -p|--project jouvin. 
47       
48 * default config file is : /etc/email2trac.conf
49
50 * Commandline opions:
51                -h,--help
52                -f,--file  <configuration file>
53                -n,--dry-run
54                -p, --project <project name>
55                -t, --ticket_prefix <name>
56
57SVN Info:
58        $Id: email2trac.py.in 404 2010-07-16 13:21:40Z bas $
59"""
60import os
61import sys
62import string
63import getopt
64import stat
65import time
66import email
67import email.Iterators
68import email.Header
69import re
70import urllib
71import unicodedata
72from stat import *
73import mimetypes
74import traceback
75import logging
76import logging.handlers
77import UserDict
78
79
80from trac import __version__ as trac_version
81
82# Will fail where unavailable, e.g. Windows
83#
84try:
85    import syslog
86    SYSLOG_AVAILABLE = True
87except ImportError:
88    SYSLOG_AVAILABLE = False
89
90from datetime import tzinfo, timedelta, datetime
91from trac import config as trac_config
92
93# Some global variables
94#
95trac_default_version = '0.11'
96m = None
97
98# A UTC class needed for trac version 0.11, added by
99# tbaschak at ktc dot mb dot ca
100#
101class UTC(tzinfo):
102        """UTC"""
103        ZERO = timedelta(0)
104        HOUR = timedelta(hours=1)
105       
106        def utcoffset(self, dt):
107                return self.ZERO
108               
109        def tzname(self, dt):
110                return "UTC"
111               
112        def dst(self, dt):
113                return self.ZERO
114
115class SaraDict(UserDict.UserDict):
116        def __init__(self, dictin = None):
117                UserDict.UserDict.__init__(self)
118                self.name = None
119               
120                if dictin:
121                        if dictin.has_key('name'):
122                                self.name = dictin['name']
123                                del dictin['name']
124                        self.data = dictin
125                       
126        def get_value(self, key):
127                if self.has_key(key):
128                        return self[key]
129                else:
130                        return None
131                               
132        def __repr__(self):
133                return repr(self.data)
134
135        def __str__(self):
136                return str(self.data)
137                       
138        def __getattr__(self, name):
139                """
140                override the class attribute get method. Return the value
141                from the Userdict
142                """
143                if self.data.has_key(name):
144                        return self.data[name]
145                else:
146                        return None
147                       
148        def __setattr__(self, name, value):
149                """
150                override the class attribute set method only when the UserDict
151                has set its class attribute
152                """
153                if self.__dict__.has_key('data'):
154                        self.data[name] = value
155                else:
156                        self.__dict__[name] = value
157
158        def __iter__(self):
159                return iter(self.data.keys())
160
161class TicketEmailParser(object):
162        env = None
163        comment = '> '
164
165        def __init__(self, env, parameters, version):
166                self.env = env
167
168                # Database connection
169                #
170                self.db = None
171
172                # Save parameters
173                #
174                self.parameters = parameters
175
176                # Some useful mail constants
177                #
178                self.email_name = None
179                self.email_addr = None
180                self.email_from = None
181                self.author     = None
182                self.id         = None
183               
184                self.STRIP_CONTENT_TYPES = list()
185
186                self.VERSION = version
187
188                self.get_config = self.env.config.get
189
190                self.setup_log()
191                self.setup_parameters()
192
193                self.DRY_RUN = parameters['dry_run']
194                self.VERBOSE = parameters['verbose']
195
196
197
198        def setup_log(self):
199                """
200                Setup loging
201
202                Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
203        would be interpreted by the ConfigParser itself.
204
205                """
206                if not self.parameters.log_type:
207                        self.parameters.log_type = 'syslog'
208
209                if self.parameters.log_type == 'file':
210                        if not os.path.isabs(parameters.log_file):
211                                self.parameters.log_file = os.path.join(self.env.path, 'log')
212                        hdlr = logging.FileHandler()
213
214                elif self.parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
215                        # Requires win32 extensions
216                        hdlr = logging.handlers.NTEventLogHandler(logid, logtype='Application')
217
218                elif self.parameters.log_type in ('syslog', 'unix'):
219                        hdlr = logging.handlers.SysLogHandler('/dev/log')
220
221                elif self.parameters.log_type in ('stderr'):
222                        hdlr = logging.StreamHandler(sys.stderr)
223
224                else:
225                        hdlr = logging.handlers.BufferingHandler(0)
226
227                if self.parameters.log_format:
228                        self.parameters['log_format'] = self.parameters.log_format.replace('$(', '%(')
229                else:
230                        self.parameters.log_format = 'Email2trac: %(message)s'
231
232
233                self.logger = logging.getLogger('email2trac')
234
235                ## Setup debug level
236               
237
238        def setup_parameters(self):
239                if self.parameters.has_key('umask'):
240                        os.umask(int(self.parameters['umask'], 8))
241
242                if self.parameters.has_key('debug'):
243                        self.DEBUG = int(self.parameters['debug'])
244                else:
245                        self.DEBUG = 0
246
247                if self.parameters.has_key('mailto_link'):
248                        self.MAILTO = int(self.parameters['mailto_link'])
249                        if self.parameters.has_key('mailto_cc'):
250                                self.MAILTO_CC = self.parameters['mailto_cc']
251                        else:
252                                self.MAILTO_CC = ''
253                else:
254                        self.MAILTO = 0
255
256                if self.parameters.has_key('spam_level'):
257                        self.SPAM_LEVEL = int(self.parameters['spam_level'])
258                else:
259                        self.SPAM_LEVEL = 0
260
261                if self.parameters.has_key('spam_header'):
262                        self.SPAM_HEADER = self.parameters['spam_header']
263                else:
264                        self.SPAM_HEADER = 'X-Spam-Score'
265
266                if self.parameters.has_key('email_quote'):
267                        self.EMAIL_QUOTE = str(self.parameters['email_quote'])
268                else:   
269                        self.EMAIL_QUOTE = '> '
270
271                if self.parameters.has_key('email_header'):
272                        self.EMAIL_HEADER = int(self.parameters['email_header'])
273                else:
274                        self.EMAIL_HEADER = 0
275
276                if self.parameters.has_key('alternate_notify_template'):
277                        self.notify_template = str(self.parameters['alternate_notify_template'])
278                else:
279                        self.notify_template = None
280
281                if self.parameters.has_key('alternate_notify_template_update'):
282                        self.notify_template_update = str(self.parameters['alternate_notify_template_update'])
283                else:
284                        self.notify_template_update = None
285
286                if self.parameters.has_key('reply_all'):
287                        self.REPLY_ALL = int(self.parameters['reply_all'])
288                else:
289                        self.REPLY_ALL = 0
290
291                if self.parameters.has_key('ticket_permission_system'):
292                        self.TICKET_PERMISSION_SYSTEM = str(self.parameters['ticket_permission_system'])
293                else:
294                        self.TICKET_PERMISSION_SYSTEM = None
295
296                if self.parameters.has_key('ticket_update'):
297                        self.TICKET_UPDATE = int(self.parameters['ticket_update'])
298                else:
299                        self.TICKET_UPDATE = 0
300
301                if self.parameters.has_key('ticket_update_by_subject'):
302                        self.TICKET_UPDATE_BY_SUBJECT = int(self.parameters['ticket_update_by_subject'])
303                else:
304                        self.TICKET_UPDATE_BY_SUBJECT = 0
305
306                if self.parameters.has_key('ticket_update_by_subject_lookback'):
307                        self.TICKET_UPDATE_BY_SUBJECT_LOOKBACK = int(self.parameters['ticket_update_by_subject_lookback'])
308                else:
309                        self.TICKET_UPDATE_BY_SUBJECT_LOOKBACK = 30
310
311                if self.parameters.has_key('drop_spam'):
312                        self.DROP_SPAM = int(self.parameters['drop_spam'])
313                else:
314                        self.DROP_SPAM = 0
315
316                if self.parameters.has_key('verbatim_format'):
317                        self.VERBATIM_FORMAT = int(self.parameters['verbatim_format'])
318                else:
319                        self.VERBATIM_FORMAT = 1
320
321                if self.parameters.has_key('reflow'):
322                        self.REFLOW = int(self.parameters['reflow'])
323                else:
324                        self.REFLOW = 1
325
326                if self.parameters.has_key('drop_alternative_html_version'):
327                        self.DROP_ALTERNATIVE_HTML_VERSION = int(self.parameters['drop_alternative_html_version'])
328                else:
329                        self.DROP_ALTERNATIVE_HTML_VERSION = 0
330
331                if self.parameters.has_key('strip_signature'):
332                        self.STRIP_SIGNATURE = int(self.parameters['strip_signature'])
333                else:
334                        self.STRIP_SIGNATURE = 0
335
336                if self.parameters.has_key('strip_quotes'):
337                        self.STRIP_QUOTES = int(self.parameters['strip_quotes'])
338                else:
339                        self.STRIP_QUOTES = 0
340
341                self.properties = dict()
342                if self.parameters.has_key('inline_properties'):
343                        self.INLINE_PROPERTIES = int(self.parameters['inline_properties'])
344                else:
345                        self.INLINE_PROPERTIES = 0
346
347                if self.parameters.has_key('use_textwrap'):
348                        self.USE_TEXTWRAP = int(self.parameters['use_textwrap'])
349                else:
350                        self.USE_TEXTWRAP = 0
351
352                if self.parameters.has_key('binhex'):
353                        self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
354
355                if self.parameters.has_key('applesingle'):
356                        self.STRIP_CONTENT_TYPES.append('application/applefile')
357
358                if self.parameters.has_key('appledouble'):
359                        self.STRIP_CONTENT_TYPES.append('application/applefile')
360
361                if self.parameters.has_key('strip_content_types'):
362                        items = self.parameters['strip_content_types'].split(',')
363                        for item in items:
364                                self.STRIP_CONTENT_TYPES.append(item.strip())
365
366                self.WORKFLOW = None
367                if self.parameters.has_key('workflow'):
368                        self.WORKFLOW = self.parameters['workflow']
369
370                # Use OS independend functions
371                #
372                self.TMPDIR = os.path.normcase('/tmp')
373                if self.parameters.has_key('tmpdir'):
374                        self.TMPDIR = os.path.normcase(str(self.parameters['tmpdir']))
375
376                if self.parameters.has_key('ignore_trac_user_settings'):
377                        self.IGNORE_TRAC_USER_SETTINGS = int(self.parameters['ignore_trac_user_settings'])
378                else:
379                        self.IGNORE_TRAC_USER_SETTINGS = 0
380
381                if self.parameters.has_key('email_triggers_workflow'):
382                        self.EMAIL_TRIGGERS_WORKFLOW = int(self.parameters['email_triggers_workflow'])
383                else:
384                        self.EMAIL_TRIGGERS_WORKFLOW = 1
385
386                if self.parameters.has_key('subject_field_separator'):
387                        self.SUBJECT_FIELD_SEPARATOR = self.parameters['subject_field_separator'].strip()
388                else:
389                        self.SUBJECT_FIELD_SEPARATOR = '&'
390
391                self.trac_smtp_from = self.get_config('notification', 'smtp_from')
392
393                self.system = None
394
395########## Email Header Functions ###########################################################
396
397        def spam(self, message):
398                """
399                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
400                # Note if Spam_level then '*' are included
401                """
402                spam = False
403                if message.has_key(self.SPAM_HEADER):
404                        spam_l = string.split(message[self.SPAM_HEADER])
405
406                        try:
407                                number = spam_l[0].count('*')
408                        except IndexError, detail:
409                                number = 0
410                               
411                        if number >= self.SPAM_LEVEL:
412                                spam = True
413                               
414                # treat virus mails as spam
415                #
416                elif message.has_key('X-Virus-found'):                 
417                        spam = True
418
419                # How to handle SPAM messages
420                #
421                if self.DROP_SPAM and spam:
422                        if self.DEBUG > 2 :
423                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
424
425                        return 'drop'   
426
427                elif spam:
428
429                        return 'Spam'   
430
431                else:
432
433                        return False
434
435        def email_header_acl(self, keyword, header_field, default):
436                """
437                This function wil check if the email address is allowed or denied
438                to send mail to the ticket list
439            """
440                try:
441                        mail_addresses = self.parameters[keyword]
442
443                        # Check if we have an empty string
444                        #
445                        if not mail_addresses:
446                                return default
447
448                except KeyError, detail:
449                        if self.DEBUG > 2 :
450                                print 'TD: %s not defined, all messages are allowed.' %(keyword)
451
452                        return default
453
454                mail_addresses = string.split(mail_addresses, ',')
455
456                for entry in mail_addresses:
457                        entry = entry.strip()
458                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
459                        result =  TO_RE.search(header_field)
460                        if result:
461                                return True
462
463                return False
464
465        def email_header_txt(self, m):
466                """
467                Display To and CC addresses in description field
468                """
469                s = ''
470
471                if m['To'] and len(m['To']) > 0:
472                        s = "'''To:''' %s\r\n" %(m['To'])
473                if m['Cc'] and len(m['Cc']) > 0:
474                        s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
475
476                return  self.email_to_unicode(s)
477
478
479        def get_sender_info(self, message):
480                """
481                Get the default author name and email address from the message
482                """
483
484                self.email_to = self.email_to_unicode(message['to'])
485                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
486
487                self.email_from = self.email_to_unicode(message['from'])
488                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
489
490                ## Trac can not handle author's name that contains spaces
491                #  and forbid the ticket email address as author field
492
493                if self.email_addr == self.trac_smtp_from:
494                        if self.email_name:
495                                self.author = self.email_name
496                        else:
497                                self.author = "email2trac"
498                else:
499                        self.author = self.email_addr
500
501                if self.IGNORE_TRAC_USER_SETTINGS:
502                        return
503
504                # Is this a registered user, use email address as search key:
505                # result:
506                #   u : login name
507                #   n : Name that the user has set in the settings tab
508                #   e : email address that the user has set in the settings tab
509                #
510                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
511                        if e and (e.lower() == self.email_addr.lower()) ]
512
513                if len(users) == 1:
514                        self.email_from = users[0][0]
515                        self.author = users[0][0]
516
517        def set_reply_fields(self, ticket, message):
518                """
519                Set all the right fields for a new ticket
520                """
521                if self.DEBUG:
522                        print 'TD: set_reply_fields'
523
524                ## Only use name or email adress
525                #ticket['reporter'] = self.email_from
526                ticket['reporter'] = self.author
527
528
529                # Put all CC-addresses in ticket CC field
530                #
531                if self.REPLY_ALL:
532
533                        email_cc = ''
534
535                        cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
536
537                        if not cc_addrs:
538                                return
539
540                        ## Build a list of forbidden CC addresses
541                        #
542                        #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
543                        #to_list = list()
544                        #for n,e in to_addrs:
545                        #       to_list.append(e)
546                               
547                        # Always Remove reporter email address from cc-list
548                        #
549                        try:
550                                cc_addrs.remove((self.author, self.email_addr))
551                        except ValueError, detail:
552                                pass
553
554                        for name,addr in cc_addrs:
555               
556                                ## Prevent mail loop
557                                #
558                                #if addr in to_list:
559
560                                if addr == self.trac_smtp_from:
561                                        if self.DEBUG:
562                                                print "Skipping %s mail address for CC-field" %(addr)
563                                        continue
564
565                                if email_cc:
566                                        email_cc = '%s, %s' %(email_cc, addr)
567                                else:
568                                        email_cc = addr
569
570                        if email_cc:
571                                if self.DEBUG:
572                                        print 'TD: set_reply_fields: %s' %email_cc
573
574                                ticket['cc'] = self.email_to_unicode(email_cc)
575
576
577########## DEBUG functions  ###########################################################
578
579        def debug_body(self, message_body, tempfile=False):
580                if tempfile:
581                        import tempfile
582                        body_file = tempfile.mktemp('.email2trac')
583                else:
584                        body_file = os.path.join(self.TMPDIR, 'body.txt')
585
586                if self.DRY_RUN:
587                        print 'DRY-RUN: not saving body to %s' %(body_file)
588                        return
589
590                print 'TD: writing body to %s' %(body_file)
591                fx = open(body_file, 'wb')
592                if not message_body:
593                                message_body = '(None)'
594
595                message_body = message_body.encode('utf-8')
596                #message_body = unicode(message_body, 'iso-8859-15')
597
598                fx.write(message_body)
599                fx.close()
600                try:
601                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
602                except OSError:
603                        pass
604
605        def debug_attachments(self, message_parts):
606                """
607                """
608                if self.VERBOSE:
609                        print "VB: debug_attachments"
610               
611                n = 0
612                for item in message_parts:
613                        # Skip inline text parts
614                        if not isinstance(item, tuple):
615                                continue
616                               
617                        (original, filename, part) = item
618
619                        n = n + 1
620                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
621               
622                        s = 'TD: part%d: filename: %s' %(n, filename)
623                        self.print_unicode(s)
624       
625                        ## Forbidden chars
626                        #
627                        filename = filename.replace('\\', '_')
628                        filename = filename.replace('/', '_')
629       
630
631                        part_file = os.path.join(self.TMPDIR, filename)
632                        s = 'TD: writing part%d (%s)' % (n,part_file)
633                        self.print_unicode(s)
634
635                        if self.DRY_RUN:
636                                print 'DRY_RUN: NOT saving attachments'
637                                continue
638
639                        part_file = util.text.unicode_quote(part_file)
640
641                        fx = open(part_file, 'wb')
642                        text = part.get_payload(decode=1)
643
644                        if not text:
645                                text = '(None)'
646
647                        fx.write(text)
648                        fx.close()
649
650                        try:
651                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
652                        except OSError:
653                                pass
654
655        def save_email_for_debug(self, message, tempfile=False):
656
657                if tempfile:
658                        import tempfile
659                        msg_file = tempfile.mktemp('.email2trac')
660                else:
661                        #msg_file = '/var/tmp/msg.txt'
662                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
663
664                if self.DRY_RUN:
665                        print 'DRY_RUN: NOT saving email message to %s' %(msg_file)
666                else:
667                        print 'TD: saving email to %s' %(msg_file)
668
669                        fx = open(msg_file, 'wb')
670                        fx.write('%s' % message)
671                        fx.close()
672                       
673                        try:
674                                os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
675                        except OSError:
676                                pass
677
678                message_parts = self.get_message_parts(message)
679                message_parts = self.unique_attachment_names(message_parts)
680                body_text = self.body_text(message_parts)
681                self.debug_body(body_text, True)
682                self.debug_attachments(message_parts)
683
684########## Conversion functions  ###########################################################
685
686        def email_to_unicode(self, message_str):
687                """
688                Email has 7 bit ASCII code, convert it to unicode with the charset
689                that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
690                understands it.
691                """
692                if self.VERBOSE:
693                        print "VB: email_to_unicode"
694
695                results =  email.Header.decode_header(message_str)
696
697                s = None
698                for text,format in results:
699                        if format:
700                                try:
701                                        temp = unicode(text, format)
702                                except UnicodeError, detail:
703                                        # This always works
704                                        #
705                                        temp = unicode(text, 'iso-8859-15')
706                                except LookupError, detail:
707                                        #text = 'ERROR: Could not find charset: %s, please install' %format
708                                        #temp = unicode(text, 'iso-8859-15')
709                                        temp = message_str
710                                       
711                        else:
712                                temp = string.strip(text)
713                                temp = unicode(text, 'iso-8859-15')
714
715                        if s:
716                                s = '%s %s' %(s, temp)
717                        else:
718                                s = '%s' %temp
719
720                #s = s.encode('utf-8')
721                return s
722
723        def str_to_dict(self, s):
724                """
725                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
726                """
727                if self.VERBOSE:
728                        print "VB: str_to_dict"
729
730                fields = string.split(s, self.SUBJECT_FIELD_SEPARATOR)
731
732                result = dict()
733                for field in fields:
734                        try:
735                                index, value = string.split(field, '=')
736
737                                # We can not change the description of a ticket via the subject
738                                # line. The description is the body of the email
739                                #
740                                if index.lower() in ['description']:
741                                        continue
742
743                                if value:
744                                        result[index.lower()] = value
745
746                        except ValueError:
747                                pass
748                return result
749
750        def print_unicode(self,s):
751                """
752                This function prints unicode strings uif possible else it will quote it
753                """
754                try:
755                        print s
756                except UnicodeEncodeError, detail:
757                        print util.text.unicode_quote(s)
758
759########## TRAC ticket functions  ###########################################################
760
761        def check_permission_participants(self, tkt):
762                """
763                Check if the mailer is allowed to update the ticket
764                """
765
766                if tkt['reporter'].lower() in [self.author, self.email_addr]:
767                        if self.DEBUG:
768                                print 'ALLOW, %s is the ticket reporter' %(self.email_addr)
769                        return True
770
771                perm = PermissionSystem(self.env)
772                if perm.check_permission('TICKET_MODIFY', self.author):
773                        if self.DEBUG:
774                                print 'ALLOW, %s has trac permission to update the ticket' %(self.author)
775                        return True
776                else:
777                        return False
778               
779
780                # Is the updater in the CC?
781                try:
782                        cc_list = tkt['cc'].split(',')
783                        for cc in cc_list:
784                                if self.email_addr.lower() in cc.strip():
785
786                                        if self.DEBUG:
787                                                print 'ALLOW, %s is in the CC' %(self.email_addr)
788
789                                        return True
790
791                except KeyError:
792                        return False
793
794        def check_permission(self, tkt, action):
795                """
796                check if the reporter has the right permission for the action:
797          - TICKET_CREATE
798          - TICKET_MODIFY
799
800                There are three models:
801                        - None      : no checking at all
802                        - trac      : check the permission via trac permission model
803                        - email2trac: ....
804                """
805                if self.VERBOSE:
806                        print "VB: check_permission"
807
808                if self.TICKET_PERMISSION_SYSTEM in ['trac']:
809
810                        perm = PermissionSystem(self.env)
811                        if perm.check_permission(action, self.author):
812                                return True
813                        else:
814                                return False
815
816                elif self.TICKET_PERMISSION_SYSTEM in ['update_restricted_to_participants']:
817                        if action in ['TICKET_MODIFY']:
818                                return (self.check_permission_participants(tkt))       
819                        else:
820                                return True
821
822                # Default is to allow everybody ticket updates and ticket creation
823                else:
824                                return True
825
826
827        def update_ticket_fields(self, ticket, user_dict, use_default=None):
828                """
829                This will update the ticket fields. It will check if the
830                given fields are known and if the right values are specified
831                It will only update the ticket field value:
832                        - If the field is known
833                        - If the value supplied is valid for the ticket field.
834                          If not then there are two options:
835                           1) Skip the value (use_default=None)
836                           2) Set default value for field (use_default=1)
837                """
838                if self.VERBOSE:
839                        print "VB: update_ticket_fields"
840
841                # Build a system dictionary from the ticket fields
842                # with field as index and option as value
843                #
844                sys_dict = dict()
845                for field in ticket.fields:
846                        try:
847                                sys_dict[field['name']] = field['options']
848
849                        except KeyError:
850                                sys_dict[field['name']] = None
851                                pass
852
853                ## Check user supplied fields an compare them with the
854                # system one's
855                #
856                for field,value in user_dict.items():
857                        if self.DEBUG >= 10:
858                                s = 'TD: user_field\t %s = %s' %(field,value)
859                                self.print_unicode(s)
860
861                        ## To prevent mail loop
862                        #
863                        if field == 'cc':
864
865                                cc_list = user_dict['cc'].split(',')
866
867                                if self.trac_smtp_from in cc_list:
868                                        if self.DEBUG > 10:
869                                                print 'TD: MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from)
870                                        cc_list.remove(self.trac_smtp_from)
871
872                                value = ','.join(cc_list)
873                               
874
875                        if sys_dict.has_key(field):
876
877                                # Check if value is an allowed system option, if TypeError then
878                                # every value is allowed
879                                #
880                                try:
881                                        if value in sys_dict[field]:
882                                                ticket[field] = value
883                                        else:
884                                                # Must we set a default if value is not allowed
885                                                #
886                                                if use_default:
887                                                        value = self.get_config('ticket', 'default_%s' %(field) )
888
889                                except TypeError:
890                                        pass
891
892                                ## Only set if we have a value
893                                #
894                                if value:
895                                        ticket[field] = value
896
897                                if self.DEBUG >= 10:
898                                        s = 'ticket_field\t %s = %s' %(field,  ticket[field])
899                                        self.print_unicode(s)
900
901        def ticket_update(self, m, id, spam):
902                """
903                If the current email is a reply to an existing ticket, this function
904                will append the contents of this email to that ticket, instead of
905                creating a new one.
906                """
907                if self.VERBOSE:
908                        print "VB: ticket_update: %s" %id
909
910                # Must we update ticket fields
911                #
912                update_fields = dict()
913                try:
914                        id, keywords = string.split(id, '?')
915
916                        update_fields = self.str_to_dict(keywords)
917
918                        # Strip '#'
919                        #
920                        self.id = int(id[1:])
921
922                except ValueError:
923
924                        # Strip '#'
925                        #
926                        self.id = int(id[1:])
927
928                if self.VERBOSE:
929                        print "VB: ticket_update: %s" %id
930
931
932                # When is the change committed
933                #
934                if self.VERSION < 0.11:
935                        when = int(time.time())
936                else:
937                        utc = UTC()
938                        when = datetime.now(utc)
939
940                try:
941                        tkt = Ticket(self.env, self.id, self.db)
942
943                except util.TracError, detail:
944
945                        # Not a valid ticket
946
947                        self.id = None
948                        return False
949
950                # Check the permission of the reporter
951                #
952                if self.TICKET_PERMISSION_SYSTEM:
953                        if not self.check_permission(tkt, 'TICKET_MODIFY'):
954                                print 'Reporter: %s has no permission to modify tickets' %self.author
955                                return False
956
957                # How many changes has this ticket
958                cnum = len(tkt.get_changelog())
959
960
961                # reopen the ticket if it is was closed
962                # We must use the ticket workflow framework
963                #
964                if tkt['status'] in ['closed'] and self.EMAIL_TRIGGERS_WORKFLOW:
965
966                        #print controller.actions['reopen']
967                        #
968                        # As reference 
969                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
970                        #
971                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
972                        #print 'controller : ', a
973                        #
974                        #b = controller.get_all_status()
975                        #print 'get all status: ', b
976                        #
977                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
978                        #print 'get_ticket_changes :', b
979
980                        if self.WORKFLOW and (self.VERSION >= 0.11 ) :
981                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
982                                from trac.test import Mock, MockPerm
983
984                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
985
986                                controller = ConfigurableTicketWorkflow(self.env)
987                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
988
989                                if self.DEBUG:
990                                        print 'TD: Workflow ticket update fields: ', fields
991
992                                for key in fields.keys():
993                                        tkt[key] = fields[key]
994
995                        else:
996                                tkt['status'] = 'reopened'
997                                tkt['resolution'] = ''
998
999                # Must we update some ticket fields properties via subjectline
1000                #
1001                if update_fields:
1002                        self.update_ticket_fields(tkt, update_fields)
1003
1004                message_parts = self.get_message_parts(m)
1005                message_parts = self.unique_attachment_names(message_parts)
1006
1007                # Must we update some ticket fields properties via body_text
1008                #
1009                if self.properties:
1010                                self.update_ticket_fields(tkt, self.properties)
1011
1012                if self.EMAIL_HEADER:
1013                        message_parts.insert(0, self.email_header_txt(m))
1014
1015                body_text = self.body_text(message_parts)
1016
1017                error_with_attachments = self.attach_attachments(message_parts)
1018
1019                if body_text.strip() or update_fields or self.properties:
1020                        if self.DRY_RUN:
1021                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
1022                        else:
1023                                if error_with_attachments:
1024                                        body_text = '%s\\%s' %(error_with_attachments, body_text)
1025                               
1026                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
1027                       
1028
1029                if not spam:
1030                        self.notify(tkt, False, when)
1031
1032                return True
1033
1034        def set_ticket_fields(self, ticket):
1035                """
1036                set the ticket fields to value specified
1037                        - /etc/email2trac.conf with <prefix>_<field>
1038                        - trac default values, trac.ini
1039                """
1040                user_dict = dict()
1041
1042                for field in ticket.fields:
1043
1044                        name = field['name']
1045
1046                        ## default trac value
1047                        #
1048                        if not field.get('custom'):
1049                                value = self.get_config('ticket', 'default_%s' %(name) )
1050                        else:
1051                                ##  Else we get the default value for reporter
1052                                #
1053                                value = field.get('value')
1054                                options = field.get('options')
1055
1056                                if value and options and (value not in options):
1057                                         value = options[int(value)]
1058       
1059                        if self.DEBUG > 10:
1060                                s = 'TD: trac.ini name %s = %s' %(name, value)
1061                                self.print_unicode(s)
1062
1063                        ## email2trac.conf settings
1064                        #
1065                        prefix = self.parameters['ticket_prefix']
1066                        try:
1067                                value = self.parameters['%s_%s' %(prefix, name)]
1068                                if self.DEBUG > 10:
1069                                        s = 'TD: email2trac.conf %s = %s ' %(name, value)
1070                                        self.print_unicode(s)
1071
1072                        except KeyError, detail:
1073                                pass
1074               
1075                        if self.DEBUG:
1076                                s = 'TD: user_dict[%s] = %s' %(name, value)
1077                                self.print_unicode(s)
1078
1079                        if value:
1080                                user_dict[name] = value
1081
1082                self.update_ticket_fields(ticket, user_dict, use_default=1)
1083
1084                if 'status' not in user_dict.keys():
1085                        ticket['status'] = 'new'
1086
1087
1088        def ticket_update_by_subject(self, subject):
1089                """
1090                This list of Re: prefixes is probably incomplete. Taken from
1091                wikipedia. Here is how the subject is matched
1092                  - Re: <subject>
1093                  - Re: (<Mail list label>:)+ <subject>
1094
1095                So we must have the last column
1096                """
1097                if self.VERBOSE:
1098                        print "VB: ticket_update_by_subject()"
1099
1100                matched_id = None
1101                if self.TICKET_UPDATE and self.TICKET_UPDATE_BY_SUBJECT:
1102                               
1103                        SUBJECT_RE = re.compile(r'^(RE|AW|VS|SV):(.*:)*\s*(.*)', re.IGNORECASE)
1104                        result = SUBJECT_RE.search(subject)
1105
1106                        if result:
1107                                # This is a reply
1108                                orig_subject = result.group(3)
1109
1110                                if self.DEBUG:
1111                                        print 'TD: subject search string: %s' %(orig_subject)
1112
1113                                cursor = self.db.cursor()
1114                                summaries = [orig_subject, '%%: %s' % orig_subject]
1115
1116                                ##
1117                                # Convert days to seconds
1118                                lookback = int(time.mktime(time.gmtime())) - \
1119                                                self.TICKET_UPDATE_BY_SUBJECT_LOOKBACK * 24 * 3600
1120
1121
1122                                for summary in summaries:
1123                                        if self.DEBUG:
1124                                                print 'TD: Looking for summary matching: "%s"' % summary
1125                                        sql = """SELECT id FROM ticket
1126                                                        WHERE changetime >= %s AND summary LIKE %s
1127                                                        ORDER BY changetime DESC"""
1128                                        cursor.execute(sql, [lookback, summary.strip()])
1129
1130                                        for row in cursor:
1131                                                (matched_id,) = row
1132                                                if self.DEBUG:
1133                                                        print 'TD: Found matching ticket id: %d' % matched_id
1134                                                break
1135
1136                                        if matched_id:
1137                                                matched_id = '#%d' % matched_id
1138                                                return matched_id
1139
1140                return matched_id
1141
1142
1143        def new_ticket(self, msg, subject, spam, set_fields = None):
1144                """
1145                Create a new ticket
1146                """
1147                if self.VERBOSE:
1148                        print "VB: function new_ticket()"
1149
1150                tkt = Ticket(self.env)
1151
1152                self.set_reply_fields(tkt, msg)
1153
1154                self.set_ticket_fields(tkt)
1155
1156                # Check the permission of the reporter
1157                #
1158                if self.TICKET_PERMISSION_SYSTEM:
1159                        if not self.check_permission(tkt, 'TICKET_CREATE'):
1160                                print 'Reporter: %s has no permission to create tickets' %self.author
1161                                return False
1162
1163                # Old style setting for component, will be removed
1164                #
1165                if spam:
1166                        tkt['component'] = 'Spam'
1167
1168                elif self.parameters.has_key('component'):
1169                        tkt['component'] = self.parameters['component']
1170
1171                if not msg['Subject']:
1172                        tkt['summary'] = u'(No subject)'
1173                else:
1174                        tkt['summary'] = subject
1175
1176
1177                if set_fields:
1178                        rest, keywords = string.split(set_fields, '?')
1179
1180                        if keywords:
1181                                update_fields = self.str_to_dict(keywords)
1182                                self.update_ticket_fields(tkt, update_fields)
1183
1184                # produce e-mail like header
1185                #
1186                head = ''
1187                if self.EMAIL_HEADER > 0:
1188                        head = self.email_header_txt(msg)
1189
1190                message_parts = self.get_message_parts(msg)
1191
1192                # Must we update some ticket fields properties via body_text
1193                #
1194                if self.properties:
1195                                self.update_ticket_fields(tkt, self.properties)
1196
1197                if self.DEBUG:
1198                        print 'TD: self.get_message_parts ',
1199                        print message_parts
1200
1201                message_parts = self.unique_attachment_names(message_parts)
1202                if self.DEBUG:
1203                        print 'TD: self.unique_attachment_names',
1204                        print message_parts
1205               
1206                if self.EMAIL_HEADER > 0:
1207                        message_parts.insert(0, self.email_header_txt(msg))
1208                       
1209                body_text = self.body_text(message_parts)
1210
1211                tkt['description'] = body_text
1212
1213                #when = int(time.time())
1214                #
1215                utc = UTC()
1216                when = datetime.now(utc)
1217
1218                if not self.DRY_RUN:
1219                        self.id = tkt.insert()
1220       
1221                changed = False
1222                comment = ''
1223
1224                # some routines in trac are dependend on ticket id     
1225                # like alternate notify template
1226                #
1227                if self.notify_template:
1228                        tkt['id'] = self.id
1229                        changed = True
1230
1231                ## Rewrite the description if we have mailto enabled
1232                #
1233                if self.MAILTO:
1234                        changed = True
1235                        comment = u'\nadded mailto line\n'
1236                        mailto = self.html_mailto_link( m['Subject'])
1237
1238                        tkt['description'] = u'%s\r\n%s%s\r\n' \
1239                                %(head, mailto, body_text)
1240       
1241                ## Save the attachments to the ticket   
1242                #
1243                error_with_attachments =  self.attach_attachments(message_parts)
1244
1245                if error_with_attachments:
1246                        changed = True
1247                        comment = '%s\n%s\n' %(comment, error_with_attachments)
1248
1249                if changed:
1250                        if self.DRY_RUN:
1251                                print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
1252                        else:
1253                                tkt.save_changes(tkt['reporter'], comment)
1254                                #print tkt.get_changelog(self.db, when)
1255
1256                if not spam:
1257                        self.notify(tkt, True)
1258
1259
1260        def attach_attachments(self, message_parts, update=False):
1261                '''
1262                save any attachments as files in the ticket's directory
1263                '''
1264                if self.VERBOSE:
1265                        print "VB: attach_attachments()"
1266
1267                if self.DRY_RUN:
1268                        print "DRY_RUN: no attachments attached to tickets"
1269                        return ''
1270
1271                count = 0
1272
1273                # Get Maxium attachment size
1274                #
1275                max_size = int(self.get_config('attachment', 'max_size'))
1276                status   = None
1277               
1278                for item in message_parts:
1279                        # Skip body parts
1280                        if not isinstance(item, tuple):
1281                                continue
1282                               
1283                        (original, filename, part) = item
1284                        #
1285                        # We have to determine the size so we use this temporary solution. we must escape it
1286                        # else we get UnicodeErrors.
1287                        #
1288                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, util.text.unicode_quote(filename)))
1289                        text = part.get_payload(decode=1)
1290                        if not text:
1291                                text = '(None)'
1292                        fd.write(text)
1293                        fd.close()
1294
1295                        # get the file_size
1296                        #
1297                        stats = os.lstat(path)
1298                        file_size = stats[stat.ST_SIZE]
1299
1300                        # Check if the attachment size is allowed
1301                        #
1302                        if (max_size != -1) and (file_size > max_size):
1303                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1304                                        %(status, original, file_size, max_size)
1305
1306                                os.unlink(path)
1307                                continue
1308                        else:
1309                                count = count + 1
1310                                       
1311                        # Insert the attachment
1312                        #
1313                        fd = open(path, 'rb')
1314                        if self.system == 'discussion':
1315                                att = attachment.Attachment(self.env, 'discussion', 'topic/%s'
1316                                  % (self.id,))
1317                        else:
1318                                att = attachment.Attachment(self.env, 'ticket', self.id)
1319 
1320                        # This will break the ticket_update system, the body_text is vaporized
1321                        # ;-(
1322                        #
1323                        if not update:
1324                                att.author = self.author
1325                                att.description = self.email_to_unicode('Added by email2trac')
1326
1327                        try:
1328                                att.insert(filename, fd, file_size)
1329                        except OSError, detail:
1330                                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
1331
1332                        # Remove the created temporary filename
1333                        #
1334                        fd.close()
1335                        os.unlink(path)
1336
1337                ## return error
1338                #
1339                return status
1340
1341########## Fullblog functions  #################################################
1342
1343        def blog(self, id):
1344                """
1345                The blog create/update function
1346                """
1347                # import the modules
1348                #
1349                from tracfullblog.core import FullBlogCore
1350                from tracfullblog.model import BlogPost, BlogComment
1351                from trac.test import Mock, MockPerm
1352
1353                # instantiate blog core
1354                blog = FullBlogCore(self.env)
1355                req = Mock(authname='anonymous', perm=MockPerm(), args={})
1356
1357                if id:
1358
1359                        # update blog
1360                        #
1361                        comment = BlogComment(self.env, id)
1362                        comment.author = self.author
1363
1364                        message_parts = self.get_message_parts(m)
1365                        comment.comment = self.body_text(message_parts)
1366
1367                        blog.create_comment(req, comment)
1368
1369                else:
1370                        # create blog
1371                        #
1372                        import time
1373                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
1374
1375                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
1376                       
1377                        post.author = self.author
1378                        post.title = self.email_to_unicode(m['Subject'])
1379
1380                        message_parts = self.get_message_parts(m)
1381                        post.body = self.body_text(message_parts)
1382                       
1383                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
1384
1385
1386########## Discussion functions  ##############################################
1387
1388        def discussion_topic(self, content, subject):
1389
1390                # Import modules.
1391                from tracdiscussion.api import DiscussionApi
1392                from trac.util.datefmt import to_timestamp, utc
1393
1394                if self.DEBUG:
1395                        print 'TD: Creating a new topic in forum:', self.id
1396
1397                # Get dissussion API component.
1398                api = self.env[DiscussionApi]
1399                context = self._create_context(content, subject)
1400
1401                # Get forum for new topic.
1402                forum = api.get_forum(context, self.id)
1403
1404                if not forum and self.DEBUG:
1405                        print 'ERROR: Replied forum doesn\'t exist'
1406
1407                # Prepare topic.
1408                topic = {'forum' : forum['id'],
1409                                 'subject' : context.subject,
1410                                 'time': to_timestamp(datetime.now(utc)),
1411                                 'author' : self.author,
1412                                 'subscribers' : [self.email_addr],
1413                                 'body' : self.body_text(context.content_parts)}
1414
1415                # Add topic to DB and commit it.
1416                self._add_topic(api, context, topic)
1417                self.db.commit()
1418
1419        def discussion_topic_reply(self, content, subject):
1420
1421                # Import modules.
1422                from tracdiscussion.api import DiscussionApi
1423                from trac.util.datefmt import to_timestamp, utc
1424
1425                if self.DEBUG:
1426                        print 'TD: Replying to discussion topic', self.id
1427
1428                # Get dissussion API component.
1429                api = self.env[DiscussionApi]
1430                context = self._create_context(content, subject)
1431
1432                # Get replied topic.
1433                topic = api.get_topic(context, self.id)
1434
1435                if not topic and self.DEBUG:
1436                        print 'ERROR: Replied topic doesn\'t exist'
1437
1438                # Prepare message.
1439                message = {'forum' : topic['forum'],
1440                                   'topic' : topic['id'],
1441                                   'replyto' : -1,
1442                                   'time' : to_timestamp(datetime.now(utc)),
1443                                   'author' : self.author,
1444                                   'body' : self.body_text(context.content_parts)}
1445
1446                # Add message to DB and commit it.
1447                self._add_message(api, context, message)
1448                self.db.commit()
1449
1450        def discussion_message_reply(self, content, subject):
1451
1452                # Import modules.
1453                from tracdiscussion.api import DiscussionApi
1454                from trac.util.datefmt import to_timestamp, utc
1455
1456                if self.DEBUG:
1457                        print 'TD: Replying to discussion message', self.id
1458
1459                # Get dissussion API component.
1460                api = self.env[DiscussionApi]
1461                context = self._create_context(content, subject)
1462
1463                # Get replied message.
1464                message = api.get_message(context, self.id)
1465
1466                if not message and self.DEBUG:
1467                        print 'ERROR: Replied message doesn\'t exist'
1468
1469                # Prepare message.
1470                message = {'forum' : message['forum'],
1471                                   'topic' : message['topic'],
1472                                   'replyto' : message['id'],
1473                                   'time' : to_timestamp(datetime.now(utc)),
1474                                   'author' : self.author,
1475                                   'body' : self.body_text(context.content_parts)}
1476
1477                # Add message to DB and commit it.
1478                self._add_message(api, context, message)
1479                self.db.commit()
1480
1481        def _create_context(self, content, subject):
1482
1483                # Import modules.
1484                from trac.mimeview import Context
1485                from trac.web.api import Request
1486                from trac.perm import PermissionCache
1487
1488                # TODO: Read server base URL from config.
1489                # Create request object to mockup context creation.
1490                #
1491                environ = {'SERVER_PORT' : 80,
1492                                   'SERVER_NAME' : 'test',
1493                                   'REQUEST_METHOD' : 'POST',
1494                                   'wsgi.url_scheme' : 'http',
1495                                   'wsgi.input' : sys.stdin}
1496                chrome =  {'links': {},
1497                                   'scripts': [],
1498                                   'ctxtnav': [],
1499                                   'warnings': [],
1500                                   'notices': []}
1501
1502                if self.env.base_url_for_redirect:
1503                        environ['trac.base_url'] = self.env.base_url
1504
1505                req = Request(environ, None)
1506                req.chrome = chrome
1507                req.tz = 'missing'
1508                req.authname = self.author
1509                req.perm = PermissionCache(self.env, self.author)
1510
1511                # Create and return context.
1512                context = Context.from_request(req)
1513                context.realm = 'discussion-email2trac'
1514                context.cursor = self.db.cursor()
1515                context.content = content
1516                context.subject = subject
1517
1518                # Read content parts from content.
1519                context.content_parts = self.get_message_parts(content)
1520                context.content_parts = self.unique_attachment_names(
1521                  context.content_parts)
1522
1523                return context
1524
1525        def _add_topic(self, api, context, topic):
1526                context.req.perm.assert_permission('DISCUSSION_APPEND')
1527
1528                # Filter topic.
1529                for discussion_filter in api.discussion_filters:
1530                        accept, topic_or_error = discussion_filter.filter_topic(
1531                          context, topic)
1532                        if accept:
1533                                topic = topic_or_error
1534                        else:
1535                                raise TracError(topic_or_error)
1536
1537                # Add a new topic.
1538                api.add_topic(context, topic)
1539
1540                # Get inserted topic with new ID.
1541                topic = api.get_topic_by_time(context, topic['time'])
1542
1543                # Attach attachments.
1544                self.id = topic['id']
1545                self.attach_attachments(context.content_parts, True)
1546
1547                # Notify change listeners.
1548                for listener in api.topic_change_listeners:
1549                        listener.topic_created(context, topic)
1550
1551        def _add_message(self, api, context, message):
1552                context.req.perm.assert_permission('DISCUSSION_APPEND')
1553
1554                # Filter message.
1555                for discussion_filter in api.discussion_filters:
1556                        accept, message_or_error = discussion_filter.filter_message(
1557                          context, message)
1558                        if accept:
1559                                message = message_or_error
1560                        else:
1561                                raise TracError(message_or_error)
1562
1563                # Add message.
1564                api.add_message(context, message)
1565
1566                # Get inserted message with new ID.
1567                message = api.get_message_by_time(context, message['time'])
1568
1569                # Attach attachments.
1570                self.id = message['topic']
1571                self.attach_attachments(context.content_parts, True)
1572
1573                # Notify change listeners.
1574                for listener in api.message_change_listeners:
1575                        listener.message_created(context, message)
1576
1577########## MAIN function  ######################################################
1578
1579        def parse(self, fp):
1580                """
1581                """
1582                if self.VERBOSE:
1583                        print "VB: main function parse()"
1584                global m
1585
1586                m = email.message_from_file(fp)
1587               
1588                if not m:
1589                        if self.DEBUG:
1590                                print "TD: This is not a valid email message format"
1591                        return
1592                       
1593                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1594                try:
1595                        m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1596                except AttributeError, detail:
1597                        pass
1598
1599                if self.DEBUG > 1:        # save the entire e-mail message text
1600                        self.save_email_for_debug(m, True)
1601
1602                self.db = self.env.get_db_cnx()
1603                self.get_sender_info(m)
1604
1605                if not self.email_header_acl('white_list', self.email_addr, True):
1606                        if self.DEBUG > 1 :
1607                                print 'Message rejected : %s not in white list' %(self.email_addr)
1608                        return False
1609
1610                if self.email_header_acl('black_list', self.email_addr, False):
1611                        if self.DEBUG > 1 :
1612                                print 'Message rejected : %s in black list' %(self.email_addr)
1613                        return False
1614
1615                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
1616                        if self.DEBUG > 1 :
1617                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
1618                        return False
1619
1620                # If drop the message
1621                #
1622                if self.spam(m) == 'drop':
1623                        return False
1624
1625                elif self.spam(m) == 'spam':
1626                        spam_msg = True
1627                else:
1628                        spam_msg = False
1629
1630                if not m['Subject']:
1631                        subject  = 'No Subject'
1632                else:
1633                        subject  = self.email_to_unicode(m['Subject'])
1634
1635                if self.DEBUG:
1636                         print "TD:", subject
1637
1638                #
1639                # [hic] #1529: Re: LRZ
1640                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
1641                #
1642                ticket_regex = r'''
1643                        (?P<new_fields>[#][?].*)
1644                        |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
1645                        '''
1646                # Check if  FullBlogPlugin is installed
1647                #
1648                blog_enabled = None
1649                blog_regex = ''
1650                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1651                        blog_enabled = True
1652                        blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))'''
1653
1654
1655                # Check if DiscussionPlugin is installed
1656                #
1657                discussion_enabled = None
1658                discussion_regex = ''
1659                if self.get_config('components', 'tracdiscussion.api.*') in ['enabled']:
1660                        discussion_enabled = True
1661                        discussion_regex = r'''
1662                        |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1663                        |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1664                        |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1665                        '''
1666
1667
1668                regex_str = ticket_regex + blog_regex + discussion_regex
1669                SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
1670
1671                # Find out if this is a ticket, a blog or a discussion
1672                #
1673                result =  SYSTEM_RE.search(subject)
1674
1675                if result:
1676                        # update ticket + fields
1677                        #
1678                        if result.group('reply') and self.TICKET_UPDATE:
1679                                self.system = 'ticket'
1680
1681                                # Skip the last ':' character
1682                                #
1683                                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1684                                        self.new_ticket(m, subject, spam_msg)
1685
1686                        # New ticket + fields
1687                        #
1688                        elif result.group('new_fields'):
1689                                self.system = 'ticket'
1690                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1691
1692                        if blog_enabled:
1693                                if result.group('blog'):
1694                                        self.system = 'blog'
1695                                        self.blog(result.group('blog_id'))
1696
1697                        if discussion_enabled:
1698                                # New topic.
1699                                #
1700                                if result.group('forum'):
1701                                        self.system = 'discussion'
1702                                        self.id = int(result.group('forum_id'))
1703                                        self.discussion_topic(m, subject[result.end('forum'):])
1704
1705                                # Reply to topic.
1706                                #
1707                                elif result.group('topic'):
1708                                        self.system = 'discussion'
1709                                        self.id = int(result.group('topic_id'))
1710                                        self.discussion_topic_reply(m, subject[result.end('topic'):])
1711
1712                                # Reply to topic message.
1713                                #
1714                                elif result.group('message'):
1715                                        self.system = 'discussion'
1716                                        self.id = int(result.group('message_id'))
1717                                        self.discussion_message_reply(m, subject[result.end('message'):])
1718
1719                else:
1720                        self.system = 'ticket'
1721                        result = self.ticket_update_by_subject(subject)
1722                        if result:
1723                                if not self.ticket_update(m, result, spam_msg):
1724                                        self.new_ticket(m, subject, spam_msg)
1725                        else:
1726                                # No update by subject, so just create a new ticket
1727                                self.new_ticket(m, subject, spam_msg)
1728
1729
1730########## BODY TEXT functions  ###########################################################
1731
1732        def strip_signature(self, text):
1733                """
1734                Strip signature from message, inspired by Mailman software
1735                """
1736                body = []
1737                for line in text.splitlines():
1738                        if line == '-- ':
1739                                break
1740                        body.append(line)
1741
1742                return ('\n'.join(body))
1743
1744        def reflow(self, text, delsp = 0):
1745                """
1746                Reflow the message based on the format="flowed" specification (RFC 3676)
1747                """
1748                flowedlines = []
1749                quotelevel = 0
1750                prevflowed = 0
1751
1752                for line in text.splitlines():
1753                        from re import match
1754                       
1755                        # Figure out the quote level and the content of the current line
1756                        m = match('(>*)( ?)(.*)', line)
1757                        linequotelevel = len(m.group(1))
1758                        line = m.group(3)
1759
1760                        # Determine whether this line is flowed
1761                        if line and line != '-- ' and line[-1] == ' ':
1762                                flowed = 1
1763                        else:
1764                                flowed = 0
1765
1766                        if flowed and delsp and line and line[-1] == ' ':
1767                                line = line[:-1]
1768
1769                        # If the previous line is flowed, append this line to it
1770                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1771                                flowedlines[-1] += line
1772                        # Otherwise, start a new line
1773                        else:
1774                                flowedlines.append('>' * linequotelevel + line)
1775
1776                        prevflowed = flowed
1777                       
1778
1779                return '\n'.join(flowedlines)
1780
1781        def strip_quotes(self, text):
1782                """
1783                Strip quotes from message by Nicolas Mendoza
1784                """
1785                body = []
1786                for line in text.splitlines():
1787                        if line.startswith(self.EMAIL_QUOTE):
1788                                continue
1789                        body.append(line)
1790
1791                return ('\n'.join(body))
1792
1793        def inline_properties(self, text):
1794                """
1795                Parse text if we use inline keywords to set ticket fields
1796                """
1797                if self.DEBUG:
1798                        print 'TD: inline_properties function'
1799
1800                properties = dict()
1801                body = list()
1802
1803                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1804
1805                for line in text.splitlines():
1806                        match = INLINE_EXP.match(line)
1807                        if match:
1808                                keyword, value = match.groups()
1809                                self.properties[keyword] = value.strip()
1810                                if self.DEBUG:
1811                                        print "TD: inline properties: %s : %s" %(keyword,value)
1812                        else:
1813                                body.append(line)
1814                               
1815                return '\n'.join(body)
1816
1817
1818        def wrap_text(self, text, replace_whitespace = False):
1819                """
1820                Will break a lines longer then given length into several small
1821                lines of size given length
1822                """
1823                import textwrap
1824
1825                LINESEPARATOR = '\n'
1826                reformat = ''
1827
1828                for s in text.split(LINESEPARATOR):
1829                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1830                        if tmp:
1831                                reformat = '%s\n%s' %(reformat,tmp)
1832                        else:
1833                                reformat = '%s\n' %reformat
1834
1835                return reformat
1836
1837                # Python2.4 and higher
1838                #
1839                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1840                #
1841
1842########## EMAIL attachements functions ###########################################################
1843
1844        def inline_part(self, part):
1845                """
1846                """
1847                if self.VERBOSE:
1848                        print "VB: inline_part()"
1849
1850                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1851
1852        def get_message_parts(self, msg):
1853                """
1854                parses the email message and returns a list of body parts and attachments
1855                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1856                """
1857                if self.VERBOSE:
1858                        print "VB: get_message_parts()"
1859
1860                message_parts = list()
1861       
1862                ALTERNATIVE_MULTIPART = False
1863
1864                for part in msg.walk():
1865                        if self.DEBUG:
1866                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
1867                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
1868
1869                        ## Check content type
1870                        #
1871                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
1872
1873                                if self.DEBUG:
1874                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
1875
1876                                continue
1877
1878                        ## Catch some mulitpart execptions
1879                        #
1880                        if part.get_content_type() == 'multipart/alternative':
1881                                ALTERNATIVE_MULTIPART = True
1882                                continue
1883
1884                        ## Skip multipart containers
1885                        #
1886                        if part.get_content_maintype() == 'multipart':
1887                                if self.DEBUG:
1888                                        print "TD: Skipping multipart container"
1889                                continue
1890                       
1891                        ## Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline"
1892                        #
1893                        inline = self.inline_part(part)
1894
1895                        ## Drop HTML message
1896                        #
1897                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1898                                if part.get_content_type() == 'text/html':
1899                                        if self.DEBUG:
1900                                                print "TD: Skipping alternative HTML message"
1901
1902                                        ALTERNATIVE_MULTIPART = False
1903                                        continue
1904
1905                        ## Inline text parts are where the body is
1906                        #
1907                        if part.get_content_type() == 'text/plain' and inline:
1908                                if self.DEBUG:
1909                                        print 'TD:               Inline body part'
1910
1911                                # Try to decode, if fails then do not decode
1912                                #
1913                                body_text = part.get_payload(decode=1)
1914                                if not body_text:                       
1915                                        body_text = part.get_payload(decode=0)
1916
1917                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1918                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1919
1920                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1921                                        body_text = self.reflow(body_text, delsp == 'yes')
1922       
1923                                if self.STRIP_SIGNATURE:
1924                                        body_text = self.strip_signature(body_text)
1925
1926                                if self.STRIP_QUOTES:
1927                                        body_text = self.strip_quotes(body_text)
1928
1929                                if self.INLINE_PROPERTIES:
1930                                        body_text = self.inline_properties(body_text)
1931
1932                                if self.USE_TEXTWRAP:
1933                                        body_text = self.wrap_text(body_text)
1934
1935                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
1936                                #
1937                                charset = part.get_content_charset()
1938                                if not charset:
1939                                        charset = 'iso-8859-15'
1940
1941                                try:
1942                                        ubody_text = unicode(body_text, charset)
1943
1944                                except UnicodeError, detail:
1945                                        ubody_text = unicode(body_text, 'iso-8859-15')
1946
1947                                except LookupError, detail:
1948                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1949
1950                                if self.VERBATIM_FORMAT:
1951                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1952                                else:
1953                                        message_parts.append('%s' %ubody_text)
1954                        else:
1955                                if self.DEBUG:
1956                                        s = 'TD:               Filename: %s' % part.get_filename()
1957                                        self.print_unicode(s)
1958
1959                                ##
1960                                #  First try to use email header function to convert filename.
1961                                #  If this fails the use the plan filename
1962                                try:
1963                                        filename = self.email_to_unicode(part.get_filename())
1964                                except UnicodeEncodeError, detail:
1965                                        filename = part.get_filename()
1966
1967                                message_parts.append((filename, part))
1968
1969                return message_parts
1970               
1971        def unique_attachment_names(self, message_parts):
1972                """
1973                """
1974                renamed_parts = []
1975                attachment_names = set()
1976
1977                for item in message_parts:
1978                       
1979                        ## If not an attachment, leave it alone
1980                        #
1981                        if not isinstance(item, tuple):
1982                                renamed_parts.append(item)
1983                                continue
1984                               
1985                        (filename, part) = item
1986
1987                        ## If no filename, use a default one
1988                        #
1989                        if not filename:
1990                                filename = 'untitled-part'
1991
1992                                # Guess the extension from the content type, use non strict mode
1993                                # some additional non-standard but commonly used MIME types
1994                                # are also recognized
1995                                #
1996                                ext = mimetypes.guess_extension(part.get_content_type(), False)
1997                                if not ext:
1998                                        ext = '.bin'
1999
2000                                filename = '%s%s' % (filename, ext)
2001
2002                        ## Discard relative paths for windows/unix in attachment names
2003                        #
2004                        #filename = filename.replace('\\', '/').replace(':', '/')
2005                        filename = filename.replace('\\', '_')
2006                        filename = filename.replace('/', '_')
2007
2008                        #
2009                        # We try to normalize the filename to utf-8 NFC if we can.
2010                        # Files uploaded from OS X might be in NFD.
2011                        # Check python version and then try it
2012                        #
2013                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
2014                        #       try:
2015                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
2016                        #       except TypeError:
2017                        #               pass
2018
2019                        # Make the filename unique for this ticket
2020                        num = 0
2021                        unique_filename = filename
2022                        dummy_filename, ext = os.path.splitext(filename)
2023
2024                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
2025                                num += 1
2026                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
2027                               
2028                        if self.DEBUG:
2029                                s = 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
2030                                self.print_unicode(s)
2031
2032                        attachment_names.add(unique_filename)
2033
2034                        renamed_parts.append((filename, unique_filename, part))
2035       
2036                return renamed_parts
2037                       
2038                       
2039        def attachment_exists(self, filename):
2040
2041                if self.DEBUG:
2042                        s = 'TD: attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
2043                        self.print_unicode(s)
2044
2045                # We have no valid ticket id
2046                #
2047                if not self.id:
2048                        return False
2049
2050                try:
2051                        if self.system == 'discussion':
2052                                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s'
2053                                  % (self.id,), filename)
2054                        else:
2055                                att = attachment.Attachment(self.env, 'ticket', self.id,
2056                                  filename)
2057                        return True
2058                except attachment.ResourceNotFound:
2059                        return False
2060
2061########## TRAC Ticket Text ###########################################################
2062                       
2063        def body_text(self, message_parts):
2064                body_text = []
2065               
2066                for part in message_parts:
2067                        # Plain text part, append it
2068                        if not isinstance(part, tuple):
2069                                body_text.extend(part.strip().splitlines())
2070                                body_text.append("")
2071                                continue
2072                               
2073                        (original, filename, part) = part
2074                        inline = self.inline_part(part)
2075                       
2076                        if part.get_content_maintype() == 'image' and inline:
2077                                if self.system != 'discussion':
2078                                        body_text.append('[[Image(%s)]]' % filename)
2079                                body_text.append("")
2080                        else:
2081                                if self.system != 'discussion':
2082                                        body_text.append('[attachment:"%s"]' % filename)
2083                                body_text.append("")
2084                               
2085                body_text = '\r\n'.join(body_text)
2086                return body_text
2087
2088        def html_mailto_link(self, subject):
2089                """
2090                This function returns a HTML mailto tag with the ticket id and author email address
2091                """
2092                if not self.author:
2093                        author = self.email_addr
2094                else:   
2095                        author = self.author
2096
2097                # use urllib to escape the chars
2098                #
2099                s = 'mailto:%s?Subject=%s&Cc=%s' %(
2100                       urllib.quote(self.email_addr),
2101                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
2102                           urllib.quote(self.MAILTO_CC)
2103                           )
2104
2105                s = '\r\n{{{\r\n#!html\r\n<a\r\n href="%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
2106                return s
2107
2108########## TRAC notify section ###########################################################
2109
2110        def notify(self, tkt, new=True, modtime=0):
2111                """
2112                A wrapper for the TRAC notify function. So we can use templates
2113                """
2114                if self.VERBOSE:
2115                        print "VB: notify()"
2116
2117                if self.DRY_RUN:
2118                                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
2119                                return
2120                try:
2121
2122                        #from trac.ticket.web_ui import TicketModule
2123                        #from trac.ticket.notification import TicketNotificationSystem
2124                        #ticket_sys = TicketNotificationSystem(self.env)
2125                        #a = TicketModule(self.env)
2126                        #print a.__dict__
2127                        #tn_sys = TicketNotificationSystem(self.env)
2128                        #print tn_sys
2129                        #print tn_sys.__dict__
2130                        #sys.exit(0)
2131
2132                        # create false {abs_}href properties, to trick Notify()
2133                        #
2134                        if not (self.VERSION in [0.11, 0.12]):
2135                                self.env.abs_href = Href(self.get_config('project', 'url'))
2136                                self.env.href = Href(self.get_config('project', 'url'))
2137
2138
2139                        tn = TicketNotifyEmail(self.env)
2140
2141                        if self.notify_template:
2142
2143                                if self.VERSION >= 0.11:
2144
2145                                        from trac.web.chrome import Chrome
2146
2147                                        if self.notify_template_update and not new:
2148                                                tn.template_name = self.notify_template_update
2149                                        else:
2150                                                tn.template_name = self.notify_template
2151
2152                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
2153                                               
2154                                else:
2155
2156                                        tn.template_name = self.notify_template;
2157
2158                        tn.notify(tkt, new, modtime)
2159
2160                except Exception, e:
2161                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
2162
2163
2164
2165########## Parse Config File  ###########################################################
2166
2167def ReadConfig(file, name):
2168        """
2169        Parse the config file
2170        """
2171        if not os.path.isfile(file):
2172                print 'File %s does not exist' %file
2173                sys.exit(1)
2174
2175        config = trac_config.Configuration(file)
2176
2177        # Use given project name else use defaults
2178        #
2179        if name:
2180                sections = config.sections()
2181                if not name in sections:
2182                        print "Not a valid project name: %s" %name
2183                        print "Valid names: %s" %sections
2184                        sys.exit(1)
2185
2186                project =  SaraDict()
2187                for option, value in  config.options(name):
2188                        project[option] = value
2189
2190        else:
2191                # use some trac internals to get the defaults
2192                #
2193                tmp = config.parser.defaults()
2194                project =  SaraDict()
2195
2196                for option,value in tmp.items():
2197                        project[option] = value
2198
2199        return project
2200
2201
2202if __name__ == '__main__':
2203        # Default config file
2204        #
2205        configfile = '@email2trac_conf@'
2206        project = ''
2207        component = ''
2208        ticket_prefix = 'default'
2209        dry_run = None
2210        verbose = None
2211
2212        ENABLE_SYSLOG = 0
2213
2214        SHORT_OPT = 'chf:np:t:v'
2215        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
2216
2217        try:
2218                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
2219        except getopt.error,detail:
2220                print __doc__
2221                print detail
2222                sys.exit(1)
2223       
2224        project_name = None
2225        for opt,value in opts:
2226                if opt in [ '-h', '--help']:
2227                        print __doc__
2228                        sys.exit(0)
2229                elif opt in ['-c', '--component']:
2230                        component = value
2231                elif opt in ['-f', '--file']:
2232                        configfile = value
2233                elif opt in ['-n', '--dry-run']:
2234                        dry_run = True
2235                elif opt in ['-p', '--project']:
2236                        project_name = value
2237                elif opt in ['-t', '--ticket_prefix']:
2238                        ticket_prefix = value
2239                elif opt in ['-v', '--verbose']:
2240                        verbose = True
2241       
2242        settings = ReadConfig(configfile, project_name)
2243        if not settings.has_key('project'):
2244                print __doc__
2245                print 'No Trac project is defined in the email2trac config file.'
2246                sys.exit(1)
2247       
2248        if component:
2249                settings['component'] = component
2250
2251        # The default prefix for ticket values in email2trac.conf
2252        #
2253        settings['ticket_prefix'] = ticket_prefix
2254        settings['dry_run'] = dry_run
2255        settings['verbose'] = verbose
2256
2257
2258        # Determine major trac version used to be in email2trac.conf
2259        # Quick hack for 0.12
2260        #
2261        version = '0.%s' %(trac_version.split('.')[1])
2262        if version.startswith('0.12'):
2263                version = '0.12'
2264
2265        if verbose:
2266                print "Found trac version: %s" %(version)
2267       
2268        #debug HvB
2269        #print settings
2270
2271        try:
2272                if version == '0.10':
2273                        from trac import attachment
2274                        from trac.env import Environment
2275                        from trac.ticket import Ticket
2276                        from trac.web.href import Href
2277                        from trac import util
2278                        #
2279                        # return  util.text.to_unicode(str)
2280                        #
2281                        # see http://projects.edgewall.com/trac/changeset/2799
2282                        from trac.ticket.notification import TicketNotifyEmail
2283                        from trac import config as trac_config
2284                        from trac.core import TracError
2285
2286                elif version == '0.11':
2287                        from trac import attachment
2288                        from trac.env import Environment
2289                        from trac.ticket import Ticket
2290                        from trac.web.href import Href
2291                        from trac import config as trac_config
2292                        from trac import util
2293                        from trac.core import TracError
2294                        from trac.perm import PermissionSystem
2295
2296                        #
2297                        # return  util.text.to_unicode(str)
2298                        #
2299                        # see http://projects.edgewall.com/trac/changeset/2799
2300                        from trac.ticket.notification import TicketNotifyEmail
2301
2302                elif version == '0.12':
2303                        from trac import attachment
2304                        from trac.env import Environment
2305                        from trac.ticket import Ticket
2306                        from trac.web.href import Href
2307                        from trac import config as trac_config
2308                        from trac import util
2309                        from trac.core import TracError
2310                        from trac.perm import PermissionSystem
2311
2312                        #
2313                        # return  util.text.to_unicode(str)
2314                        #
2315                        # see http://projects.edgewall.com/trac/changeset/2799
2316                        from trac.ticket.notification import TicketNotifyEmail
2317
2318
2319                else:
2320                        print 'TRAC version %s is not supported' %version
2321                        sys.exit(1)
2322                       
2323                if settings.has_key('enable_syslog'):
2324                        if SYSLOG_AVAILABLE:
2325                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
2326
2327
2328                # Must be set before environment is created
2329                #
2330                if settings.has_key('python_egg_cache'):
2331                        python_egg_cache = str(settings['python_egg_cache'])
2332                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
2333
2334       
2335                if int(settings['debug']) > 0:
2336                        print 'Loading environment', settings['project']
2337
2338                env = Environment(settings['project'], create=0)
2339
2340                tktparser = TicketEmailParser(env, settings, float(version))
2341                tktparser.parse(sys.stdin)
2342
2343        # Catch all errors ans log to SYSLOG if we have enabled this
2344        # else stdout
2345        #
2346        except Exception, error:
2347                if ENABLE_SYSLOG:
2348                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
2349
2350                        etype, evalue, etb = sys.exc_info()
2351                        for e in traceback.format_exception(etype, evalue, etb):
2352                                syslog.syslog(e)
2353
2354                        syslog.closelog()
2355                else:
2356                        traceback.print_exc()
2357
2358                if m:
2359                        tktparser.save_email_for_debug(m, True)
2360
2361
2362                sys.exit(1)
2363# EOB
Note: See TracBrowser for help on using the repository browser.