source: trunk/email2trac.py.in @ 283

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

email2trac.py.in:

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