source: trunk/email2trac.py.in @ 334

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

changed some debugs to verbose messages

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