source: trunk/email2trac.py.in @ 301

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

prevent mail loop, closes #172

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