source: trunk/email2trac.py.in @ 272

Last change on this file since 272 was 272, checked in by bas, 15 years ago

email2trac.py.in:

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