source: trunk/email2trac.py.in @ 294

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

email2trac.py.in:

  • added new configuration parameter, strip_content_types thanks to Otto. eg:

strip_content_types: application/pgp-signature, application/mac-binhex40

Will skip pgp signature attachments and apple binhex40 attachments, closes #68

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 39.6 KB
RevLine 
[22]1#!@PYTHON@
2# Copyright (C) 2002
3#
4# This file is part of the email2trac utils
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
19#
[80]20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
[22]22"""
[54]23email2trac.py -- Email tickets to Trac.
[22]24
25A simple MTA filter to create Trac tickets from inbound emails.
26
27Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28Copyright 2005, Edgewall Software
29
[282]30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
[22]33
34The scripts reads emails from stdin and inserts directly into a Trac database.
35
36How to use
37----------
[282]38 * See https://subtrac.sara.nl/oss/email2trac/
39
[22]40 * Create an config file:
[282]41    [DEFAULT]                        # REQUIRED
42    project      : /data/trac/test   # REQUIRED
43    debug        : 1                 # OPTIONAL, if set print some DEBUG info
44    trac_version : 0.9               # OPTIONAL, default is 0.11
[22]45
[282]46    [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
47    project      : /data/trac/jouvin # use -p|--project jouvin. 
[22]48       
49 * default config file is : /etc/email2trac.conf
50
51 * Commandline opions:
[205]52                -h,--help
53                -f,--file  <configuration file>
54                -n,--dry-run
55                -p, --project <project name>
56                -t, --ticket_prefix <name>
[22]57
58SVN Info:
59        $Id: email2trac.py.in 294 2010-01-06 12:18:48Z 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
[22]76
[190]77
78# Will fail where unavailable, e.g. Windows
79#
80try:
81    import syslog
82    SYSLOG_AVAILABLE = True
83except ImportError:
84    SYSLOG_AVAILABLE = False
85
[182]86from datetime import tzinfo, timedelta, datetime
[199]87from trac import config as trac_config
[91]88
[96]89# Some global variables
90#
[276]91trac_default_version = '0.11'
[96]92m = None
[22]93
[182]94# A UTC class needed for trac version 0.11, added by
95# tbaschak at ktc dot mb dot ca
96#
97class UTC(tzinfo):
98        """UTC"""
99        ZERO = timedelta(0)
100        HOUR = timedelta(hours=1)
101       
102        def utcoffset(self, dt):
103                return self.ZERO
104               
105        def tzname(self, dt):
106                return "UTC"
107               
108        def dst(self, dt):
109                return self.ZERO
110
111
[22]112class TicketEmailParser(object):
113        env = None
114        comment = '> '
115   
[206]116        def __init__(self, env, parameters, version):
[22]117                self.env = env
118
119                # Database connection
120                #
121                self.db = None
122
[206]123                # Save parameters
124                #
125                self.parameters = parameters
126
[72]127                # Some useful mail constants
128                #
[287]129                self.email_name = None
[72]130                self.email_addr = None
[183]131                self.email_from = None
[288]132                self.author     = None
[287]133                self.id         = None
[294]134               
135                self.STRIP_CONTENT_TYPES = list()
[72]136
[22]137                self.VERSION = version
[206]138                self.DRY_RUN = parameters['dry_run']
[204]139
[172]140                self.get_config = self.env.config.get
[22]141
142                if parameters.has_key('umask'):
143                        os.umask(int(parameters['umask'], 8))
144
[236]145                if parameters.has_key('quote_attachment_filenames'):
146                        self.QUOTE_ATTACHMENT_FILENAMES = int(parameters['quote_attachment_filenames'])
147                else:
148                        self.QUOTE_ATTACHMENT_FILENAMES = 1
149
[22]150                if parameters.has_key('debug'):
151                        self.DEBUG = int(parameters['debug'])
152                else:
153                        self.DEBUG = 0
154
155                if parameters.has_key('mailto_link'):
156                        self.MAILTO = int(parameters['mailto_link'])
[74]157                        if parameters.has_key('mailto_cc'):
158                                self.MAILTO_CC = parameters['mailto_cc']
159                        else:
160                                self.MAILTO_CC = ''
[22]161                else:
162                        self.MAILTO = 0
163
164                if parameters.has_key('spam_level'):
165                        self.SPAM_LEVEL = int(parameters['spam_level'])
166                else:
167                        self.SPAM_LEVEL = 0
168
[207]169                if parameters.has_key('spam_header'):
170                        self.SPAM_HEADER = parameters['spam_header']
171                else:
172                        self.SPAM_HEADER = 'X-Spam-Score'
173
[191]174                if parameters.has_key('email_quote'):
175                        self.EMAIL_QUOTE = str(parameters['email_quote'])
176                else:   
177                        self.EMAIL_QUOTE = '> '
[22]178
179                if parameters.has_key('email_header'):
180                        self.EMAIL_HEADER = int(parameters['email_header'])
181                else:
182                        self.EMAIL_HEADER = 0
183
[42]184                if parameters.has_key('alternate_notify_template'):
185                        self.notify_template = str(parameters['alternate_notify_template'])
186                else:
187                        self.notify_template = None
[22]188
[222]189                if parameters.has_key('alternate_notify_template_update'):
190                        self.notify_template_update = str(parameters['alternate_notify_template_update'])
191                else:
192                        self.notify_template_update = None
193
[43]194                if parameters.has_key('reply_all'):
195                        self.REPLY_ALL = int(parameters['reply_all'])
196                else:
197                        self.REPLY_ALL = 0
[42]198
[74]199                if parameters.has_key('ticket_update'):
200                        self.TICKET_UPDATE = int(parameters['ticket_update'])
201                else:
202                        self.TICKET_UPDATE = 0
[43]203
[118]204                if parameters.has_key('drop_spam'):
205                        self.DROP_SPAM = int(parameters['drop_spam'])
206                else:
207                        self.DROP_SPAM = 0
[74]208
[134]209                if parameters.has_key('verbatim_format'):
210                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
211                else:
212                        self.VERBATIM_FORMAT = 1
[118]213
[231]214                if parameters.has_key('reflow'):
[256]215                        self.REFLOW = int(parameters['reflow'])
[231]216                else:
217                        self.REFLOW = 1
218
[278]219                if parameters.has_key('drop_alternative_html_version'):
220                        self.DROP_ALTERNATIVE_HTML_VERSION = int(parameters['drop_alternative_html_version'])
221                else:
222                        self.DROP_ALTERNATIVE_HTML_VERSION = 0
223
[136]224                if parameters.has_key('strip_signature'):
225                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
226                else:
227                        self.STRIP_SIGNATURE = 0
[134]228
[191]229                if parameters.has_key('strip_quotes'):
230                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
231                else:
232                        self.STRIP_QUOTES = 0
233
[148]234                if parameters.has_key('use_textwrap'):
235                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
236                else:
237                        self.USE_TEXTWRAP = 0
238
[238]239                if parameters.has_key('binhex'):
[294]240                        self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
[238]241
242                if parameters.has_key('applesingle'):
[294]243                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]244
245                if parameters.has_key('appledouble'):
[294]246                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]247
[294]248                if parameters.has_key('strip_content_types'):
249                        items = parameters['strip_content_types'].split(',')
250                        for item in items:
251                                self.STRIP_CONTENT_TYPES.append(item.strip())
252
[257]253                self.WORKFLOW = None
254                if parameters.has_key('workflow'):
255                        self.WORKFLOW = parameters['workflow']
256
[173]257                # Use OS independend functions
258                #
259                self.TMPDIR = os.path.normcase('/tmp')
260                if parameters.has_key('tmpdir'):
261                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
262
[194]263                if parameters.has_key('ignore_trac_user_settings'):
264                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
265                else:
266                        self.IGNORE_TRAC_USER_SETTINGS = 0
[191]267
[22]268        def spam(self, message):
[191]269                """
270                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
271                # Note if Spam_level then '*' are included
272                """
[194]273                spam = False
[207]274                if message.has_key(self.SPAM_HEADER):
275                        spam_l = string.split(message[self.SPAM_HEADER])
[22]276
[207]277                        try:
278                                number = spam_l[0].count('*')
279                        except IndexError, detail:
280                                number = 0
281                               
[22]282                        if number >= self.SPAM_LEVEL:
[194]283                                spam = True
284                               
[191]285                # treat virus mails as spam
286                #
287                elif message.has_key('X-Virus-found'):                 
[194]288                        spam = True
289
290                # How to handle SPAM messages
291                #
292                if self.DROP_SPAM and spam:
293                        if self.DEBUG > 2 :
294                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
295
[204]296                        return 'drop'   
[194]297
298                elif spam:
299
[204]300                        return 'Spam'   
[67]301
[194]302                else:
[22]303
[204]304                        return False
[191]305
[221]306        def email_header_acl(self, keyword, header_field, default):
[206]307                """
[221]308                This function wil check if the email address is allowed or denied
309                to send mail to the ticket list
310            """
[206]311                try:
[221]312                        mail_addresses = self.parameters[keyword]
313
314                        # Check if we have an empty string
315                        #
316                        if not mail_addresses:
317                                return default
318
[206]319                except KeyError, detail:
[221]320                        if self.DEBUG > 2 :
[250]321                                print 'TD: %s not defined, all messages are allowed.' %(keyword)
[206]322
[221]323                        return default
[206]324
[221]325                mail_addresses = string.split(mail_addresses, ',')
326
327                for entry in mail_addresses:
[209]328                        entry = entry.strip()
[221]329                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
330                        result =  TO_RE.search(header_field)
[208]331                        if result:
332                                return True
[149]333
[208]334                return False
335
[139]336        def email_to_unicode(self, message_str):
[22]337                """
338                Email has 7 bit ASCII code, convert it to unicode with the charset
[79]339        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
[22]340                understands it.
341                """
[139]342                results =  email.Header.decode_header(message_str)
[288]343                s = None
[22]344                for text,format in results:
345                        if format:
346                                try:
347                                        temp = unicode(text, format)
[139]348                                except UnicodeError, detail:
[22]349                                        # This always works
350                                        #
351                                        temp = unicode(text, 'iso-8859-15')
[139]352                                except LookupError, detail:
353                                        #text = 'ERROR: Could not find charset: %s, please install' %format
354                                        #temp = unicode(text, 'iso-8859-15')
355                                        temp = message_str
356                                       
[22]357                        else:
358                                temp = string.strip(text)
[92]359                                temp = unicode(text, 'iso-8859-15')
[22]360
[288]361                        if s:
362                                s = '%s %s' %(s, temp)
[22]363                        else:
[288]364                                s = '%s' %temp
[22]365
[288]366                #s = s.encode('utf-8')
367                return s
[22]368
[236]369        def debug_body(self, message_body, tempfile=False):
370                if tempfile:
371                        import tempfile
372                        body_file = tempfile.mktemp('.email2trac')
373                else:
374                        body_file = os.path.join(self.TMPDIR, 'body.txt')
375
376                print 'TD: writing body (%s)' % body_file
377                fx = open(body_file, 'wb')
378                if not message_body:
379                        message_body = '(None)'
[274]380
381                message_body = message_body.encode('utf-8')
382                #message_body = unicode(message_body, 'iso-8859-15')
383
[236]384                fx.write(message_body)
385                fx.close()
386                try:
387                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
388                except OSError:
389                        pass
390
391        def debug_attachments(self, message_parts):
[22]392                n = 0
[236]393                for part in message_parts:
394                        # Skip inline text parts
395                        if not isinstance(part, tuple):
[22]396                                continue
[236]397                               
[237]398                        (original, filename, part) = part
[22]399
400                        n = n + 1
401                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
402                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
403
[236]404                        part_file = os.path.join(self.TMPDIR, filename)
[173]405                        #part_file = '/var/tmp/part%d' % n
[22]406                        print 'TD: writing part%d (%s)' % (n,part_file)
407                        fx = open(part_file, 'wb')
408                        text = part.get_payload(decode=1)
409                        if not text:
410                                text = '(None)'
411                        fx.write(text)
412                        fx.close()
413                        try:
414                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
415                        except OSError:
416                                pass
417
418        def email_header_txt(self, m):
[72]419                """
420                Display To and CC addresses in description field
421                """
[288]422                s = ''
[213]423                #if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
424                if m['To'] and len(m['To']) > 0:
[288]425                        s = "'''To:''' %s\r\n" %(m['To'])
[22]426                if m['Cc'] and len(m['Cc']) > 0:
[288]427                        s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
[22]428
[288]429                return  self.email_to_unicode(s)
[22]430
[138]431
[194]432        def get_sender_info(self, message):
[45]433                """
[72]434                Get the default author name and email address from the message
[226]435                """
[43]436
[226]437                self.email_to = self.email_to_unicode(message['to'])
438                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
439
[194]440                self.email_from = self.email_to_unicode(message['from'])
[287]441                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]442
[252]443                # Trac can not handle author's name that contains spaces
444                #
445                self.author = self.email_addr
[194]446
447                if self.IGNORE_TRAC_USER_SETTINGS:
448                        return
449
450                # Is this a registered user, use email address as search key:
451                # result:
452                #   u : login name
453                #   n : Name that the user has set in the settings tab
454                #   e : email address that the user has set in the settings tab
[45]455                #
[194]456                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
[250]457                        if e and (e.lower() == self.email_addr.lower()) ]
[43]458
[45]459                if len(users) == 1:
[194]460                        self.email_from = users[0][0]
[250]461                        self.author = users[0][0]
[45]462
[72]463        def set_reply_fields(self, ticket, message):
464                """
465                Set all the right fields for a new ticket
466                """
467
[270]468                ## Only use name or email adress
469                #ticket['reporter'] = self.email_from
470                ticket['reporter'] = self.author
471
472
[45]473                # Put all CC-addresses in ticket CC field
[43]474                #
475                if self.REPLY_ALL:
[45]476                        #tos = message.get_all('to', [])
[43]477                        ccs = message.get_all('cc', [])
478
[45]479                        addrs = email.Utils.getaddresses(ccs)
[105]480                        if not addrs:
481                                return
[43]482
483                        # Remove reporter email address if notification is
484                        # on
485                        #
486                        if self.notification:
487                                try:
[72]488                                        addrs.remove((self.author, self.email_addr))
[43]489                                except ValueError, detail:
490                                        pass
491
[45]492                        for name,mail in addrs:
[105]493                                try:
[108]494                                        mail_list = '%s, %s' %(mail_list, mail)
[230]495                                except UnboundLocalError, detail:
[105]496                                        mail_list = mail
[43]497
[105]498                        if mail_list:
[139]499                                ticket['cc'] = self.email_to_unicode(mail_list)
[96]500
501        def save_email_for_debug(self, message, tempfile=False):
502                if tempfile:
503                        import tempfile
504                        msg_file = tempfile.mktemp('.email2trac')
505                else:
[173]506                        #msg_file = '/var/tmp/msg.txt'
507                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
508
[44]509                print 'TD: saving email to %s' % msg_file
510                fx = open(msg_file, 'wb')
511                fx.write('%s' % message)
512                fx.close()
513                try:
514                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
515                except OSError:
516                        pass
517
[288]518        def str_to_dict(self, s):
[164]519                """
[288]520                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
[164]521                """
522
[288]523                fields = string.split(s, ',')
[262]524
[164]525                result = dict()
526                for field in fields:
527                        try:
[262]528                                index, value = string.split(field, '=')
[169]529
530                                # We can not change the description of a ticket via the subject
531                                # line. The description is the body of the email
532                                #
533                                if index.lower() in ['description']:
534                                        continue
535
[164]536                                if value:
[165]537                                        result[index.lower()] = value
[169]538
[164]539                        except ValueError:
540                                pass
541
[165]542                return result
[167]543
[202]544        def update_ticket_fields(self, ticket, user_dict, use_default=None):
545                """
546                This will update the ticket fields. It will check if the
547                given fields are known and if the right values are specified
548                It will only update the ticket field value:
[169]549                        - If the field is known
[202]550                        - If the value supplied is valid for the ticket field.
551                          If not then there are two options:
552                           1) Skip the value (use_default=None)
553                           2) Set default value for field (use_default=1)
[169]554                """
555
556                # Build a system dictionary from the ticket fields
557                # with field as index and option as value
558                #
559                sys_dict = dict()
560                for field in ticket.fields:
[167]561                        try:
[169]562                                sys_dict[field['name']] = field['options']
563
[167]564                        except KeyError:
[169]565                                sys_dict[field['name']] = None
[167]566                                pass
[169]567
568                # Check user supplied fields an compare them with the
569                # system one's
570                #
571                for field,value in user_dict.items():
[202]572                        if self.DEBUG >= 10:
573                                print  'user_field\t %s = %s' %(field,value)
[169]574
575                        if sys_dict.has_key(field):
576
577                                # Check if value is an allowed system option, if TypeError then
578                                # every value is allowed
579                                #
580                                try:
581                                        if value in sys_dict[field]:
582                                                ticket[field] = value
[202]583                                        else:
584                                                # Must we set a default if value is not allowed
585                                                #
586                                                if use_default:
587                                                        value = self.get_config('ticket', 'default_%s' %(field) )
588                                                        ticket[field] = value
[169]589
590                                except TypeError:
591                                        ticket[field] = value
[202]592
593                                if self.DEBUG >= 10:
594                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
[169]595                                       
[260]596        def ticket_update(self, m, id, spam):
[78]597                """
[79]598                If the current email is a reply to an existing ticket, this function
599                will append the contents of this email to that ticket, instead of
600                creating a new one.
[78]601                """
[250]602                if self.DEBUG:
[260]603                        print "TD: ticket_update: %s" %id
[202]604
[164]605                # Must we update ticket fields
606                #
[220]607                update_fields = dict()
[165]608                try:
[260]609                        id, keywords = string.split(id, '?')
[262]610
611                        # Skip the last ':' character
612                        #
613                        keywords = keywords[:-1]
[220]614                        update_fields = self.str_to_dict(keywords)
[165]615
616                        # Strip '#'
617                        #
[260]618                        self.id = int(id[1:])
[165]619
[260]620                except ValueError:
[165]621                        # Strip '#' and ':'
622                        #
[260]623                        self.id = int(id[1:-1])
[164]624
[71]625
[194]626                # When is the change committed
627                #
628                if self.VERSION == 0.11:
629                        utc = UTC()
630                        when = datetime.now(utc)
631                else:
632                        when = int(time.time())
[77]633
[172]634                try:
[253]635                        tkt = Ticket(self.env, self.id, self.db)
[172]636                except util.TracError, detail:
[253]637                        # Not a valid ticket
638                        self.id = None
[172]639                        return False
[126]640
[288]641                # How many changes has this ticket
642                cnum = len(tkt.get_changelog())
643
644
[220]645                # reopen the ticket if it is was closed
646                # We must use the ticket workflow framework
647                #
648                if tkt['status'] in ['closed']:
649
[257]650                        #print controller.actions['reopen']
651                        #
652                        # As reference 
653                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
654                        #
655                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
656                        #print 'controller : ', a
657                        #
658                        #b = controller.get_all_status()
659                        #print 'get all status: ', b
660                        #
661                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
662                        #print 'get_ticket_changes :', b
663
[288]664                        if self.WORKFLOW and (self.VERSION in [0.11]) :
[257]665                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
666                                from trac.test import Mock, MockPerm
667
668                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
669
670                                controller = ConfigurableTicketWorkflow(self.env)
671                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
672
673                                if self.DEBUG:
674                                        print 'TD: Workflow ticket update fields: ', fields
675
676                                for key in fields.keys():
677                                        tkt[key] = fields[key]
678
679                        else:
680                                tkt['status'] = 'reopened'
681                                tkt['resolution'] = ''
682
[172]683                # Must we update some ticket fields properties
684                #
[220]685                if update_fields:
686                        self.update_ticket_fields(tkt, update_fields)
[166]687
[236]688                message_parts = self.get_message_parts(m)
[253]689                message_parts = self.unique_attachment_names(message_parts)
[210]690
[177]691                if self.EMAIL_HEADER:
[236]692                        message_parts.insert(0, self.email_header_txt(m))
[76]693
[236]694                body_text = self.body_text(message_parts)
695
[241]696                if body_text.strip() or update_fields:
[250]697                        if self.DRY_RUN:
[288]698                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
[250]699                        else:
[288]700                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
701                       
[219]702
[129]703                if self.VERSION  == 0.9:
[288]704                        s = self.attachments(message_parts, True)
[129]705                else:
[288]706                        s = self.attachments(message_parts)
[76]707
[204]708                if self.notification and not spam:
[253]709                        self.notify(tkt, False, when)
[72]710
[71]711                return True
712
[202]713        def set_ticket_fields(self, ticket):
[77]714                """
[202]715                set the ticket fields to value specified
716                        - /etc/email2trac.conf with <prefix>_<field>
717                        - trac default values, trac.ini
718                """
719                user_dict = dict()
720
721                for field in ticket.fields:
722
723                        name = field['name']
724
[215]725                        # skip some fields like resolution
726                        #
727                        if name in [ 'resolution' ]:
728                                continue
729
[202]730                        # default trac value
731                        #
[233]732                        if not field.get('custom'):
733                                value = self.get_config('ticket', 'default_%s' %(name) )
734                        else:
735                                value = field.get('value')
736                                options = field.get('options')
[234]737                                if value and options and value not in options:
[233]738                                        value = options[int(value)]
739
[202]740                        if self.DEBUG > 10:
741                                print 'trac.ini name %s = %s' %(name, value)
742
[206]743                        prefix = self.parameters['ticket_prefix']
[202]744                        try:
[206]745                                value = self.parameters['%s_%s' %(prefix, name)]
[202]746                                if self.DEBUG > 10:
747                                        print 'email2trac.conf %s = %s ' %(name, value)
748
749                        except KeyError, detail:
750                                pass
751               
752                        if self.DEBUG:
753                                print 'user_dict[%s] = %s' %(name, value)
754
755                        user_dict[name] = value
756
757                self.update_ticket_fields(ticket, user_dict, use_default=1)
758
759                # Set status ticket
760                #`
761                ticket['status'] = 'new'
762
763
764
[262]765        def new_ticket(self, msg, subject, spam, set_fields = None):
[202]766                """
[77]767                Create a new ticket
768                """
[250]769                if self.DEBUG:
770                        print "TD: new_ticket"
771
[41]772                tkt = Ticket(self.env)
[202]773                self.set_ticket_fields(tkt)
774
775                # Old style setting for component, will be removed
776                #
[204]777                if spam:
778                        tkt['component'] = 'Spam'
779
[206]780                elif self.parameters.has_key('component'):
781                        tkt['component'] = self.parameters['component']
[201]782
[22]783                if not msg['Subject']:
[151]784                        tkt['summary'] = u'(No subject)'
[22]785                else:
[264]786                        tkt['summary'] = subject
[22]787
[72]788                self.set_reply_fields(tkt, msg)
[22]789
[262]790                if set_fields:
791                        rest, keywords = string.split(set_fields, '?')
792
793                        if keywords:
794                                update_fields = self.str_to_dict(keywords)
795                                self.update_ticket_fields(tkt, update_fields)
796
[45]797                # produce e-mail like header
798                #
[22]799                head = ''
800                if self.EMAIL_HEADER > 0:
801                        head = self.email_header_txt(msg)
[92]802                       
[236]803                message_parts = self.get_message_parts(msg)
804                message_parts = self.unique_attachment_names(message_parts)
805               
806                if self.EMAIL_HEADER > 0:
807                        message_parts.insert(0, self.email_header_txt(msg))
808                       
809                body_text = self.body_text(message_parts)
[45]810
[236]811                tkt['description'] = body_text
[90]812
[182]813                #when = int(time.time())
[192]814                #
[182]815                utc = UTC()
816                when = datetime.now(utc)
[45]817
[253]818                if not self.DRY_RUN:
819                        self.id = tkt.insert()
[273]820       
[90]821                changed = False
822                comment = ''
[77]823
[273]824                # some routines in trac are dependend on ticket id     
825                # like alternate notify template
826                #
827                if self.notify_template:
[274]828                        tkt['id'] = self.id
[273]829                        changed = True
830
[90]831                # Rewrite the description if we have mailto enabled
[45]832                #
[72]833                if self.MAILTO:
[100]834                        changed = True
[142]835                        comment = u'\nadded mailto line\n'
[253]836                        mailto = self.html_mailto_link( m['Subject'], body_text)
837
[213]838                        tkt['description'] = u'%s\r\n%s%s\r\n' \
[142]839                                %(head, mailto, body_text)
[292]840               
841                #  Disabled
842                #has_attachments =  self.attachments(message_parts)
843                #if has_attachments:
844                #       changed = True
845                #       comment = '%s\n%s\n' %(comment, has_attachments)
[45]846
[90]847                if changed:
[204]848                        if self.DRY_RUN:
[250]849                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
[201]850                        else:
851                                tkt.save_changes(self.author, comment)
852                                #print tkt.get_changelog(self.db, when)
[90]853
[250]854                if self.notification and not spam:
[253]855                        self.notify(tkt, True)
[45]856
[260]857
858        def blog(self, id):
859                """
860                The blog create/update function
861                """
862                # import the modules
863                #
864                from tracfullblog.core import FullBlogCore
865                from tracfullblog.model import BlogPost, BlogCommen
866
867                # instantiate blog core
868                blog = FullBlogCore(self.env)
869                req = ''
870               
871                if id:
872
873                        # update blog
874                        #
[268]875                        comment = BlogComment(self.env, id)
[260]876                        comment.author = self.author
877                        comment.comment = self.get_body_text(m)
878                        blog.create_comment(req, comment)
879
880                else:
881                        # create blog
882                        #
883                        import time
884                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
885
886                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
887                       
888                        post.author = self.author
889                        post.title = self.email_to_unicode(m['Subject'])
890                        post.body = self.get_body_text(m)
891                       
892                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
893
894
[77]895        def parse(self, fp):
[96]896                global m
897
[77]898                m = email.message_from_file(fp)
[239]899               
[77]900                if not m:
[221]901                        if self.DEBUG:
[250]902                                print "TD: This is not a valid email message format"
[77]903                        return
[239]904                       
905                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
906                m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
907
[77]908                if self.DEBUG > 1:        # save the entire e-mail message text
[236]909                        message_parts = self.get_message_parts(m)
910                        message_parts = self.unique_attachment_names(message_parts)
[219]911                        self.save_email_for_debug(m, True)
[236]912                        body_text = self.body_text(message_parts)
913                        self.debug_body(body_text, True)
914                        self.debug_attachments(message_parts)
[77]915
916                self.db = self.env.get_db_cnx()
[194]917                self.get_sender_info(m)
[152]918
[221]919                if not self.email_header_acl('white_list', self.email_addr, True):
920                        if self.DEBUG > 1 :
921                                print 'Message rejected : %s not in white list' %(self.email_addr)
922                        return False
[77]923
[221]924                if self.email_header_acl('black_list', self.email_addr, False):
925                        if self.DEBUG > 1 :
926                                print 'Message rejected : %s in black list' %(self.email_addr)
927                        return False
928
[227]929                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
[226]930                        if self.DEBUG > 1 :
931                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
932                        return False
933
[204]934                # If drop the message
[194]935                #
[204]936                if self.spam(m) == 'drop':
[194]937                        return False
938
[204]939                elif self.spam(m) == 'spam':
940                        spam_msg = True
941                else:
942                        spam_msg = False
943
[77]944                if self.get_config('notification', 'smtp_enabled') in ['true']:
945                        self.notification = 1
946                else:
947                        self.notification = 0
948
[260]949                # Check if  FullBlogPlugin is installed
[77]950                #
[260]951                blog_enabled = None
952                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
953                        blog_enabled = True
954                       
955                if not m['Subject']:
956                        return False
957                else:
958                        subject  = self.email_to_unicode(m['Subject'])         
[77]959
[260]960                #
961                # [hic] #1529: Re: LRZ
962                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
963                #
964                TICKET_RE = re.compile(r"""
965                        (?P<blog>blog:(?P<blog_id>\w*))
[262]966                        |(?P<new_fields>[#][?].*)
967                        |(?P<reply>[#][\d]+:)
968                        |(?P<reply_fields>[#][\d]+\?.*?:)
[260]969                        """, re.VERBOSE)
[77]970
[265]971                # Find out if this is a ticket or a blog
972                #
[260]973                result =  TICKET_RE.search(subject)
974
975                if result:
976                        if result.group('blog') and blog_enabled:
977                                self.blog(result.group('blog_id'))
978
979                        # update ticket + fields
980                        #
[262]981                        if result.group('reply_fields') and self.TICKET_UPDATE:
982                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
[260]983
984                        # Update ticket
985                        #
[262]986                        elif result.group('reply') and self.TICKET_UPDATE:
987                                self.ticket_update(m, result.group('reply'), spam_msg)
[260]988
[262]989                        # New ticket + fields
990                        #
991                        elif result.group('new_fields'):
992                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
993
[260]994                # Create ticket
995                #
996                else:
[262]997                        self.new_ticket(m, subject, spam_msg)
[260]998
[136]999        def strip_signature(self, text):
1000                """
1001                Strip signature from message, inspired by Mailman software
1002                """
1003                body = []
1004                for line in text.splitlines():
1005                        if line == '-- ':
1006                                break
1007                        body.append(line)
1008
1009                return ('\n'.join(body))
1010
[231]1011        def reflow(self, text, delsp = 0):
1012                """
1013                Reflow the message based on the format="flowed" specification (RFC 3676)
1014                """
1015                flowedlines = []
1016                quotelevel = 0
1017                prevflowed = 0
1018
1019                for line in text.splitlines():
1020                        from re import match
1021                       
1022                        # Figure out the quote level and the content of the current line
1023                        m = match('(>*)( ?)(.*)', line)
1024                        linequotelevel = len(m.group(1))
1025                        line = m.group(3)
1026
1027                        # Determine whether this line is flowed
1028                        if line and line != '-- ' and line[-1] == ' ':
1029                                flowed = 1
1030                        else:
1031                                flowed = 0
1032
1033                        if flowed and delsp and line and line[-1] == ' ':
1034                                line = line[:-1]
1035
1036                        # If the previous line is flowed, append this line to it
1037                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1038                                flowedlines[-1] += line
1039                        # Otherwise, start a new line
1040                        else:
1041                                flowedlines.append('>' * linequotelevel + line)
1042
1043                        prevflowed = flowed
1044                       
1045
1046                return '\n'.join(flowedlines)
1047
[191]1048        def strip_quotes(self, text):
[193]1049                """
1050                Strip quotes from message by Nicolas Mendoza
1051                """
1052                body = []
1053                for line in text.splitlines():
1054                        if line.startswith(self.EMAIL_QUOTE):
1055                                continue
1056                        body.append(line)
[151]1057
[193]1058                return ('\n'.join(body))
[191]1059
[154]1060        def wrap_text(self, text, replace_whitespace = False):
[151]1061                """
[191]1062                Will break a lines longer then given length into several small
1063                lines of size given length
[151]1064                """
1065                import textwrap
[154]1066
[151]1067                LINESEPARATOR = '\n'
[153]1068                reformat = ''
[151]1069
[154]1070                for s in text.split(LINESEPARATOR):
1071                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1072                        if tmp:
1073                                reformat = '%s\n%s' %(reformat,tmp)
1074                        else:
1075                                reformat = '%s\n' %reformat
[153]1076
1077                return reformat
1078
[154]1079                # Python2.4 and higher
1080                #
1081                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1082                #
1083
1084
[236]1085        def get_message_parts(self, msg):
[45]1086                """
[236]1087                parses the email message and returns a list of body parts and attachments
1088                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
[45]1089                """
[236]1090                message_parts = []
[294]1091       
[278]1092                ALTERNATIVE_MULTIPART = False
1093
[22]1094                for part in msg.walk():
[236]1095                        if self.DEBUG:
[278]1096                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
[236]1097                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
[278]1098
1099                        ## Check content type
[294]1100                        #
1101                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
[238]1102
[294]1103                                if self.DEBUG:
1104                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
[238]1105
1106                                continue
1107
[294]1108                        ## Catch some mulitpart execptions
1109                        #
1110                        if part.get_content_type() == 'multipart/alternative':
[278]1111                                ALTERNATIVE_MULTIPART = True
1112                                continue
1113
[294]1114                        ## Skip multipart containers
[278]1115                        #
[45]1116                        if part.get_content_maintype() == 'multipart':
[278]1117                                if self.DEBUG:
1118                                        print "TD: Skipping multipart container"
[22]1119                                continue
[278]1120                       
[294]1121                        ## 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"
1122                        #
[236]1123                        inline = self.inline_part(part)
1124
[294]1125                        ## Drop HTML message
1126                        #
[278]1127                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1128                                if part.get_content_type() == 'text/html':
1129                                        if self.DEBUG:
1130                                                print "TD: Skipping alternative HTML message"
1131
1132                                        ALTERNATIVE_MULTIPART = False
1133                                        continue
1134
[294]1135                        ## Inline text parts are where the body is
1136                        #
[236]1137                        if part.get_content_type() == 'text/plain' and inline:
1138                                if self.DEBUG:
1139                                        print 'TD:               Inline body part'
1140
[45]1141                                # Try to decode, if fails then do not decode
1142                                #
[90]1143                                body_text = part.get_payload(decode=1)
[45]1144                                if not body_text:                       
[90]1145                                        body_text = part.get_payload(decode=0)
[231]1146
[232]1147                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1148                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1149
1150                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1151                                        body_text = self.reflow(body_text, delsp == 'yes')
[154]1152       
[136]1153                                if self.STRIP_SIGNATURE:
1154                                        body_text = self.strip_signature(body_text)
[22]1155
[191]1156                                if self.STRIP_QUOTES:
1157                                        body_text = self.strip_quotes(body_text)
1158
[148]1159                                if self.USE_TEXTWRAP:
[151]1160                                        body_text = self.wrap_text(body_text)
[148]1161
[294]1162                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
[45]1163                                #
[100]1164                                charset = part.get_content_charset()
[102]1165                                if not charset:
1166                                        charset = 'iso-8859-15'
1167
[89]1168                                try:
[96]1169                                        ubody_text = unicode(body_text, charset)
[100]1170
1171                                except UnicodeError, detail:
[96]1172                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]1173
[100]1174                                except LookupError, detail:
[139]1175                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1176
[236]1177                                if self.VERBATIM_FORMAT:
1178                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1179                                else:
1180                                        message_parts.append('%s' %ubody_text)
1181                        else:
1182                                if self.DEBUG:
1183                                        print 'TD:               Filename: %s' % part.get_filename()
[22]1184
[236]1185                                message_parts.append((part.get_filename(), part))
1186
1187                return message_parts
1188               
[253]1189        def unique_attachment_names(self, message_parts):
[236]1190                renamed_parts = []
1191                attachment_names = set()
1192                for part in message_parts:
1193                       
1194                        # If not an attachment, leave it alone
1195                        if not isinstance(part, tuple):
1196                                renamed_parts.append(part)
1197                                continue
1198                               
1199                        (filename, part) = part
1200                        # Decode the filename
1201                        if filename:
1202                                filename = self.email_to_unicode(filename)                     
1203                        # If no name, use a default one
[22]1204                        else:
[236]1205                                filename = 'untitled-part'
[22]1206
[242]1207                                # Guess the extension from the content type, use non strict mode
1208                                # some additional non-standard but commonly used MIME types
1209                                # are also recognized
1210                                #
1211                                ext = mimetypes.guess_extension(part.get_content_type(), False)
[236]1212                                if not ext:
1213                                        ext = '.bin'
[22]1214
[236]1215                                filename = '%s%s' % (filename, ext)
[22]1216
[236]1217                        # Discard relative paths in attachment names
1218                        filename = filename.replace('\\', '/').replace(':', '/')
1219                        filename = os.path.basename(filename)
[22]1220
[236]1221                        # We try to normalize the filename to utf-8 NFC if we can.
1222                        # Files uploaded from OS X might be in NFD.
1223                        # Check python version and then try it
1224                        #
1225                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1226                                try:
1227                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1228                                except TypeError:
1229                                        pass
1230                                       
1231                        if self.QUOTE_ATTACHMENT_FILENAMES:
[272]1232                                try:
1233                                        filename = urllib.quote(filename)
1234                                except KeyError, detail:
1235                                        filename = urllib.quote(filename.encode('utf-8'))
[100]1236
[236]1237                        # Make the filename unique for this ticket
1238                        num = 0
1239                        unique_filename = filename
1240                        filename, ext = os.path.splitext(filename)
[134]1241
[253]1242                        while unique_filename in attachment_names or self.attachment_exists(unique_filename):
[236]1243                                num += 1
1244                                unique_filename = "%s-%s%s" % (filename, num, ext)
1245                               
1246                        if self.DEBUG:
1247                                print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
[100]1248
[236]1249                        attachment_names.add(unique_filename)
1250
1251                        renamed_parts.append((filename, unique_filename, part))
1252               
1253                return renamed_parts
1254                       
1255        def inline_part(self, part):
1256                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1257               
1258                       
[253]1259        def attachment_exists(self, filename):
[250]1260
1261                if self.DEBUG:
[253]1262                        print "TD: attachment_exists: Ticket number : %s, Filename : %s" %(self.id, filename)
[250]1263
1264                # We have no valid ticket id
1265                #
[253]1266                if not self.id:
[236]1267                        return False
[250]1268
[236]1269                try:
[253]1270                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
[236]1271                        return True
[250]1272                except attachment.ResourceNotFound:
[236]1273                        return False
1274                       
1275        def body_text(self, message_parts):
1276                body_text = []
1277               
1278                for part in message_parts:
1279                        # Plain text part, append it
1280                        if not isinstance(part, tuple):
1281                                body_text.extend(part.strip().splitlines())
1282                                body_text.append("")
1283                                continue
1284                               
1285                        (original, filename, part) = part
1286                        inline = self.inline_part(part)
1287                       
1288                        if part.get_content_maintype() == 'image' and inline:
1289                                body_text.append('[[Image(%s)]]' % filename)
1290                                body_text.append("")
1291                        else:
1292                                body_text.append('[attachment:"%s"]' % filename)
1293                                body_text.append("")
1294                               
1295                body_text = '\r\n'.join(body_text)
1296                return body_text
1297
[253]1298        def notify(self, tkt, new=True, modtime=0):
[79]1299                """
1300                A wrapper for the TRAC notify function. So we can use templates
1301                """
[250]1302                if self.DRY_RUN:
1303                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1304                                return
[41]1305                try:
1306                        # create false {abs_}href properties, to trick Notify()
1307                        #
[193]1308                        if not self.VERSION == 0.11:
[192]1309                                self.env.abs_href = Href(self.get_config('project', 'url'))
1310                                self.env.href = Href(self.get_config('project', 'url'))
[22]1311
[41]1312                        tn = TicketNotifyEmail(self.env)
[213]1313
[42]1314                        if self.notify_template:
[222]1315
[221]1316                                if self.VERSION == 0.11:
[222]1317
[221]1318                                        from trac.web.chrome import Chrome
[222]1319
1320                                        if self.notify_template_update and not new:
1321                                                tn.template_name = self.notify_template_update
1322                                        else:
1323                                                tn.template_name = self.notify_template
1324
[221]1325                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1326                                               
1327                                else:
[222]1328
[221]1329                                        tn.template_name = self.notify_template;
[42]1330
[77]1331                        tn.notify(tkt, new, modtime)
[41]1332
1333                except Exception, e:
[253]1334                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
[41]1335
[253]1336        def html_mailto_link(self, subject, body):
1337                """
1338                This function returns a HTML mailto tag with the ticket id and author email address
1339                """
[72]1340                if not self.author:
[143]1341                        author = self.email_addr
[22]1342                else:   
[142]1343                        author = self.author
[22]1344
[253]1345                # use urllib to escape the chars
[22]1346                #
[288]1347                s = 'mailto:%s?Subject=%s&Cc=%s' %(
[74]1348                       urllib.quote(self.email_addr),
[253]1349                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
[74]1350                           urllib.quote(self.MAILTO_CC)
1351                           )
1352
[290]1353                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)
[288]1354                return s
[22]1355
[253]1356        def attachments(self, message_parts, update=False):
[79]1357                '''
1358                save any attachments as files in the ticket's directory
1359                '''
[237]1360                if self.DRY_RUN:
[250]1361                        print "DRY_RUN: no attachments saved"
[237]1362                        return ''
1363
[22]1364                count = 0
[152]1365
1366                # Get Maxium attachment size
1367                #
1368                max_size = int(self.get_config('attachment', 'max_size'))
[153]1369                status   = ''
[236]1370               
1371                for part in message_parts:
1372                        # Skip body parts
1373                        if not isinstance(part, tuple):
[22]1374                                continue
[236]1375                               
1376                        (original, filename, part) = part
[48]1377                        #
[172]1378                        # Must be tuneables HvB
1379                        #
[236]1380                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
[22]1381                        text = part.get_payload(decode=1)
1382                        if not text:
1383                                text = '(None)'
[48]1384                        fd.write(text)
1385                        fd.close()
[22]1386
[153]1387                        # get the file_size
[22]1388                        #
[48]1389                        stats = os.lstat(path)
[153]1390                        file_size = stats[stat.ST_SIZE]
[22]1391
[152]1392                        # Check if the attachment size is allowed
1393                        #
[153]1394                        if (max_size != -1) and (file_size > max_size):
1395                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
[236]1396                                        %(status, original, file_size, max_size)
[152]1397
1398                                os.unlink(path)
1399                                continue
1400                        else:
1401                                count = count + 1
1402                                       
[172]1403                        # Insert the attachment
[73]1404                        #
[242]1405                        fd = open(path, 'rb')
[253]1406                        att = attachment.Attachment(self.env, 'ticket', self.id)
[73]1407
[172]1408                        # This will break the ticket_update system, the body_text is vaporized
1409                        # ;-(
1410                        #
1411                        if not update:
1412                                att.author = self.author
1413                                att.description = self.email_to_unicode('Added by email2trac')
[73]1414
[236]1415                        att.insert(filename, fd, file_size)
[172]1416                        #except  util.TracError, detail:
1417                        #       print detail
[73]1418
[103]1419                        # Remove the created temporary filename
1420                        #
[172]1421                        fd.close()
[103]1422                        os.unlink(path)
1423
[77]1424                # Return how many attachments
1425                #
[153]1426                status = 'This message has %d attachment(s)\n%s' %(count, status)
1427                return status
[22]1428
[77]1429
[22]1430def mkdir_p(dir, mode):
1431        '''do a mkdir -p'''
1432
1433        arr = string.split(dir, '/')
1434        path = ''
1435        for part in arr:
1436                path = '%s/%s' % (path, part)
1437                try:
1438                        stats = os.stat(path)
1439                except OSError:
1440                        os.mkdir(path, mode)
1441
1442def ReadConfig(file, name):
1443        """
1444        Parse the config file
1445        """
1446        if not os.path.isfile(file):
[79]1447                print 'File %s does not exist' %file
[22]1448                sys.exit(1)
1449
[199]1450        config = trac_config.Configuration(file)
[22]1451
1452        # Use given project name else use defaults
1453        #
1454        if name:
[199]1455                sections = config.sections()
1456                if not name in sections:
[79]1457                        print "Not a valid project name: %s" %name
[199]1458                        print "Valid names: %s" %sections
[22]1459                        sys.exit(1)
1460
1461                project =  dict()
[199]1462                for option, value in  config.options(name):
1463                        project[option] = value
[22]1464
1465        else:
[270]1466                # use some trac internals to get the defaults
[217]1467                #
1468                project = config.parser.defaults()
[22]1469
1470        return project
1471
[87]1472
[22]1473if __name__ == '__main__':
1474        # Default config file
1475        #
[24]1476        configfile = '@email2trac_conf@'
[22]1477        project = ''
1478        component = ''
[202]1479        ticket_prefix = 'default'
[204]1480        dry_run = None
[202]1481
[87]1482        ENABLE_SYSLOG = 0
[201]1483
[204]1484
[202]1485        SHORT_OPT = 'chf:np:t:'
1486        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
[201]1487
[22]1488        try:
[201]1489                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
[22]1490        except getopt.error,detail:
1491                print __doc__
1492                print detail
1493                sys.exit(1)
[87]1494       
[22]1495        project_name = None
1496        for opt,value in opts:
1497                if opt in [ '-h', '--help']:
1498                        print __doc__
1499                        sys.exit(0)
1500                elif opt in ['-c', '--component']:
1501                        component = value
1502                elif opt in ['-f', '--file']:
1503                        configfile = value
[201]1504                elif opt in ['-n', '--dry-run']:
[204]1505                        dry_run = True
[22]1506                elif opt in ['-p', '--project']:
1507                        project_name = value
[202]1508                elif opt in ['-t', '--ticket_prefix']:
1509                        ticket_prefix = value
[87]1510       
[22]1511        settings = ReadConfig(configfile, project_name)
1512        if not settings.has_key('project'):
1513                print __doc__
[79]1514                print 'No Trac project is defined in the email2trac config file.'
[22]1515                sys.exit(1)
[87]1516       
[22]1517        if component:
1518                settings['component'] = component
[202]1519
1520        # The default prefix for ticket values in email2trac.conf
1521        #
1522        settings['ticket_prefix'] = ticket_prefix
[206]1523        settings['dry_run'] = dry_run
[87]1524       
[22]1525        if settings.has_key('trac_version'):
[189]1526                version = settings['trac_version']
[22]1527        else:
1528                version = trac_default_version
1529
[189]1530
[22]1531        #debug HvB
1532        #print settings
[189]1533
[87]1534        try:
[189]1535                if version == '0.9':
[87]1536                        from trac import attachment
1537                        from trac.env import Environment
1538                        from trac.ticket import Ticket
1539                        from trac.web.href import Href
1540                        from trac import util
1541                        from trac.Notify import TicketNotifyEmail
[189]1542                elif version == '0.10':
[87]1543                        from trac import attachment
1544                        from trac.env import Environment
1545                        from trac.ticket import Ticket
1546                        from trac.web.href import Href
1547                        from trac import util
[139]1548                        #
1549                        # return  util.text.to_unicode(str)
1550                        #
[87]1551                        # see http://projects.edgewall.com/trac/changeset/2799
1552                        from trac.ticket.notification import TicketNotifyEmail
[199]1553                        from trac import config as trac_config
[189]1554                elif version == '0.11':
[182]1555                        from trac import attachment
1556                        from trac.env import Environment
1557                        from trac.ticket import Ticket
1558                        from trac.web.href import Href
[199]1559                        from trac import config as trac_config
[182]1560                        from trac import util
[260]1561
1562
[182]1563                        #
1564                        # return  util.text.to_unicode(str)
1565                        #
1566                        # see http://projects.edgewall.com/trac/changeset/2799
1567                        from trac.ticket.notification import TicketNotifyEmail
[189]1568                else:
1569                        print 'TRAC version %s is not supported' %version
1570                        sys.exit(1)
1571                       
1572                if settings.has_key('enable_syslog'):
[190]1573                        if SYSLOG_AVAILABLE:
1574                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
[182]1575
[291]1576
1577                # Must be set before environment is created
1578                #
1579                if settings.has_key('python_egg_cache'):
1580                        python_egg_cache = str(settings['python_egg_cache'])
1581                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
1582
[87]1583                env = Environment(settings['project'], create=0)
[206]1584                tktparser = TicketEmailParser(env, settings, float(version))
[87]1585                tktparser.parse(sys.stdin)
[22]1586
[87]1587        # Catch all errors ans log to SYSLOG if we have enabled this
1588        # else stdout
1589        #
1590        except Exception, error:
1591                if ENABLE_SYSLOG:
1592                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
[187]1593
[87]1594                        etype, evalue, etb = sys.exc_info()
1595                        for e in traceback.format_exception(etype, evalue, etb):
1596                                syslog.syslog(e)
[187]1597
[87]1598                        syslog.closelog()
1599                else:
1600                        traceback.print_exc()
[22]1601
[97]1602                if m:
[98]1603                        tktparser.save_email_for_debug(m, True)
[97]1604
[249]1605                sys.exit(1)
[22]1606# EOB
Note: See TracBrowser for help on using the repository browser.