source: trunk/email2trac.py.in @ 414

Last change on this file since 414 was 414, checked in by bas, 14 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
RevLine 
[22]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#
[80]20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
[22]22"""
[54]23email2trac.py -- Email tickets to Trac.
[22]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
[282]30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
[22]33
34The scripts reads emails from stdin and inserts directly into a Trac database.
35
36How to use
37----------
[282]38 * See https://subtrac.sara.nl/oss/email2trac/
39
[22]40 * Create an config file:
[282]41    [DEFAULT]                        # REQUIRED
42    project      : /data/trac/test   # REQUIRED
43    debug        : 1                 # OPTIONAL, if set print some DEBUG info
[22]44
[282]45    [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
46    project      : /data/trac/jouvin # use -p|--project jouvin. 
[22]47       
48 * default config file is : /etc/email2trac.conf
49
50 * Commandline opions:
[205]51                -h,--help
52                -f,--file  <configuration file>
[410]53                -n,--dry-run
[205]54                -p, --project <project name>
[410]55                -t, --ticket_prefix <name>
[22]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
[136]67import email.Iterators
68import email.Header
[22]69import re
70import urllib
71import unicodedata
72from stat import *
73import mimetypes
[96]74import traceback
[403]75import logging
76import logging.handlers
[404]77import UserDict
[411]78from datetime import tzinfo, timedelta, datetime
[22]79
[403]80
[363]81from trac import __version__ as trac_version
[199]82from trac import config as trac_config
[91]83
[96]84# Some global variables
85#
[276]86trac_default_version = '0.11'
[96]87m = None
[22]88
[182]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
[404]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)
[182]125
[404]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
[409]132                from the dictionary
[404]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
[22]152class TicketEmailParser(object):
153        env = None
154        comment = '> '
[359]155
[410]156        def __init__(self, env, parameters, logger, version):
[22]157                self.env = env
158
159                # Database connection
160                #
161                self.db = None
162
[206]163                # Save parameters
164                #
165                self.parameters = parameters
[410]166                self.logger = logger
[206]167
[72]168                # Some useful mail constants
169                #
[287]170                self.email_name = None
[72]171                self.email_addr = None
[183]172                self.email_from = None
[288]173                self.author     = None
[287]174                self.id         = None
[294]175               
176                self.STRIP_CONTENT_TYPES = list()
[72]177
[22]178                self.VERSION = version
[402]179
[404]180                self.get_config = self.env.config.get
181
[410]182                ## init function ##
183                #
[402]184                self.setup_parameters()
185
[404]186        def setup_parameters(self):
[402]187                if self.parameters.has_key('umask'):
188                        os.umask(int(self.parameters['umask'], 8))
189
[22]190
[402]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']
[74]195                        else:
196                                self.MAILTO_CC = ''
[22]197                else:
198                        self.MAILTO = 0
199
[402]200                if self.parameters.has_key('spam_level'):
201                        self.SPAM_LEVEL = int(self.parameters['spam_level'])
[22]202                else:
203                        self.SPAM_LEVEL = 0
204
[402]205                if self.parameters.has_key('spam_header'):
206                        self.SPAM_HEADER = self.parameters['spam_header']
[207]207                else:
208                        self.SPAM_HEADER = 'X-Spam-Score'
209
[402]210                if self.parameters.has_key('email_quote'):
211                        self.EMAIL_QUOTE = str(self.parameters['email_quote'])
[191]212                else:   
213                        self.EMAIL_QUOTE = '> '
[22]214
[402]215                if self.parameters.has_key('email_header'):
216                        self.EMAIL_HEADER = int(self.parameters['email_header'])
[22]217                else:
218                        self.EMAIL_HEADER = 0
219
[402]220                if self.parameters.has_key('alternate_notify_template'):
221                        self.notify_template = str(self.parameters['alternate_notify_template'])
[42]222                else:
223                        self.notify_template = None
[22]224
[402]225                if self.parameters.has_key('alternate_notify_template_update'):
226                        self.notify_template_update = str(self.parameters['alternate_notify_template_update'])
[222]227                else:
228                        self.notify_template_update = None
229
[402]230                if self.parameters.has_key('reply_all'):
231                        self.REPLY_ALL = int(self.parameters['reply_all'])
[43]232                else:
233                        self.REPLY_ALL = 0
[42]234
[402]235                if self.parameters.has_key('ticket_permission_system'):
236                        self.TICKET_PERMISSION_SYSTEM = str(self.parameters['ticket_permission_system'])
[397]237                else:
238                        self.TICKET_PERMISSION_SYSTEM = None
239
[402]240                if self.parameters.has_key('ticket_update'):
241                        self.TICKET_UPDATE = int(self.parameters['ticket_update'])
[74]242                else:
243                        self.TICKET_UPDATE = 0
[43]244
[402]245                if self.parameters.has_key('ticket_update_by_subject'):
246                        self.TICKET_UPDATE_BY_SUBJECT = int(self.parameters['ticket_update_by_subject'])
[353]247                else:
248                        self.TICKET_UPDATE_BY_SUBJECT = 0
249
[402]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'])
[360]252                else:
253                        self.TICKET_UPDATE_BY_SUBJECT_LOOKBACK = 30
254
[402]255                if self.parameters.has_key('drop_spam'):
256                        self.DROP_SPAM = int(self.parameters['drop_spam'])
[118]257                else:
258                        self.DROP_SPAM = 0
[74]259
[402]260                if self.parameters.has_key('verbatim_format'):
261                        self.VERBATIM_FORMAT = int(self.parameters['verbatim_format'])
[134]262                else:
263                        self.VERBATIM_FORMAT = 1
[118]264
[402]265                if self.parameters.has_key('reflow'):
266                        self.REFLOW = int(self.parameters['reflow'])
[231]267                else:
268                        self.REFLOW = 1
269
[402]270                if self.parameters.has_key('drop_alternative_html_version'):
271                        self.DROP_ALTERNATIVE_HTML_VERSION = int(self.parameters['drop_alternative_html_version'])
[278]272                else:
273                        self.DROP_ALTERNATIVE_HTML_VERSION = 0
274
[402]275                if self.parameters.has_key('strip_signature'):
276                        self.STRIP_SIGNATURE = int(self.parameters['strip_signature'])
[136]277                else:
278                        self.STRIP_SIGNATURE = 0
[134]279
[402]280                if self.parameters.has_key('strip_quotes'):
281                        self.STRIP_QUOTES = int(self.parameters['strip_quotes'])
[191]282                else:
283                        self.STRIP_QUOTES = 0
284
[309]285                self.properties = dict()
[402]286                if self.parameters.has_key('inline_properties'):
287                        self.INLINE_PROPERTIES = int(self.parameters['inline_properties'])
[309]288                else:
289                        self.INLINE_PROPERTIES = 0
290
[402]291                if self.parameters.has_key('use_textwrap'):
292                        self.USE_TEXTWRAP = int(self.parameters['use_textwrap'])
[148]293                else:
294                        self.USE_TEXTWRAP = 0
295
[402]296                if self.parameters.has_key('binhex'):
[294]297                        self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
[238]298
[402]299                if self.parameters.has_key('applesingle'):
[294]300                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]301
[402]302                if self.parameters.has_key('appledouble'):
[294]303                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]304
[402]305                if self.parameters.has_key('strip_content_types'):
306                        items = self.parameters['strip_content_types'].split(',')
[294]307                        for item in items:
308                                self.STRIP_CONTENT_TYPES.append(item.strip())
309
[257]310                self.WORKFLOW = None
[402]311                if self.parameters.has_key('workflow'):
312                        self.WORKFLOW = self.parameters['workflow']
[257]313
[173]314                # Use OS independend functions
315                #
316                self.TMPDIR = os.path.normcase('/tmp')
[402]317                if self.parameters.has_key('tmpdir'):
318                        self.TMPDIR = os.path.normcase(str(self.parameters['tmpdir']))
[173]319
[402]320                if self.parameters.has_key('ignore_trac_user_settings'):
321                        self.IGNORE_TRAC_USER_SETTINGS = int(self.parameters['ignore_trac_user_settings'])
[194]322                else:
323                        self.IGNORE_TRAC_USER_SETTINGS = 0
[191]324
[402]325                if self.parameters.has_key('email_triggers_workflow'):
326                        self.EMAIL_TRIGGERS_WORKFLOW = int(self.parameters['email_triggers_workflow'])
[320]327                else:
328                        self.EMAIL_TRIGGERS_WORKFLOW = 1
329
[402]330                if self.parameters.has_key('subject_field_separator'):
331                        self.SUBJECT_FIELD_SEPARATOR = self.parameters['subject_field_separator'].strip()
[297]332                else:
333                        self.SUBJECT_FIELD_SEPARATOR = '&'
334
[305]335                self.trac_smtp_from = self.get_config('notification', 'smtp_from')
336
[359]337                self.system = None
338
[341]339########## Email Header Functions ###########################################################
[339]340
[22]341        def spam(self, message):
[191]342                """
343                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
344                # Note if Spam_level then '*' are included
345                """
[194]346                spam = False
[207]347                if message.has_key(self.SPAM_HEADER):
348                        spam_l = string.split(message[self.SPAM_HEADER])
[22]349
[207]350                        try:
351                                number = spam_l[0].count('*')
352                        except IndexError, detail:
353                                number = 0
354                               
[22]355                        if number >= self.SPAM_LEVEL:
[194]356                                spam = True
357                               
[191]358                # treat virus mails as spam
359                #
360                elif message.has_key('X-Virus-found'):                 
[194]361                        spam = True
362
363                # How to handle SPAM messages
364                #
365                if self.DROP_SPAM and spam:
[408]366                        if self.parameters.debug > 2 :
367                                set.logger.debug('Message is a SPAM. Automatic ticket insertion refused (SPAM level > %d)' % self.SPAM_LEVEL)
[194]368
[204]369                        return 'drop'   
[194]370
371                elif spam:
372
[204]373                        return 'Spam'   
[67]374
[194]375                else:
[22]376
[204]377                        return False
[191]378
[221]379        def email_header_acl(self, keyword, header_field, default):
[206]380                """
[221]381                This function wil check if the email address is allowed or denied
382                to send mail to the ticket list
383            """
[409]384                self.logger.debug('function email_header_acl: %s' %keyword)
385
[206]386                try:
[221]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
[206]394                except KeyError, detail:
[408]395                        if self.parameters.debug > 2 :
396                                self.logger.debug('%s not defined, all messages are allowed.' %(keyword))
[206]397
[221]398                        return default
[206]399
[221]400                mail_addresses = string.split(mail_addresses, ',')
401
402                for entry in mail_addresses:
[209]403                        entry = entry.strip()
[221]404                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
405                        result =  TO_RE.search(header_field)
[208]406                        if result:
407                                return True
[149]408
[208]409                return False
410
[22]411        def email_header_txt(self, m):
[72]412                """
413                Display To and CC addresses in description field
414                """
[288]415                s = ''
[334]416
[213]417                if m['To'] and len(m['To']) > 0:
[288]418                        s = "'''To:''' %s\r\n" %(m['To'])
[22]419                if m['Cc'] and len(m['Cc']) > 0:
[288]420                        s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
[22]421
[288]422                return  self.email_to_unicode(s)
[22]423
[138]424
[194]425        def get_sender_info(self, message):
[45]426                """
[72]427                Get the default author name and email address from the message
[226]428                """
[43]429
[226]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
[194]433                self.email_from = self.email_to_unicode(message['from'])
[287]434                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]435
[304]436                ## Trac can not handle author's name that contains spaces
437                #  and forbid the ticket email address as author field
[194]438
[305]439                if self.email_addr == self.trac_smtp_from:
[395]440                        if self.email_name:
441                                self.author = self.email_name
442                        else:
443                                self.author = "email2trac"
[304]444                else:
445                        self.author = self.email_addr
446
[194]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
[45]455                #
[194]456                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
[250]457                        if e and (e.lower() == self.email_addr.lower()) ]
[43]458
[45]459                if len(users) == 1:
[194]460                        self.email_from = users[0][0]
[250]461                        self.author = users[0][0]
[45]462
[72]463        def set_reply_fields(self, ticket, message):
464                """
465                Set all the right fields for a new ticket
466                """
[409]467                self.logger.debug('function set_reply_fields')
[72]468
[270]469                ## Only use name or email adress
470                #ticket['reporter'] = self.email_from
471                ticket['reporter'] = self.author
472
473
[45]474                # Put all CC-addresses in ticket CC field
[43]475                #
476                if self.REPLY_ALL:
477
[299]478                        email_cc = ''
479
480                        cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
481
482                        if not cc_addrs:
[105]483                                return
[43]484
[299]485                        ## Build a list of forbidden CC addresses
486                        #
[300]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)
[299]491                               
[392]492                        # Always Remove reporter email address from cc-list
[43]493                        #
[392]494                        try:
495                                cc_addrs.remove((self.author, self.email_addr))
496                        except ValueError, detail:
497                                pass
[43]498
[299]499                        for name,addr in cc_addrs:
500               
501                                ## Prevent mail loop
502                                #
[300]503                                #if addr in to_list:
[304]504
505                                if addr == self.trac_smtp_from:
[408]506                                        if self.parameters.debug:
507                                                self.logger.debug("Skipping %s mail address for CC-field" %(addr))
[299]508                                        continue
[43]509
[299]510                                if email_cc:
511                                        email_cc = '%s, %s' %(email_cc, addr)
512                                else:
513                                        email_cc = addr
[96]514
[299]515                        if email_cc:
[408]516                                if self.parameters.debug:
517                                                self.logger.debug('set_reply_fields: %s' %email_cc)
[299]518
519                                ticket['cc'] = self.email_to_unicode(email_cc)
520
[339]521
522########## DEBUG functions  ###########################################################
523
[310]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
[407]531                if self.parameters.dry_run:
[331]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')
[310]537                if not message_body:
[331]538                                message_body = '(None)'
[310]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):
[317]551                """
552                """
[409]553                self.logger.debug('function debug_attachments')
[317]554               
[310]555                n = 0
[331]556                for item in message_parts:
[310]557                        # Skip inline text parts
[331]558                        if not isinstance(item, tuple):
[310]559                                continue
560                               
[331]561                        (original, filename, part) = item
[310]562
563                        n = n + 1
564                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
[379]565               
566                        s = 'TD: part%d: filename: %s' %(n, filename)
567                        self.print_unicode(s)
568       
[348]569                        ## Forbidden chars
570                        #
571                        filename = filename.replace('\\', '_')
572                        filename = filename.replace('/', '_')
573       
574
575                        part_file = os.path.join(self.TMPDIR, filename)
[379]576                        s = 'TD: writing part%d (%s)' % (n,part_file)
577                        self.print_unicode(s)
[331]578
[407]579                        if self.parameters.dry_run:
[331]580                                print 'DRY_RUN: NOT saving attachments'
581                                continue
582
[377]583                        part_file = util.text.unicode_quote(part_file)
584
[310]585                        fx = open(part_file, 'wb')
586                        text = part.get_payload(decode=1)
[331]587
[310]588                        if not text:
589                                text = '(None)'
[331]590
[310]591                        fx.write(text)
592                        fx.close()
[331]593
[310]594                        try:
595                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
596                        except OSError:
597                                pass
598
[96]599        def save_email_for_debug(self, message, tempfile=False):
[309]600
[96]601                if tempfile:
602                        import tempfile
603                        msg_file = tempfile.mktemp('.email2trac')
604                else:
[173]605                        #msg_file = '/var/tmp/msg.txt'
606                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
607
[407]608                if self.parameters.dry_run:
[331]609                        print 'DRY_RUN: NOT saving email message to %s' %(msg_file)
610                else:
611                        print 'TD: saving email to %s' %(msg_file)
[44]612
[331]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
[309]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
[339]628########## Conversion functions  ###########################################################
629
[341]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                """
[409]636                self.logger.debug("function email_to_unicode")
[341]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
[288]666        def str_to_dict(self, s):
[164]667                """
[288]668                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
[164]669                """
[409]670                self.logger.debug("function str_to_dict")
[164]671
[297]672                fields = string.split(s, self.SUBJECT_FIELD_SEPARATOR)
[262]673
[164]674                result = dict()
675                for field in fields:
676                        try:
[262]677                                index, value = string.split(field, '=')
[169]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
[164]685                                if value:
[165]686                                        result[index.lower()] = value
[169]687
[164]688                        except ValueError:
689                                pass
[165]690                return result
[167]691
[379]692        def print_unicode(self,s):
693                """
694                This function prints unicode strings uif possible else it will quote it
695                """
696                try:
[408]697                        self.logger.debug(s)
[379]698                except UnicodeEncodeError, detail:
[408]699                        self.logger.debug(util.text.unicode_quote(s))
[379]700
[339]701########## TRAC ticket functions  ###########################################################
702
[398]703        def check_permission_participants(self, tkt):
[397]704                """
[398]705                Check if the mailer is allowed to update the ticket
706                """
[409]707                self.logger.debug('function check_permission_participants')
[398]708
709                if tkt['reporter'].lower() in [self.author, self.email_addr]:
[408]710                        if self.parameters.debug:
[412]711                                self.logger.debug('ALLOW, %s is the ticket reporter' %(self.email_addr))
[408]712
[398]713                        return True
714
715                perm = PermissionSystem(self.env)
716                if perm.check_permission('TICKET_MODIFY', self.author):
[408]717                        if self.parameters.debug:
718                                self.logger.debug('ALLOW, %s has trac permission to update the ticket' %(self.author))
719
[398]720                        return True
[408]721
[398]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
[408]732                                        if self.parameters.debug:
733                                                self.logger.debug('ALLOW, %s is in the CC' %(self.email_addr))
[398]734
735                                        return True
736
737                except KeyError:
738                        return False
739
740        def check_permission(self, tkt, action):
741                """
[397]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                """
[409]751                self.logger.debug("function check_permission")
[397]752
753                if self.TICKET_PERMISSION_SYSTEM in ['trac']:
[398]754
[397]755                        perm = PermissionSystem(self.env)
756                        if perm.check_permission(action, self.author):
757                                return True
758                        else:
759                                return False
[398]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
[397]768                else:
769                                return True
770
771
[202]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:
[169]777                        - If the field is known
[202]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)
[169]782                """
[409]783                self.logger.debug("function update_ticket_fields")
[169]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:
[167]790                        try:
[169]791                                sys_dict[field['name']] = field['options']
792
[167]793                        except KeyError:
[169]794                                sys_dict[field['name']] = None
[167]795                                pass
[169]796
[301]797                ## Check user supplied fields an compare them with the
[169]798                # system one's
799                #
800                for field,value in user_dict.items():
[408]801                        if self.parameters.debug:
[379]802                                s = 'TD: user_field\t %s = %s' %(field,value)
803                                self.print_unicode(s)
[169]804
[301]805                        ## To prevent mail loop
806                        #
807                        if field == 'cc':
808
809                                cc_list = user_dict['cc'].split(',')
810
[304]811                                if self.trac_smtp_from in cc_list:
[408]812                                        if self.parameters.debug:
813                                                self.logger.debug('TD: MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from))
814
[304]815                                        cc_list.remove(self.trac_smtp_from)
[301]816
817                                value = ','.join(cc_list)
818                               
819
[169]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
[202]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) )
[169]833
834                                except TypeError:
[345]835                                        pass
836
837                                ## Only set if we have a value
838                                #
839                                if value:
[169]840                                        ticket[field] = value
[202]841
[408]842                                if self.parameters.debug:
[379]843                                        s = 'ticket_field\t %s = %s' %(field,  ticket[field])
844                                        self.print_unicode(s)
[345]845
[260]846        def ticket_update(self, m, id, spam):
[78]847                """
[79]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.
[78]851                """
[409]852                self.logger.debug("function ticket_update")
[202]853
[164]854                # Must we update ticket fields
855                #
[220]856                update_fields = dict()
[165]857                try:
[260]858                        id, keywords = string.split(id, '?')
[262]859
[220]860                        update_fields = self.str_to_dict(keywords)
[165]861
862                        # Strip '#'
863                        #
[260]864                        self.id = int(id[1:])
[165]865
[260]866                except ValueError:
[390]867
[362]868                        # Strip '#'
[165]869                        #
[362]870                        self.id = int(id[1:])
[164]871
[409]872                self.logger.debug("function ticket_update id %s" %id)
[71]873
[194]874                # When is the change committed
875                #
[386]876                if self.VERSION < 0.11:
877                        when = int(time.time())
878                else:
[194]879                        utc = UTC()
880                        when = datetime.now(utc)
[77]881
[172]882                try:
[253]883                        tkt = Ticket(self.env, self.id, self.db)
[390]884
[172]885                except util.TracError, detail:
[390]886
[253]887                        # Not a valid ticket
[390]888
[253]889                        self.id = None
[172]890                        return False
[126]891
[398]892                # Check the permission of the reporter
893                #
894                if self.TICKET_PERMISSION_SYSTEM:
895                        if not self.check_permission(tkt, 'TICKET_MODIFY'):
[409]896                                self.logger.info('Reporter: %s has no permission to modify tickets' %self.author)
[398]897                                return False
898
[288]899                # How many changes has this ticket
900                cnum = len(tkt.get_changelog())
901
902
[220]903                # reopen the ticket if it is was closed
904                # We must use the ticket workflow framework
905                #
[320]906                if tkt['status'] in ['closed'] and self.EMAIL_TRIGGERS_WORKFLOW:
[220]907
[257]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
[388]922                        if self.WORKFLOW and (self.VERSION >= 0.11 ) :
[257]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
[408]931                                if self.parameters.debug:
932                                        self.logger.debug('Workflow ticket update fields: ')
[257]933
934                                for key in fields.keys():
[408]935                                        if self.parameters.debug:
936                                                self.logger.debug('\t %s : %s' %(key, fields[key]))
937
[257]938                                        tkt[key] = fields[key]
939
940                        else:
941                                tkt['status'] = 'reopened'
942                                tkt['resolution'] = ''
943
[309]944                # Must we update some ticket fields properties via subjectline
[172]945                #
[220]946                if update_fields:
947                        self.update_ticket_fields(tkt, update_fields)
[166]948
[236]949                message_parts = self.get_message_parts(m)
[253]950                message_parts = self.unique_attachment_names(message_parts)
[210]951
[309]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
[177]957                if self.EMAIL_HEADER:
[236]958                        message_parts.insert(0, self.email_header_txt(m))
[76]959
[236]960                body_text = self.body_text(message_parts)
961
[401]962                error_with_attachments = self.attach_attachments(message_parts)
[348]963
[309]964                if body_text.strip() or update_fields or self.properties:
[407]965                        if self.parameters.dry_run:
[288]966                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
[250]967                        else:
[348]968                                if error_with_attachments:
969                                        body_text = '%s\\%s' %(error_with_attachments, body_text)
970                               
[288]971                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
972                       
[219]973
[392]974                if not spam:
[253]975                        self.notify(tkt, False, when)
[72]976
[71]977                return True
978
[202]979        def set_ticket_fields(self, ticket):
[77]980                """
[202]981                set the ticket fields to value specified
982                        - /etc/email2trac.conf with <prefix>_<field>
983                        - trac default values, trac.ini
984                """
[410]985                self.logger.debug('function set_ticket_fields')
986
[202]987                user_dict = dict()
988
989                for field in ticket.fields:
990
991                        name = field['name']
992
[335]993                        ## default trac value
[202]994                        #
[233]995                        if not field.get('custom'):
996                                value = self.get_config('ticket', 'default_%s' %(name) )
997                        else:
[345]998                                ##  Else we get the default value for reporter
999                                #
[233]1000                                value = field.get('value')
1001                                options = field.get('options')
[345]1002
[335]1003                                if value and options and (value not in options):
[345]1004                                         value = options[int(value)]
1005       
[408]1006                        if self.parameters.debug:
[379]1007                                s = 'TD: trac.ini name %s = %s' %(name, value)
1008                                self.print_unicode(s)
[202]1009
[335]1010                        ## email2trac.conf settings
1011                        #
[206]1012                        prefix = self.parameters['ticket_prefix']
[202]1013                        try:
[206]1014                                value = self.parameters['%s_%s' %(prefix, name)]
[408]1015                                if self.parameters.debug > 10:
[379]1016                                        s = 'TD: email2trac.conf %s = %s ' %(name, value)
1017                                        self.print_unicode(s)
[202]1018
1019                        except KeyError, detail:
1020                                pass
1021               
[408]1022                        if self.parameters.debug:
[379]1023                                s = 'TD: user_dict[%s] = %s' %(name, value)
1024                                self.print_unicode(s)
[202]1025
[345]1026                        if value:
1027                                user_dict[name] = value
[202]1028
1029                self.update_ticket_fields(ticket, user_dict, use_default=1)
1030
[352]1031                if 'status' not in user_dict.keys():
1032                        ticket['status'] = 'new'
[202]1033
1034
[356]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>
[202]1041
[356]1042                So we must have the last column
1043                """
[409]1044                self.logger.debug('function ticket_update_by_subject')
[356]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
[408]1056                                if self.parameters.debug:
1057                                        self.logger.debug('subject search string: %s' %(orig_subject))
[356]1058
1059                                cursor = self.db.cursor()
1060                                summaries = [orig_subject, '%%: %s' % orig_subject]
1061
[360]1062                                ##
1063                                # Convert days to seconds
1064                                lookback = int(time.mktime(time.gmtime())) - \
1065                                                self.TICKET_UPDATE_BY_SUBJECT_LOOKBACK * 24 * 3600
[356]1066
[360]1067
[356]1068                                for summary in summaries:
[408]1069                                        if self.parameters.debug:
1070                                                self.logger.debug('Looking for summary matching: "%s"' % summary)
1071
[356]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
[408]1079
1080                                                if self.parameters.debug:
1081                                                        self.logger.debug('Found matching ticket id: %d' % matched_id)
1082
[356]1083                                                break
1084
1085                                        if matched_id:
[366]1086                                                matched_id = '#%d' % matched_id
[356]1087                                                return matched_id
1088
1089                return matched_id
1090
1091
[262]1092        def new_ticket(self, msg, subject, spam, set_fields = None):
[202]1093                """
[77]1094                Create a new ticket
1095                """
[409]1096                self.logger.debug('function new_ticket')
[250]1097
[41]1098                tkt = Ticket(self.env)
[326]1099
1100                self.set_reply_fields(tkt, msg)
1101
[202]1102                self.set_ticket_fields(tkt)
1103
[397]1104                # Check the permission of the reporter
1105                #
1106                if self.TICKET_PERMISSION_SYSTEM:
[398]1107                        if not self.check_permission(tkt, 'TICKET_CREATE'):
[409]1108                                self.logger.info('Reporter: %s has no permission to create tickets' %self.author)
[397]1109                                return False
1110
[202]1111                # Old style setting for component, will be removed
1112                #
[204]1113                if spam:
1114                        tkt['component'] = 'Spam'
1115
[206]1116                elif self.parameters.has_key('component'):
1117                        tkt['component'] = self.parameters['component']
[201]1118
[22]1119                if not msg['Subject']:
[151]1120                        tkt['summary'] = u'(No subject)'
[22]1121                else:
[264]1122                        tkt['summary'] = subject
[22]1123
1124
[262]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
[45]1132                # produce e-mail like header
1133                #
[22]1134                head = ''
1135                if self.EMAIL_HEADER > 0:
1136                        head = self.email_header_txt(msg)
[296]1137
[236]1138                message_parts = self.get_message_parts(msg)
[309]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
[236]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)
[45]1151
[236]1152                tkt['description'] = body_text
[90]1153
[182]1154                #when = int(time.time())
[192]1155                #
[182]1156                utc = UTC()
1157                when = datetime.now(utc)
[45]1158
[408]1159                if self.parameters.dry_run:
1160                        print 'DRY_RUN: tkt.insert()'
1161                else:
[253]1162                        self.id = tkt.insert()
[273]1163       
[90]1164                changed = False
1165                comment = ''
[77]1166
[273]1167                # some routines in trac are dependend on ticket id     
1168                # like alternate notify template
1169                #
1170                if self.notify_template:
[274]1171                        tkt['id'] = self.id
[273]1172                        changed = True
1173
[295]1174                ## Rewrite the description if we have mailto enabled
[45]1175                #
[72]1176                if self.MAILTO:
[100]1177                        changed = True
[142]1178                        comment = u'\nadded mailto line\n'
[343]1179                        mailto = self.html_mailto_link( m['Subject'])
[253]1180
[213]1181                        tkt['description'] = u'%s\r\n%s%s\r\n' \
[142]1182                                %(head, mailto, body_text)
[295]1183       
1184                ## Save the attachments to the ticket   
1185                #
[340]1186                error_with_attachments =  self.attach_attachments(message_parts)
[295]1187
[319]1188                if error_with_attachments:
1189                        changed = True
1190                        comment = '%s\n%s\n' %(comment, error_with_attachments)
[45]1191
[90]1192                if changed:
[408]1193                        if self.parameters.dry_run:
[344]1194                                print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
[201]1195                        else:
[344]1196                                tkt.save_changes(tkt['reporter'], comment)
[201]1197                                #print tkt.get_changelog(self.db, when)
[90]1198
[392]1199                if not spam:
[253]1200                        self.notify(tkt, True)
[45]1201
[260]1202
[342]1203        def attach_attachments(self, message_parts, update=False):
1204                '''
1205                save any attachments as files in the ticket's directory
1206                '''
[409]1207                self.logger.debug('function attach_attachments()')
[342]1208
[407]1209                if self.parameters.dry_run:
[342]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                        #
[377]1227                        # We have to determine the size so we use this temporary solution. we must escape it
1228                        # else we get UnicodeErrors.
[342]1229                        #
[377]1230                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, util.text.unicode_quote(filename)))
[342]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')
[359]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 
[342]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
[348]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)
[342]1273
1274                        # Remove the created temporary filename
1275                        #
1276                        fd.close()
1277                        os.unlink(path)
1278
1279                ## return error
1280                #
1281                return status
1282
[359]1283########## Fullblog functions  #################################################
[339]1284
[260]1285        def blog(self, id):
1286                """
1287                The blog create/update function
1288                """
1289                # import the modules
1290                #
1291                from tracfullblog.core import FullBlogCore
[312]1292                from tracfullblog.model import BlogPost, BlogComment
1293                from trac.test import Mock, MockPerm
[260]1294
1295                # instantiate blog core
1296                blog = FullBlogCore(self.env)
[312]1297                req = Mock(authname='anonymous', perm=MockPerm(), args={})
1298
[260]1299                if id:
1300
1301                        # update blog
1302                        #
[268]1303                        comment = BlogComment(self.env, id)
[260]1304                        comment.author = self.author
[312]1305
1306                        message_parts = self.get_message_parts(m)
1307                        comment.comment = self.body_text(message_parts)
1308
[260]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'])
[312]1321
1322                        message_parts = self.get_message_parts(m)
1323                        post.body = self.body_text(message_parts)
[260]1324                       
1325                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
1326
1327
[359]1328########## Discussion functions  ##############################################
[342]1329
[359]1330        def discussion_topic(self, content, subject):
[342]1331
[359]1332                # Import modules.
1333                from tracdiscussion.api import DiscussionApi
1334                from trac.util.datefmt import to_timestamp, utc
1335
[408]1336                if self.parameters.debug:
1337                        self.logger.debug('Creating a new topic in forum:', self.id)
[359]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
[408]1346                if not forum and self.parameters.debug:
1347                        self.logger.debug("ERROR: Replied forum doesn't exist")
[359]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
[408]1367                if self.parameters.debug:
1368                        self.logger.debug('Replying to discussion topic', self.id)
[359]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
[408]1377                if not topic and self.parameters.debug:
1378                        self.logger.debug("ERROR: Replied topic doesn't exist")
[359]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
[408]1398                if self.parameters.debug:
1399                        self.loggger.debug('TD: Replying to discussion message', self.id)
[359]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
[408]1408                if not message and self.parameters.debug:
1409                        self.logger.debug("ERROR: Replied message doesn't exist")
[359]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']
[401]1487                self.attach_attachments(context.content_parts, True)
[359]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']
[401]1513                self.attach_attachments(context.content_parts, True)
[359]1514
1515                # Notify change listeners.
1516                for listener in api.message_change_listeners:
1517                        listener.message_created(context, message)
1518
1519########## MAIN function  ######################################################
1520
[77]1521        def parse(self, fp):
[356]1522                """
1523                """
[409]1524                self.logger.debug('Main function parse')
[96]1525                global m
1526
[77]1527                m = email.message_from_file(fp)
[239]1528               
[77]1529                if not m:
[408]1530                        if self.parameters.debug:
1531                                self.logger.debug('This is not a valid email message format')
1532
[77]1533                        return
[239]1534                       
1535                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
[316]1536                try:
1537                        m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1538                except AttributeError, detail:
1539                        pass
[239]1540
[408]1541                if self.parameters.debug:         # save the entire e-mail message text
[219]1542                        self.save_email_for_debug(m, True)
[77]1543
1544                self.db = self.env.get_db_cnx()
[194]1545                self.get_sender_info(m)
[152]1546
[221]1547                if not self.email_header_acl('white_list', self.email_addr, True):
[408]1548                        if self.parameters.debug:
1549                                self.logger.debug('Message rejected : %s not in white list' %(self.email_addr))
1550
[221]1551                        return False
[77]1552
[221]1553                if self.email_header_acl('black_list', self.email_addr, False):
[408]1554                        if self.parameters.debug:
1555                                self.logger.debug('Message rejected : %s in black list' %(self.email_addr))
1556
[221]1557                        return False
1558
[227]1559                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
[408]1560                        if self.parameters.debug:
1561                                self.logger.debug('Message rejected : %s not in recipient list' %(self.to_email_addr))
1562
[226]1563                        return False
1564
[204]1565                # If drop the message
[194]1566                #
[204]1567                if self.spam(m) == 'drop':
[194]1568                        return False
1569
[204]1570                elif self.spam(m) == 'spam':
1571                        spam_msg = True
1572                else:
1573                        spam_msg = False
1574
[359]1575                if not m['Subject']:
1576                        subject  = 'No Subject'
1577                else:
1578                        subject  = self.email_to_unicode(m['Subject'])
[304]1579
[408]1580                if self.parameters.debug:
1581                        self.logger.debug('subject: %s' %subject)
[359]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>[#][?].*)
[390]1589                        |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
[359]1590                        '''
[260]1591                # Check if  FullBlogPlugin is installed
[77]1592                #
[260]1593                blog_enabled = None
[359]1594                blog_regex = ''
[260]1595                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1596                        blog_enabled = True
[359]1597                        blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))'''
[329]1598
[77]1599
[359]1600                # Check if DiscussionPlugin is installed
[260]1601                #
[359]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                        '''
[77]1611
[359]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
[265]1617                #
[359]1618                result =  SYSTEM_RE.search(subject)
[390]1619
[260]1620                if result:
1621                        # update ticket + fields
1622                        #
[359]1623                        if result.group('reply') and self.TICKET_UPDATE:
1624                                self.system = 'ticket'
[260]1625
[390]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
[262]1631                        # New ticket + fields
1632                        #
1633                        elif result.group('new_fields'):
[359]1634                                self.system = 'ticket'
[262]1635                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1636
[359]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
[260]1664                else:
[359]1665                        self.system = 'ticket'
[356]1666                        result = self.ticket_update_by_subject(subject)
1667                        if result:
[390]1668                                if not self.ticket_update(m, result, spam_msg):
1669                                        self.new_ticket(m, subject, spam_msg)
[353]1670                        else:
1671                                # No update by subject, so just create a new ticket
1672                                self.new_ticket(m, subject, spam_msg)
1673
[356]1674
[343]1675########## BODY TEXT functions  ###########################################################
1676
[136]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
[231]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
[191]1726        def strip_quotes(self, text):
[193]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)
[151]1735
[193]1736                return ('\n'.join(body))
[191]1737
[309]1738        def inline_properties(self, text):
1739                """
1740                Parse text if we use inline keywords to set ticket fields
1741                """
[409]1742                self.logger.debug('function inline_properties')
[309]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()
[408]1754
1755                                if self.parameters.debug:
1756                                        self.logger.debug('inline properties: %s : %s' %(keyword,value))
[309]1757                        else:
1758                                body.append(line)
1759                               
1760                return '\n'.join(body)
1761
1762
[154]1763        def wrap_text(self, text, replace_whitespace = False):
[151]1764                """
[191]1765                Will break a lines longer then given length into several small
1766                lines of size given length
[151]1767                """
1768                import textwrap
[154]1769
[151]1770                LINESEPARATOR = '\n'
[153]1771                reformat = ''
[151]1772
[154]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
[153]1779
1780                return reformat
1781
[154]1782                # Python2.4 and higher
1783                #
1784                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1785                #
1786
[343]1787########## EMAIL attachements functions ###########################################################
1788
[340]1789        def inline_part(self, part):
1790                """
1791                """
[409]1792                self.logger.debug('function inline_part()')
[154]1793
[340]1794                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1795
[236]1796        def get_message_parts(self, msg):
[45]1797                """
[236]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)
[45]1800                """
[409]1801                self.logger.debug('function get_message_parts()')
[317]1802
[309]1803                message_parts = list()
[294]1804       
[278]1805                ALTERNATIVE_MULTIPART = False
1806
[22]1807                for part in msg.walk():
[408]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())
[278]1811
1812                        ## Check content type
[294]1813                        #
1814                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
[238]1815
[408]1816                                if self.parameters.debug:
1817                                        self.logger.debug("A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename()))
[238]1818
1819                                continue
1820
[294]1821                        ## Catch some mulitpart execptions
1822                        #
1823                        if part.get_content_type() == 'multipart/alternative':
[278]1824                                ALTERNATIVE_MULTIPART = True
1825                                continue
1826
[294]1827                        ## Skip multipart containers
[278]1828                        #
[45]1829                        if part.get_content_maintype() == 'multipart':
[408]1830                                if self.parameters.debug:
1831                                        self.logger.debug("Skipping multipart container")
[22]1832                                continue
[278]1833                       
[294]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                        #
[236]1836                        inline = self.inline_part(part)
1837
[294]1838                        ## Drop HTML message
1839                        #
[278]1840                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1841                                if part.get_content_type() == 'text/html':
[408]1842                                        if self.parameters.debug:
1843                                                self.logger.debug('Skipping alternative HTML message')
[278]1844
1845                                        ALTERNATIVE_MULTIPART = False
1846                                        continue
1847
[294]1848                        ## Inline text parts are where the body is
1849                        #
[236]1850                        if part.get_content_type() == 'text/plain' and inline:
[408]1851                                if self.parameters.debug:
1852                                        self.logger.debug('               Inline body part')
[236]1853
[45]1854                                # Try to decode, if fails then do not decode
1855                                #
[90]1856                                body_text = part.get_payload(decode=1)
[45]1857                                if not body_text:                       
[90]1858                                        body_text = part.get_payload(decode=0)
[231]1859
[232]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()
[231]1862
1863                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1864                                        body_text = self.reflow(body_text, delsp == 'yes')
[154]1865       
[136]1866                                if self.STRIP_SIGNATURE:
1867                                        body_text = self.strip_signature(body_text)
[22]1868
[191]1869                                if self.STRIP_QUOTES:
1870                                        body_text = self.strip_quotes(body_text)
1871
[309]1872                                if self.INLINE_PROPERTIES:
1873                                        body_text = self.inline_properties(body_text)
1874
[148]1875                                if self.USE_TEXTWRAP:
[151]1876                                        body_text = self.wrap_text(body_text)
[148]1877
[294]1878                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
[45]1879                                #
[100]1880                                charset = part.get_content_charset()
[102]1881                                if not charset:
1882                                        charset = 'iso-8859-15'
1883
[89]1884                                try:
[96]1885                                        ubody_text = unicode(body_text, charset)
[100]1886
1887                                except UnicodeError, detail:
[96]1888                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]1889
[100]1890                                except LookupError, detail:
[139]1891                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1892
[236]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:
[408]1898                                if self.parameters.debug:
[379]1899                                        s = 'TD:               Filename: %s' % part.get_filename()
1900                                        self.print_unicode(s)
[22]1901
[383]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
[317]1910                                message_parts.append((filename, part))
[236]1911
1912                return message_parts
1913               
[253]1914        def unique_attachment_names(self, message_parts):
[296]1915                """
1916                """
[236]1917                renamed_parts = []
1918                attachment_names = set()
[296]1919
[331]1920                for item in message_parts:
[236]1921                       
[296]1922                        ## If not an attachment, leave it alone
1923                        #
[331]1924                        if not isinstance(item, tuple):
1925                                renamed_parts.append(item)
[236]1926                                continue
1927                               
[331]1928                        (filename, part) = item
[295]1929
[296]1930                        ## If no filename, use a default one
1931                        #
1932                        if not filename:
[236]1933                                filename = 'untitled-part'
[22]1934
[242]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)
[236]1940                                if not ext:
1941                                        ext = '.bin'
[22]1942
[236]1943                                filename = '%s%s' % (filename, ext)
[22]1944
[348]1945                        ## Discard relative paths for windows/unix in attachment names
[296]1946                        #
[348]1947                        #filename = filename.replace('\\', '/').replace(':', '/')
1948                        filename = filename.replace('\\', '_')
1949                        filename = filename.replace('/', '_')
[347]1950
[296]1951                        #
[236]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                        #
[348]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
[100]1961
[236]1962                        # Make the filename unique for this ticket
1963                        num = 0
1964                        unique_filename = filename
[296]1965                        dummy_filename, ext = os.path.splitext(filename)
[134]1966
[339]1967                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
[236]1968                                num += 1
[296]1969                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
[236]1970                               
[408]1971                        if self.parameters.debug:
[379]1972                                s = 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1973                                self.print_unicode(s)
[100]1974
[236]1975                        attachment_names.add(unique_filename)
1976
1977                        renamed_parts.append((filename, unique_filename, part))
[296]1978       
[236]1979                return renamed_parts
1980                       
1981                       
[253]1982        def attachment_exists(self, filename):
[250]1983
[408]1984                if self.parameters.debug:
[379]1985                        s = 'TD: attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
1986                        self.print_unicode(s)
[250]1987
1988                # We have no valid ticket id
1989                #
[253]1990                if not self.id:
[236]1991                        return False
[250]1992
[236]1993                try:
[359]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)
[236]2000                        return True
[250]2001                except attachment.ResourceNotFound:
[236]2002                        return False
[343]2003
2004########## TRAC Ticket Text ###########################################################
[236]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:
[359]2020                                if self.system != 'discussion':
2021                                        body_text.append('[[Image(%s)]]' % filename)
[236]2022                                body_text.append("")
2023                        else:
[359]2024                                if self.system != 'discussion':
2025                                        body_text.append('[attachment:"%s"]' % filename)
[236]2026                                body_text.append("")
2027                               
2028                body_text = '\r\n'.join(body_text)
2029                return body_text
2030
[343]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
[253]2053        def notify(self, tkt, new=True, modtime=0):
[79]2054                """
2055                A wrapper for the TRAC notify function. So we can use templates
2056                """
[409]2057                self.logger.debug('function notify()')
[392]2058
[407]2059                if self.parameters.dry_run:
[344]2060                                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
[250]2061                                return
[41]2062                try:
[392]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
[41]2074                        # create false {abs_}href properties, to trick Notify()
2075                        #
[369]2076                        if not (self.VERSION in [0.11, 0.12]):
[192]2077                                self.env.abs_href = Href(self.get_config('project', 'url'))
2078                                self.env.href = Href(self.get_config('project', 'url'))
[22]2079
[392]2080
[41]2081                        tn = TicketNotifyEmail(self.env)
[213]2082
[42]2083                        if self.notify_template:
[222]2084
[388]2085                                if self.VERSION >= 0.11:
[222]2086
[221]2087                                        from trac.web.chrome import Chrome
[222]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
[221]2094                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
2095                                               
2096                                else:
[222]2097
[221]2098                                        tn.template_name = self.notify_template;
[42]2099
[77]2100                        tn.notify(tkt, new, modtime)
[41]2101
2102                except Exception, e:
[410]2103                        set.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
[41]2104
[22]2105
[74]2106
[343]2107########## Parse Config File  ###########################################################
[22]2108
2109def ReadConfig(file, name):
2110        """
2111        Parse the config file
2112        """
2113        if not os.path.isfile(file):
[79]2114                print 'File %s does not exist' %file
[22]2115                sys.exit(1)
2116
[199]2117        config = trac_config.Configuration(file)
[22]2118
2119        # Use given project name else use defaults
2120        #
2121        if name:
[199]2122                sections = config.sections()
2123                if not name in sections:
[79]2124                        print "Not a valid project name: %s" %name
[199]2125                        print "Valid names: %s" %sections
[22]2126                        sys.exit(1)
2127
[404]2128                project =  SaraDict()
[199]2129                for option, value in  config.options(name):
2130                        project[option] = value
[22]2131
2132        else:
[270]2133                # use some trac internals to get the defaults
[217]2134                #
[404]2135                tmp = config.parser.defaults()
2136                project =  SaraDict()
[22]2137
[404]2138                for option,value in tmp.items():
2139                        project[option] = value
2140
[410]2141        ## Convert debug value to int
2142        #
2143        if project.debug:
2144                project.debug = int(project.debug)
2145        else:
2146                project.debug = 0
2147
[22]2148        return project
2149
[410]2150########## Setup Logging ###############################################################
[87]2151
[414]2152def setup_log(parameters, interactive=None):
[410]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
[414]2162        if interactive:
2163                parameters.log_type = 'stderr'
2164
[410]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
[22]2216if __name__ == '__main__':
2217        # Default config file
2218        #
[24]2219        configfile = '@email2trac_conf@'
[22]2220        project = ''
2221        component = ''
[202]2222        ticket_prefix = 'default'
[204]2223        dry_run = None
[317]2224        verbose = None
[414]2225        debug_interactive = None
[202]2226
[412]2227        SHORT_OPT = 'cdhf:np:t:v'
2228        LONG_OPT  =  ['component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
[201]2229
[22]2230        try:
[201]2231                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
[22]2232        except getopt.error,detail:
2233                print __doc__
2234                print detail
2235                sys.exit(1)
[87]2236       
[22]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
[412]2244                elif opt in ['-d', '--debug']:
[414]2245                        debug_interactive = 1
[22]2246                elif opt in ['-f', '--file']:
2247                        configfile = value
[201]2248                elif opt in ['-n', '--dry-run']:
[204]2249                        dry_run = True
[22]2250                elif opt in ['-p', '--project']:
2251                        project_name = value
[202]2252                elif opt in ['-t', '--ticket_prefix']:
2253                        ticket_prefix = value
[388]2254                elif opt in ['-v', '--verbose']:
[317]2255                        verbose = True
[87]2256       
[22]2257        settings = ReadConfig(configfile, project_name)
[410]2258
2259        # The default prefix for ticket values in email2trac.conf
2260        #
[412]2261        settings.ticket_prefix = ticket_prefix
2262        settings.dry_run = dry_run
2263        settings.verbose = verbose
[410]2264
[414]2265        if not settings.debug and debug_interactive:
2266                settings.debug = debug_interactive
[412]2267
[414]2268        logger = setup_log(settings, debug_interactive)
[410]2269
[22]2270        if not settings.has_key('project'):
2271                print __doc__
[79]2272                print 'No Trac project is defined in the email2trac config file.'
[22]2273                sys.exit(1)
[87]2274       
[22]2275        if component:
2276                settings['component'] = component
[202]2277
[363]2278        # Determine major trac version used to be in email2trac.conf
[373]2279        # Quick hack for 0.12
[363]2280        #
2281        version = '0.%s' %(trac_version.split('.')[1])
[373]2282        if version.startswith('0.12'):
2283                version = '0.12'
2284
[410]2285        logger.debug("Found trac version: %s" %(version))
[363]2286       
[22]2287        #debug HvB
2288        #print settings
[189]2289
[87]2290        try:
[401]2291                if version == '0.10':
[87]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
[139]2297                        #
2298                        # return  util.text.to_unicode(str)
2299                        #
[87]2300                        # see http://projects.edgewall.com/trac/changeset/2799
2301                        from trac.ticket.notification import TicketNotifyEmail
[199]2302                        from trac import config as trac_config
[359]2303                        from trac.core import TracError
2304
[189]2305                elif version == '0.11':
[182]2306                        from trac import attachment
2307                        from trac.env import Environment
2308                        from trac.ticket import Ticket
2309                        from trac.web.href import Href
[199]2310                        from trac import config as trac_config
[182]2311                        from trac import util
[359]2312                        from trac.core import TracError
[397]2313                        from trac.perm import PermissionSystem
[260]2314
[368]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
[260]2320
[368]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
[397]2329                        from trac.perm import PermissionSystem
[368]2330
[182]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
[368]2336
2337
[189]2338                else:
[410]2339                        logger.error('TRAC version %s is not supported' %version)
[189]2340                        sys.exit(1)
[182]2341
[291]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
[410]2348                if settings.debug > 0:
2349                        logger.debug('Loading environment %s', settings.project)
[359]2350
[87]2351                env = Environment(settings['project'], create=0)
[333]2352
[410]2353                tktparser = TicketEmailParser(env, settings, logger, float(version))
[87]2354                tktparser.parse(sys.stdin)
[22]2355
[87]2356        # Catch all errors ans log to SYSLOG if we have enabled this
2357        # else stdout
2358        #
2359        except Exception, error:
[187]2360
[411]2361                etype, evalue, etb = sys.exc_info()
2362                for e in traceback.format_exception(etype, evalue, etb):
2363                        logger.critical(e)
[187]2364
[97]2365                if m:
[98]2366                        tktparser.save_email_for_debug(m, True)
[97]2367
[249]2368                sys.exit(1)
[22]2369# EOB
Note: See TracBrowser for help on using the repository browser.