source: trunk/email2trac.py.in @ 436

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

ported EMAIL_QUOTE to new UserDict?

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