source: trunk/email2trac.py.in @ 432

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

Fixed another spellimg ;-) error set.logger. must be self.logger, closes #212

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