source: trunk/email2trac.py.in @ 353

Last change on this file since 353 was 353, checked in by bas, 12 years ago

email2trac.py.in:

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