source: trunk/email2trac.py.in @ 414

Last change on this file since 414 was 414, checked in by bas, 13 years ago

email2trac.py.in:

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