source: trunk/email2trac.py.in @ 406

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

converter all self.VERBOSE to self.logger.info

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