source: trunk/email2trac.py.in @ 409

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

email2trac.py.in:

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