source: trunk/email2trac.py.in @ 268

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

email2trac.py.in:

  • fixed an error in blog update function
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 40.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
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 268 2009-04-15 07:31:22Z 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                """
[183]476                ticket['reporter'] = self.email_from
[72]477
[45]478                # Put all CC-addresses in ticket CC field
[43]479                #
480                if self.REPLY_ALL:
[45]481                        #tos = message.get_all('to', [])
[43]482                        ccs = message.get_all('cc', [])
483
[45]484                        addrs = email.Utils.getaddresses(ccs)
[105]485                        if not addrs:
486                                return
[43]487
488                        # Remove reporter email address if notification is
489                        # on
490                        #
491                        if self.notification:
492                                try:
[72]493                                        addrs.remove((self.author, self.email_addr))
[43]494                                except ValueError, detail:
495                                        pass
496
[45]497                        for name,mail in addrs:
[105]498                                try:
[108]499                                        mail_list = '%s, %s' %(mail_list, mail)
[230]500                                except UnboundLocalError, detail:
[105]501                                        mail_list = mail
[43]502
[105]503                        if mail_list:
[139]504                                ticket['cc'] = self.email_to_unicode(mail_list)
[96]505
506        def save_email_for_debug(self, message, tempfile=False):
507                if tempfile:
508                        import tempfile
509                        msg_file = tempfile.mktemp('.email2trac')
510                else:
[173]511                        #msg_file = '/var/tmp/msg.txt'
512                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
513
[44]514                print 'TD: saving email to %s' % msg_file
515                fx = open(msg_file, 'wb')
516                fx.write('%s' % message)
517                fx.close()
518                try:
519                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
520                except OSError:
521                        pass
522
[167]523        def str_to_dict(self, str):
[164]524                """
525                Transfrom a str of the form [<key>=<value>]+ to dict[<key>] = <value>
526                """
527
[262]528                fields = string.split(str, ',')
529
[164]530                result = dict()
531                for field in fields:
532                        try:
[262]533                                index, value = string.split(field, '=')
[169]534
535                                # We can not change the description of a ticket via the subject
536                                # line. The description is the body of the email
537                                #
538                                if index.lower() in ['description']:
539                                        continue
540
[164]541                                if value:
[165]542                                        result[index.lower()] = value
[169]543
[164]544                        except ValueError:
545                                pass
546
[165]547                return result
[167]548
[202]549        def update_ticket_fields(self, ticket, user_dict, use_default=None):
550                """
551                This will update the ticket fields. It will check if the
552                given fields are known and if the right values are specified
553                It will only update the ticket field value:
[169]554                        - If the field is known
[202]555                        - If the value supplied is valid for the ticket field.
556                          If not then there are two options:
557                           1) Skip the value (use_default=None)
558                           2) Set default value for field (use_default=1)
[169]559                """
560
561                # Build a system dictionary from the ticket fields
562                # with field as index and option as value
563                #
564                sys_dict = dict()
565                for field in ticket.fields:
[167]566                        try:
[169]567                                sys_dict[field['name']] = field['options']
568
[167]569                        except KeyError:
[169]570                                sys_dict[field['name']] = None
[167]571                                pass
[169]572
573                # Check user supplied fields an compare them with the
574                # system one's
575                #
576                for field,value in user_dict.items():
[202]577                        if self.DEBUG >= 10:
578                                print  'user_field\t %s = %s' %(field,value)
[169]579
580                        if sys_dict.has_key(field):
581
582                                # Check if value is an allowed system option, if TypeError then
583                                # every value is allowed
584                                #
585                                try:
586                                        if value in sys_dict[field]:
587                                                ticket[field] = value
[202]588                                        else:
589                                                # Must we set a default if value is not allowed
590                                                #
591                                                if use_default:
592                                                        value = self.get_config('ticket', 'default_%s' %(field) )
593                                                        ticket[field] = value
[169]594
595                                except TypeError:
596                                        ticket[field] = value
[202]597
598                                if self.DEBUG >= 10:
599                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
[169]600                                       
[260]601        def ticket_update(self, m, id, spam):
[78]602                """
[79]603                If the current email is a reply to an existing ticket, this function
604                will append the contents of this email to that ticket, instead of
605                creating a new one.
[78]606                """
[250]607                if self.DEBUG:
[260]608                        print "TD: ticket_update: %s" %id
[202]609
[164]610                # Must we update ticket fields
611                #
[220]612                update_fields = dict()
[165]613                try:
[260]614                        id, keywords = string.split(id, '?')
[262]615
616                        # Skip the last ':' character
617                        #
618                        keywords = keywords[:-1]
[220]619                        update_fields = self.str_to_dict(keywords)
[165]620
621                        # Strip '#'
622                        #
[260]623                        self.id = int(id[1:])
[165]624
[260]625                except ValueError:
[165]626                        # Strip '#' and ':'
627                        #
[260]628                        self.id = int(id[1:-1])
[164]629
[71]630
[194]631                # When is the change committed
632                #
[77]633                #
[194]634                if self.VERSION == 0.11:
635                        utc = UTC()
636                        when = datetime.now(utc)
637                else:
638                        when = int(time.time())
[77]639
[172]640                try:
[253]641                        tkt = Ticket(self.env, self.id, self.db)
[172]642                except util.TracError, detail:
[253]643                        # Not a valid ticket
644                        self.id = None
[172]645                        return False
[126]646
[220]647                # reopen the ticket if it is was closed
648                # We must use the ticket workflow framework
649                #
650                if tkt['status'] in ['closed']:
651
[257]652                        #print controller.actions['reopen']
653                        #
654                        # As reference 
655                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
656                        #
657                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
658                        #print 'controller : ', a
659                        #
660                        #b = controller.get_all_status()
661                        #print 'get all status: ', b
662                        #
663                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
664                        #print 'get_ticket_changes :', b
665
[258]666                        if self.WORKFLOW and (self.VERSION in ['0.11']) :
[257]667                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
668                                from trac.test import Mock, MockPerm
669
670                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
671
672                                controller = ConfigurableTicketWorkflow(self.env)
673                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
674
675                                if self.DEBUG:
676                                        print 'TD: Workflow ticket update fields: ', fields
677
678                                for key in fields.keys():
679                                        tkt[key] = fields[key]
680
681                        else:
682                                tkt['status'] = 'reopened'
683                                tkt['resolution'] = ''
684
[172]685                # Must we update some ticket fields properties
686                #
[220]687                if update_fields:
688                        self.update_ticket_fields(tkt, update_fields)
[166]689
[236]690                message_parts = self.get_message_parts(m)
[253]691                message_parts = self.unique_attachment_names(message_parts)
[210]692
[177]693                if self.EMAIL_HEADER:
[236]694                        message_parts.insert(0, self.email_header_txt(m))
[76]695
[236]696                body_text = self.body_text(message_parts)
697
[241]698                if body_text.strip() or update_fields:
[250]699                        if self.DRY_RUN:
700                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
701                        else:
702                                tkt.save_changes(self.author, body_text, when)
[219]703
[129]704                if self.VERSION  == 0.9:
[253]705                        str = self.attachments(message_parts, True)
[129]706                else:
[253]707                        str = self.attachments(message_parts)
[76]708
[204]709                if self.notification and not spam:
[253]710                        self.notify(tkt, False, when)
[72]711
[71]712                return True
713
[202]714        def set_ticket_fields(self, ticket):
[77]715                """
[202]716                set the ticket fields to value specified
717                        - /etc/email2trac.conf with <prefix>_<field>
718                        - trac default values, trac.ini
719                """
720                user_dict = dict()
721
722                for field in ticket.fields:
723
724                        name = field['name']
725
[215]726                        # skip some fields like resolution
727                        #
728                        if name in [ 'resolution' ]:
729                                continue
730
[202]731                        # default trac value
732                        #
[233]733                        if not field.get('custom'):
734                                value = self.get_config('ticket', 'default_%s' %(name) )
735                        else:
736                                value = field.get('value')
737                                options = field.get('options')
[234]738                                if value and options and value not in options:
[233]739                                        value = options[int(value)]
740
[202]741                        if self.DEBUG > 10:
742                                print 'trac.ini name %s = %s' %(name, value)
743
[206]744                        prefix = self.parameters['ticket_prefix']
[202]745                        try:
[206]746                                value = self.parameters['%s_%s' %(prefix, name)]
[202]747                                if self.DEBUG > 10:
748                                        print 'email2trac.conf %s = %s ' %(name, value)
749
750                        except KeyError, detail:
751                                pass
752               
753                        if self.DEBUG:
754                                print 'user_dict[%s] = %s' %(name, value)
755
756                        user_dict[name] = value
757
758                self.update_ticket_fields(ticket, user_dict, use_default=1)
759
760                # Set status ticket
761                #`
762                ticket['status'] = 'new'
763
764
765
[262]766        def new_ticket(self, msg, subject, spam, set_fields = None):
[202]767                """
[77]768                Create a new ticket
769                """
[250]770                if self.DEBUG:
771                        print "TD: new_ticket"
772
[41]773                tkt = Ticket(self.env)
[202]774                self.set_ticket_fields(tkt)
775
776                # Old style setting for component, will be removed
777                #
[204]778                if spam:
779                        tkt['component'] = 'Spam'
780
[206]781                elif self.parameters.has_key('component'):
782                        tkt['component'] = self.parameters['component']
[201]783
[22]784                if not msg['Subject']:
[151]785                        tkt['summary'] = u'(No subject)'
[22]786                else:
[264]787                        tkt['summary'] = subject
[22]788
[72]789                self.set_reply_fields(tkt, msg)
[22]790
[262]791                if set_fields:
792                        rest, keywords = string.split(set_fields, '?')
793
794                        if keywords:
795                                update_fields = self.str_to_dict(keywords)
796                                self.update_ticket_fields(tkt, update_fields)
797
[45]798                # produce e-mail like header
799                #
[22]800                head = ''
801                if self.EMAIL_HEADER > 0:
802                        head = self.email_header_txt(msg)
[92]803                       
[236]804                message_parts = self.get_message_parts(msg)
805                message_parts = self.unique_attachment_names(message_parts)
806               
807                if self.EMAIL_HEADER > 0:
808                        message_parts.insert(0, self.email_header_txt(msg))
809                       
810                body_text = self.body_text(message_parts)
[45]811
[236]812                tkt['description'] = body_text
[90]813
[182]814                #when = int(time.time())
[192]815                #
[182]816                utc = UTC()
817                when = datetime.now(utc)
[45]818
[253]819                if not self.DRY_RUN:
820                        self.id = tkt.insert()
[250]821               
[90]822                changed = False
823                comment = ''
[77]824
[90]825                # Rewrite the description if we have mailto enabled
[45]826                #
[72]827                if self.MAILTO:
[100]828                        changed = True
[142]829                        comment = u'\nadded mailto line\n'
[253]830                        mailto = self.html_mailto_link( m['Subject'], body_text)
831
[213]832                        tkt['description'] = u'%s\r\n%s%s\r\n' \
[142]833                                %(head, mailto, body_text)
[45]834
[253]835                str =  self.attachments(message_parts)
[152]836                if str:
[100]837                        changed = True
[152]838                        comment = '%s\n%s\n' %(comment, str)
[77]839
[90]840                if changed:
[204]841                        if self.DRY_RUN:
[250]842                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
[201]843                        else:
844                                tkt.save_changes(self.author, comment)
845                                #print tkt.get_changelog(self.db, when)
[90]846
[250]847                if self.notification and not spam:
[253]848                        self.notify(tkt, True)
[45]849
[260]850
851        def blog(self, id):
852                """
853                The blog create/update function
854                """
855                # import the modules
856                #
857                from tracfullblog.core import FullBlogCore
858                from tracfullblog.model import BlogPost, BlogCommen
859
860                # instantiate blog core
861                blog = FullBlogCore(self.env)
862                req = ''
863               
864                if id:
865
866                        # update blog
867                        #
[268]868                        comment = BlogComment(self.env, id)
[260]869                        comment.author = self.author
870                        comment.comment = self.get_body_text(m)
871                        blog.create_comment(req, comment)
872
873                else:
874                        # create blog
875                        #
876                        import time
877                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
878
879                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
880                       
881                        post.author = self.author
882                        post.title = self.email_to_unicode(m['Subject'])
883                        post.body = self.get_body_text(m)
884                       
885                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
886
887
[77]888        def parse(self, fp):
[96]889                global m
890
[77]891                m = email.message_from_file(fp)
[239]892               
[77]893                if not m:
[221]894                        if self.DEBUG:
[250]895                                print "TD: This is not a valid email message format"
[77]896                        return
[239]897                       
898                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
899                m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
900
[77]901                if self.DEBUG > 1:        # save the entire e-mail message text
[236]902                        message_parts = self.get_message_parts(m)
903                        message_parts = self.unique_attachment_names(message_parts)
[219]904                        self.save_email_for_debug(m, True)
[236]905                        body_text = self.body_text(message_parts)
906                        self.debug_body(body_text, True)
907                        self.debug_attachments(message_parts)
[77]908
909                self.db = self.env.get_db_cnx()
[194]910                self.get_sender_info(m)
[152]911
[221]912                if not self.email_header_acl('white_list', self.email_addr, True):
913                        if self.DEBUG > 1 :
914                                print 'Message rejected : %s not in white list' %(self.email_addr)
915                        return False
[77]916
[221]917                if self.email_header_acl('black_list', self.email_addr, False):
918                        if self.DEBUG > 1 :
919                                print 'Message rejected : %s in black list' %(self.email_addr)
920                        return False
921
[227]922                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
[226]923                        if self.DEBUG > 1 :
924                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
925                        return False
926
[204]927                # If drop the message
[194]928                #
[204]929                if self.spam(m) == 'drop':
[194]930                        return False
931
[204]932                elif self.spam(m) == 'spam':
933                        spam_msg = True
[194]934
[204]935                else:
936                        spam_msg = False
937
[77]938                if self.get_config('notification', 'smtp_enabled') in ['true']:
939                        self.notification = 1
940                else:
941                        self.notification = 0
942
[260]943                # Check if  FullBlogPlugin is installed
[77]944                #
[260]945                blog_enabled = None
946                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
947                        blog_enabled = True
948                       
949                if not m['Subject']:
950                        return False
951                else:
952                        subject  = self.email_to_unicode(m['Subject'])         
[77]953
[260]954                #
955                # [hic] #1529: Re: LRZ
956                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
957                #
958                TICKET_RE = re.compile(r"""
959                        (?P<blog>blog:(?P<blog_id>\w*))
[262]960                        |(?P<new_fields>[#][?].*)
961                        |(?P<reply>[#][\d]+:)
962                        |(?P<reply_fields>[#][\d]+\?.*?:)
[260]963                        """, re.VERBOSE)
[77]964
[265]965                # Find out if this is a ticket or a blog
966                #
[260]967                result =  TICKET_RE.search(subject)
968
969                if result:
970                        if result.group('blog') and blog_enabled:
971                                self.blog(result.group('blog_id'))
972
973                        # update ticket + fields
974                        #
[262]975                        if result.group('reply_fields') and self.TICKET_UPDATE:
976                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
[260]977
978                        # Update ticket
979                        #
[262]980                        elif result.group('reply') and self.TICKET_UPDATE:
981                                self.ticket_update(m, result.group('reply'), spam_msg)
[260]982
[262]983                        # New ticket + fields
984                        #
985                        elif result.group('new_fields'):
986                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
987
[260]988                # Create ticket
989                #
990                else:
[262]991                        self.new_ticket(m, subject, spam_msg)
[260]992
[136]993        def strip_signature(self, text):
994                """
995                Strip signature from message, inspired by Mailman software
996                """
997                body = []
998                for line in text.splitlines():
999                        if line == '-- ':
1000                                break
1001                        body.append(line)
1002
1003                return ('\n'.join(body))
1004
[231]1005        def reflow(self, text, delsp = 0):
1006                """
1007                Reflow the message based on the format="flowed" specification (RFC 3676)
1008                """
1009                flowedlines = []
1010                quotelevel = 0
1011                prevflowed = 0
1012
1013                for line in text.splitlines():
1014                        from re import match
1015                       
1016                        # Figure out the quote level and the content of the current line
1017                        m = match('(>*)( ?)(.*)', line)
1018                        linequotelevel = len(m.group(1))
1019                        line = m.group(3)
1020
1021                        # Determine whether this line is flowed
1022                        if line and line != '-- ' and line[-1] == ' ':
1023                                flowed = 1
1024                        else:
1025                                flowed = 0
1026
1027                        if flowed and delsp and line and line[-1] == ' ':
1028                                line = line[:-1]
1029
1030                        # If the previous line is flowed, append this line to it
1031                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1032                                flowedlines[-1] += line
1033                        # Otherwise, start a new line
1034                        else:
1035                                flowedlines.append('>' * linequotelevel + line)
1036
1037                        prevflowed = flowed
1038                       
1039
1040                return '\n'.join(flowedlines)
1041
[191]1042        def strip_quotes(self, text):
[193]1043                """
1044                Strip quotes from message by Nicolas Mendoza
1045                """
1046                body = []
1047                for line in text.splitlines():
1048                        if line.startswith(self.EMAIL_QUOTE):
1049                                continue
1050                        body.append(line)
[151]1051
[193]1052                return ('\n'.join(body))
[191]1053
[154]1054        def wrap_text(self, text, replace_whitespace = False):
[151]1055                """
[191]1056                Will break a lines longer then given length into several small
1057                lines of size given length
[151]1058                """
1059                import textwrap
[154]1060
[151]1061                LINESEPARATOR = '\n'
[153]1062                reformat = ''
[151]1063
[154]1064                for s in text.split(LINESEPARATOR):
1065                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1066                        if tmp:
1067                                reformat = '%s\n%s' %(reformat,tmp)
1068                        else:
1069                                reformat = '%s\n' %reformat
[153]1070
1071                return reformat
1072
[154]1073                # Python2.4 and higher
1074                #
1075                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1076                #
1077
1078
[236]1079        def get_message_parts(self, msg):
[45]1080                """
[236]1081                parses the email message and returns a list of body parts and attachments
1082                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
[45]1083                """
[236]1084                message_parts = []
[238]1085               
1086                # This is used to figure out when we are inside an AppleDouble container
1087                # AppleDouble containers consists of two parts: Mac-specific file data, and platform-independent data
1088                # We strip away Mac-specific stuff
1089                appledouble_parts = []
[236]1090
[22]1091                for part in msg.walk():
[236]1092                        if self.DEBUG:
1093                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
[238]1094                               
1095                        # Check whether we just finished processing an AppleDouble container
1096                        if part not in appledouble_parts:
1097                                appledouble_parts = []
[236]1098
[238]1099                        # Special handling for BinHex attachments. Options are drop (leave out with no warning), warn (and leave out), and keep
1100                        if part.get_content_type() == 'application/mac-binhex40':
1101                                if self.BINHEX == 'warn':
1102                                        message_parts.append("'''A BinHex attachment named '%s' was ignored (use MIME encoding instead).'''" % part.get_filename())
1103                                        continue
1104                                elif self.BINHEX == 'drop':
1105                                        continue
1106
1107                        # Special handling for AppleSingle attachments. Options are drop (leave out with no warning), warn (and leave out), and keep
1108                        if part.get_content_type() == 'application/applefile' and not part in appledouble_parts:
1109                                if self.APPLESINGLE == 'warn':
1110                                        message_parts.append("'''An AppleSingle attachment named '%s' was ignored (use MIME encoding instead).'''" % part.get_filename())
1111                                        continue
1112                                elif self.APPLESINGLE == 'drop':
1113                                        continue
1114
1115                        # Special handling for the Mac-specific part of AppleDouble attachments. Options are strip (leave out with no warning), warn (and leave out), and keep
1116                        if part.get_content_type() == 'application/applefile':
1117                                if self.APPLEDOUBLE == 'warn':
1118                                        message_parts.append("'''The resource fork of an attachment named '%s' was removed.'''" % part.get_filename())
1119                                        continue
1120                                elif self.APPLEDOUBLE == 'strip':
1121                                        continue
1122
1123                        # If we entering an AppleDouble container, set up appledouble_parts so that we know what to do with its subparts
1124                        if part.get_content_type() == 'multipart/appledouble':
1125                                appledouble_parts = part.get_payload()
1126                                continue
1127
1128                        # Any other multipart/* is just a container for multipart messages
[45]1129                        if part.get_content_maintype() == 'multipart':
[22]1130                                continue
1131
[236]1132                        # 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"
1133                        inline = self.inline_part(part)
1134
1135                        # Inline text parts are where the body is
1136                        if part.get_content_type() == 'text/plain' and inline:
1137                                if self.DEBUG:
1138                                        print 'TD:               Inline body part'
1139
[45]1140                                # Try to decode, if fails then do not decode
1141                                #
[90]1142                                body_text = part.get_payload(decode=1)
[45]1143                                if not body_text:                       
[90]1144                                        body_text = part.get_payload(decode=0)
[231]1145
[232]1146                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1147                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1148
1149                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1150                                        body_text = self.reflow(body_text, delsp == 'yes')
[154]1151       
[136]1152                                if self.STRIP_SIGNATURE:
1153                                        body_text = self.strip_signature(body_text)
[22]1154
[191]1155                                if self.STRIP_QUOTES:
1156                                        body_text = self.strip_quotes(body_text)
1157
[148]1158                                if self.USE_TEXTWRAP:
[151]1159                                        body_text = self.wrap_text(body_text)
[148]1160
[45]1161                                # Get contents charset (iso-8859-15 if not defined in mail headers)
1162                                #
[100]1163                                charset = part.get_content_charset()
[102]1164                                if not charset:
1165                                        charset = 'iso-8859-15'
1166
[89]1167                                try:
[96]1168                                        ubody_text = unicode(body_text, charset)
[100]1169
1170                                except UnicodeError, detail:
[96]1171                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]1172
[100]1173                                except LookupError, detail:
[139]1174                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1175
[236]1176                                if self.VERBATIM_FORMAT:
1177                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1178                                else:
1179                                        message_parts.append('%s' %ubody_text)
1180                        else:
1181                                if self.DEBUG:
1182                                        print 'TD:               Filename: %s' % part.get_filename()
[22]1183
[236]1184                                message_parts.append((part.get_filename(), part))
1185
1186                return message_parts
1187               
[253]1188        def unique_attachment_names(self, message_parts):
[236]1189                renamed_parts = []
1190                attachment_names = set()
1191                for part in message_parts:
1192                       
1193                        # If not an attachment, leave it alone
1194                        if not isinstance(part, tuple):
1195                                renamed_parts.append(part)
1196                                continue
1197                               
1198                        (filename, part) = part
1199                        # Decode the filename
1200                        if filename:
1201                                filename = self.email_to_unicode(filename)                     
1202                        # If no name, use a default one
[22]1203                        else:
[236]1204                                filename = 'untitled-part'
[22]1205
[242]1206                                # Guess the extension from the content type, use non strict mode
1207                                # some additional non-standard but commonly used MIME types
1208                                # are also recognized
1209                                #
1210                                ext = mimetypes.guess_extension(part.get_content_type(), False)
[236]1211                                if not ext:
1212                                        ext = '.bin'
[22]1213
[236]1214                                filename = '%s%s' % (filename, ext)
[22]1215
[236]1216                        # Discard relative paths in attachment names
1217                        filename = filename.replace('\\', '/').replace(':', '/')
1218                        filename = os.path.basename(filename)
[22]1219
[236]1220                        # We try to normalize the filename to utf-8 NFC if we can.
1221                        # Files uploaded from OS X might be in NFD.
1222                        # Check python version and then try it
1223                        #
1224                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1225                                try:
1226                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1227                                except TypeError:
1228                                        pass
1229                                       
1230                        if self.QUOTE_ATTACHMENT_FILENAMES:
1231                                filename = urllib.quote(filename)
[100]1232
[236]1233                        # Make the filename unique for this ticket
1234                        num = 0
1235                        unique_filename = filename
1236                        filename, ext = os.path.splitext(filename)
[134]1237
[253]1238                        while unique_filename in attachment_names or self.attachment_exists(unique_filename):
[236]1239                                num += 1
1240                                unique_filename = "%s-%s%s" % (filename, num, ext)
1241                               
1242                        if self.DEBUG:
1243                                print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
[100]1244
[236]1245                        attachment_names.add(unique_filename)
1246
1247                        renamed_parts.append((filename, unique_filename, part))
1248               
1249                return renamed_parts
1250                       
1251        def inline_part(self, part):
1252                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1253               
1254                       
[253]1255        def attachment_exists(self, filename):
[250]1256
1257                if self.DEBUG:
[253]1258                        print "TD: attachment_exists: Ticket number : %s, Filename : %s" %(self.id, filename)
[250]1259
1260                # We have no valid ticket id
1261                #
[253]1262                if not self.id:
[236]1263                        return False
[250]1264
[236]1265                try:
[253]1266                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
[236]1267                        return True
[250]1268                except attachment.ResourceNotFound:
[236]1269                        return False
1270                       
1271        def body_text(self, message_parts):
1272                body_text = []
1273               
1274                for part in message_parts:
1275                        # Plain text part, append it
1276                        if not isinstance(part, tuple):
1277                                body_text.extend(part.strip().splitlines())
1278                                body_text.append("")
1279                                continue
1280                               
1281                        (original, filename, part) = part
1282                        inline = self.inline_part(part)
1283                       
1284                        if part.get_content_maintype() == 'image' and inline:
1285                                body_text.append('[[Image(%s)]]' % filename)
1286                                body_text.append("")
1287                        else:
1288                                body_text.append('[attachment:"%s"]' % filename)
1289                                body_text.append("")
1290                               
1291                body_text = '\r\n'.join(body_text)
1292                return body_text
1293
[253]1294        def notify(self, tkt, new=True, modtime=0):
[79]1295                """
1296                A wrapper for the TRAC notify function. So we can use templates
1297                """
[250]1298                if self.DRY_RUN:
1299                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1300                                return
[41]1301                try:
1302                        # create false {abs_}href properties, to trick Notify()
1303                        #
[193]1304                        if not self.VERSION == 0.11:
[192]1305                                self.env.abs_href = Href(self.get_config('project', 'url'))
1306                                self.env.href = Href(self.get_config('project', 'url'))
[22]1307
[41]1308                        tn = TicketNotifyEmail(self.env)
[213]1309
[42]1310                        if self.notify_template:
[222]1311
[221]1312                                if self.VERSION == 0.11:
[222]1313
[221]1314                                        from trac.web.chrome import Chrome
[222]1315
1316                                        if self.notify_template_update and not new:
1317                                                tn.template_name = self.notify_template_update
1318                                        else:
1319                                                tn.template_name = self.notify_template
1320
[221]1321                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1322                                               
1323                                else:
[222]1324
[221]1325                                        tn.template_name = self.notify_template;
[42]1326
[77]1327                        tn.notify(tkt, new, modtime)
[41]1328
1329                except Exception, e:
[253]1330                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
[41]1331
[253]1332        def html_mailto_link(self, subject, body):
1333                """
1334                This function returns a HTML mailto tag with the ticket id and author email address
1335                """
[72]1336                if not self.author:
[143]1337                        author = self.email_addr
[22]1338                else:   
[142]1339                        author = self.author
[22]1340
[253]1341                # use urllib to escape the chars
[22]1342                #
[74]1343                str = 'mailto:%s?Subject=%s&Cc=%s' %(
1344                       urllib.quote(self.email_addr),
[253]1345                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
[74]1346                           urllib.quote(self.MAILTO_CC)
1347                           )
1348
[213]1349                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]1350                return str
1351
[253]1352        def attachments(self, message_parts, update=False):
[79]1353                '''
1354                save any attachments as files in the ticket's directory
1355                '''
[237]1356                if self.DRY_RUN:
[250]1357                        print "DRY_RUN: no attachments saved"
[237]1358                        return ''
1359
[22]1360                count = 0
[152]1361
1362                # Get Maxium attachment size
1363                #
1364                max_size = int(self.get_config('attachment', 'max_size'))
[153]1365                status   = ''
[236]1366               
1367                for part in message_parts:
1368                        # Skip body parts
1369                        if not isinstance(part, tuple):
[22]1370                                continue
[236]1371                               
1372                        (original, filename, part) = part
[48]1373                        #
[172]1374                        # Must be tuneables HvB
1375                        #
[236]1376                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
[22]1377                        text = part.get_payload(decode=1)
1378                        if not text:
1379                                text = '(None)'
[48]1380                        fd.write(text)
1381                        fd.close()
[22]1382
[153]1383                        # get the file_size
[22]1384                        #
[48]1385                        stats = os.lstat(path)
[153]1386                        file_size = stats[stat.ST_SIZE]
[22]1387
[152]1388                        # Check if the attachment size is allowed
1389                        #
[153]1390                        if (max_size != -1) and (file_size > max_size):
1391                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
[236]1392                                        %(status, original, file_size, max_size)
[152]1393
1394                                os.unlink(path)
1395                                continue
1396                        else:
1397                                count = count + 1
1398                                       
[172]1399                        # Insert the attachment
[73]1400                        #
[242]1401                        fd = open(path, 'rb')
[253]1402                        att = attachment.Attachment(self.env, 'ticket', self.id)
[73]1403
[172]1404                        # This will break the ticket_update system, the body_text is vaporized
1405                        # ;-(
1406                        #
1407                        if not update:
1408                                att.author = self.author
1409                                att.description = self.email_to_unicode('Added by email2trac')
[73]1410
[236]1411                        att.insert(filename, fd, file_size)
[172]1412                        #except  util.TracError, detail:
1413                        #       print detail
[73]1414
[103]1415                        # Remove the created temporary filename
1416                        #
[172]1417                        fd.close()
[103]1418                        os.unlink(path)
1419
[77]1420                # Return how many attachments
1421                #
[153]1422                status = 'This message has %d attachment(s)\n%s' %(count, status)
1423                return status
[22]1424
[77]1425
[22]1426def mkdir_p(dir, mode):
1427        '''do a mkdir -p'''
1428
1429        arr = string.split(dir, '/')
1430        path = ''
1431        for part in arr:
1432                path = '%s/%s' % (path, part)
1433                try:
1434                        stats = os.stat(path)
1435                except OSError:
1436                        os.mkdir(path, mode)
1437
1438def ReadConfig(file, name):
1439        """
1440        Parse the config file
1441        """
1442        if not os.path.isfile(file):
[79]1443                print 'File %s does not exist' %file
[22]1444                sys.exit(1)
1445
[199]1446        config = trac_config.Configuration(file)
[22]1447
1448        # Use given project name else use defaults
1449        #
1450        if name:
[199]1451                sections = config.sections()
1452                if not name in sections:
[79]1453                        print "Not a valid project name: %s" %name
[199]1454                        print "Valid names: %s" %sections
[22]1455                        sys.exit(1)
1456
1457                project =  dict()
[199]1458                for option, value in  config.options(name):
1459                        project[option] = value
[22]1460
1461        else:
[217]1462                # use some trac internales to get the defaults
1463                #
1464                project = config.parser.defaults()
[22]1465
1466        return project
1467
[87]1468
[22]1469if __name__ == '__main__':
1470        # Default config file
1471        #
[24]1472        configfile = '@email2trac_conf@'
[22]1473        project = ''
1474        component = ''
[202]1475        ticket_prefix = 'default'
[204]1476        dry_run = None
[202]1477
[87]1478        ENABLE_SYSLOG = 0
[201]1479
[204]1480
[202]1481        SHORT_OPT = 'chf:np:t:'
1482        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
[201]1483
[22]1484        try:
[201]1485                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
[22]1486        except getopt.error,detail:
1487                print __doc__
1488                print detail
1489                sys.exit(1)
[87]1490       
[22]1491        project_name = None
1492        for opt,value in opts:
1493                if opt in [ '-h', '--help']:
1494                        print __doc__
1495                        sys.exit(0)
1496                elif opt in ['-c', '--component']:
1497                        component = value
1498                elif opt in ['-f', '--file']:
1499                        configfile = value
[201]1500                elif opt in ['-n', '--dry-run']:
[204]1501                        dry_run = True
[22]1502                elif opt in ['-p', '--project']:
1503                        project_name = value
[202]1504                elif opt in ['-t', '--ticket_prefix']:
1505                        ticket_prefix = value
[87]1506       
[22]1507        settings = ReadConfig(configfile, project_name)
1508        if not settings.has_key('project'):
1509                print __doc__
[79]1510                print 'No Trac project is defined in the email2trac config file.'
[22]1511                sys.exit(1)
[87]1512       
[22]1513        if component:
1514                settings['component'] = component
[202]1515
1516        # The default prefix for ticket values in email2trac.conf
1517        #
1518        settings['ticket_prefix'] = ticket_prefix
[206]1519        settings['dry_run'] = dry_run
[87]1520       
[22]1521        if settings.has_key('trac_version'):
[189]1522                version = settings['trac_version']
[22]1523        else:
1524                version = trac_default_version
1525
[189]1526
[22]1527        #debug HvB
1528        #print settings
[189]1529
[87]1530        try:
[189]1531                if version == '0.9':
[87]1532                        from trac import attachment
1533                        from trac.env import Environment
1534                        from trac.ticket import Ticket
1535                        from trac.web.href import Href
1536                        from trac import util
1537                        from trac.Notify import TicketNotifyEmail
[189]1538                elif version == '0.10':
[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
[139]1544                        #
1545                        # return  util.text.to_unicode(str)
1546                        #
[87]1547                        # see http://projects.edgewall.com/trac/changeset/2799
1548                        from trac.ticket.notification import TicketNotifyEmail
[199]1549                        from trac import config as trac_config
[189]1550                elif version == '0.11':
[182]1551                        from trac import attachment
1552                        from trac.env import Environment
1553                        from trac.ticket import Ticket
1554                        from trac.web.href import Href
[199]1555                        from trac import config as trac_config
[182]1556                        from trac import util
[260]1557
1558
[182]1559                        #
1560                        # return  util.text.to_unicode(str)
1561                        #
1562                        # see http://projects.edgewall.com/trac/changeset/2799
1563                        from trac.ticket.notification import TicketNotifyEmail
[189]1564                else:
1565                        print 'TRAC version %s is not supported' %version
1566                        sys.exit(1)
1567                       
1568                if settings.has_key('enable_syslog'):
[190]1569                        if SYSLOG_AVAILABLE:
1570                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
[182]1571
[87]1572                env = Environment(settings['project'], create=0)
[206]1573                tktparser = TicketEmailParser(env, settings, float(version))
[87]1574                tktparser.parse(sys.stdin)
[22]1575
[87]1576        # Catch all errors ans log to SYSLOG if we have enabled this
1577        # else stdout
1578        #
1579        except Exception, error:
1580                if ENABLE_SYSLOG:
1581                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
[187]1582
[87]1583                        etype, evalue, etb = sys.exc_info()
1584                        for e in traceback.format_exception(etype, evalue, etb):
1585                                syslog.syslog(e)
[187]1586
[87]1587                        syslog.closelog()
1588                else:
1589                        traceback.print_exc()
[22]1590
[97]1591                if m:
[98]1592                        tktparser.save_email_for_debug(m, True)
[97]1593
[249]1594                sys.exit(1)
[22]1595# EOB
Note: See TracBrowser for help on using the repository browser.