source: trunk/email2trac.py.in @ 415

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

email2trac.py.in:

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