source: trunk/email2trac.py.in @ 278

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

email2trac.py.in:

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