source: trunk/email2trac.py.in @ 288

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

email2trac.py.in:

  • workflow patch, did not work, closes #157
  • When i ticket is updated we did supply all need fields, like ticket change number, closes #156

debian/changelog:

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