source: trunk/email2trac.py.in @ 407

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

changed self.DRY_RUN to self.parameters.dry_run

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