source: trunk/email2trac.py.in @ 354

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

Some modification of ticket subject updates, see #188

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 46.7 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 354 2010-04-15 12:09:29Z 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. Here is how the subject is matched
1234                                #   Re: <subject>
1235                                #   Re: (<Mail list label>:)+ <subject>
1236                                #
1237                                # So we must have column 3
1238                       
1239                               
1240                                SUBJECT_RE = re.compile(r'^(RE|AW|VS|SV):(.*:)*\s*(.*)', re.IGNORECASE)
1241                                result = SUBJECT_RE.search(subject)
1242
1243                                if result:
1244                                        # This is a reply
1245                                        orig_subject = result.group(3)
1246                                        if self.DEBUG:
1247                                                print 'TD: subject search string: %s' %(orig_subject)
1248
1249                                        cursor = self.db.cursor()
1250
1251                                        summaries = [orig_subject, '%%: %s' % orig_subject]
1252
1253                                        # hard-code the time to 30 days for now. Can easily be
1254                                        # made configurable
1255                                        lookback = int(time.mktime(time.gmtime())) - 2592000
1256
1257                                        matched_id = 0
1258                                        for summary in summaries:
1259                                                if self.DEBUG:
1260                                                        print 'TD: Looking for summary matching: "%s"' % summary
1261                                                sql = """SELECT id FROM ticket
1262                                                                                  WHERE changetime >= %s
1263                                                                                        AND summary LIKE %s
1264                                                                                        ORDER BY changetime DESC"""
1265                                                cursor.execute(sql, [lookback, summary.strip()])
1266
1267                                                for row in cursor:
1268                                                        (matched_id,) = row
1269                                                        if self.DEBUG:
1270                                                                print 'TD: Found matching ticket id: %d' % matched_id
1271                                                        break
1272
1273                                                if matched_id:
1274                                                        break
1275
1276                                        if matched_id:
1277                                                # Change the id to string, as expected by ticket_update
1278                                                matched_id = '#%d:' % matched_id
1279                                                self.ticket_update(m, matched_id, spam_msg)
1280                                                return
1281
1282                                # Not a reply or no matches found, create a new ticket
1283                                self.new_ticket(m, subject, spam_msg)
1284
1285                        else:
1286                                # No update by subject, so just create a new ticket
1287                                self.new_ticket(m, subject, spam_msg)
1288
1289########## BODY TEXT functions  ###########################################################
1290
1291        def strip_signature(self, text):
1292                """
1293                Strip signature from message, inspired by Mailman software
1294                """
1295                body = []
1296                for line in text.splitlines():
1297                        if line == '-- ':
1298                                break
1299                        body.append(line)
1300
1301                return ('\n'.join(body))
1302
1303        def reflow(self, text, delsp = 0):
1304                """
1305                Reflow the message based on the format="flowed" specification (RFC 3676)
1306                """
1307                flowedlines = []
1308                quotelevel = 0
1309                prevflowed = 0
1310
1311                for line in text.splitlines():
1312                        from re import match
1313                       
1314                        # Figure out the quote level and the content of the current line
1315                        m = match('(>*)( ?)(.*)', line)
1316                        linequotelevel = len(m.group(1))
1317                        line = m.group(3)
1318
1319                        # Determine whether this line is flowed
1320                        if line and line != '-- ' and line[-1] == ' ':
1321                                flowed = 1
1322                        else:
1323                                flowed = 0
1324
1325                        if flowed and delsp and line and line[-1] == ' ':
1326                                line = line[:-1]
1327
1328                        # If the previous line is flowed, append this line to it
1329                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1330                                flowedlines[-1] += line
1331                        # Otherwise, start a new line
1332                        else:
1333                                flowedlines.append('>' * linequotelevel + line)
1334
1335                        prevflowed = flowed
1336                       
1337
1338                return '\n'.join(flowedlines)
1339
1340        def strip_quotes(self, text):
1341                """
1342                Strip quotes from message by Nicolas Mendoza
1343                """
1344                body = []
1345                for line in text.splitlines():
1346                        if line.startswith(self.EMAIL_QUOTE):
1347                                continue
1348                        body.append(line)
1349
1350                return ('\n'.join(body))
1351
1352        def inline_properties(self, text):
1353                """
1354                Parse text if we use inline keywords to set ticket fields
1355                """
1356                if self.DEBUG:
1357                        print 'TD: inline_properties function'
1358
1359                properties = dict()
1360                body = list()
1361
1362                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1363
1364                for line in text.splitlines():
1365                        match = INLINE_EXP.match(line)
1366                        if match:
1367                                keyword, value = match.groups()
1368                                self.properties[keyword] = value.strip()
1369                                if self.DEBUG:
1370                                        print "TD: inline properties: %s : %s" %(keyword,value)
1371                        else:
1372                                body.append(line)
1373                               
1374                return '\n'.join(body)
1375
1376
1377        def wrap_text(self, text, replace_whitespace = False):
1378                """
1379                Will break a lines longer then given length into several small
1380                lines of size given length
1381                """
1382                import textwrap
1383
1384                LINESEPARATOR = '\n'
1385                reformat = ''
1386
1387                for s in text.split(LINESEPARATOR):
1388                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1389                        if tmp:
1390                                reformat = '%s\n%s' %(reformat,tmp)
1391                        else:
1392                                reformat = '%s\n' %reformat
1393
1394                return reformat
1395
1396                # Python2.4 and higher
1397                #
1398                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1399                #
1400
1401########## EMAIL attachements functions ###########################################################
1402
1403        def inline_part(self, part):
1404                """
1405                """
1406                if self.VERBOSE:
1407                        print "VB: inline_part()"
1408
1409                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1410
1411        def get_message_parts(self, msg):
1412                """
1413                parses the email message and returns a list of body parts and attachments
1414                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1415                """
1416                if self.VERBOSE:
1417                        print "VB: get_message_parts()"
1418
1419                message_parts = list()
1420       
1421                ALTERNATIVE_MULTIPART = False
1422
1423                for part in msg.walk():
1424                        if self.DEBUG:
1425                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
1426                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
1427
1428                        ## Check content type
1429                        #
1430                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
1431
1432                                if self.DEBUG:
1433                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
1434
1435                                continue
1436
1437                        ## Catch some mulitpart execptions
1438                        #
1439                        if part.get_content_type() == 'multipart/alternative':
1440                                ALTERNATIVE_MULTIPART = True
1441                                continue
1442
1443                        ## Skip multipart containers
1444                        #
1445                        if part.get_content_maintype() == 'multipart':
1446                                if self.DEBUG:
1447                                        print "TD: Skipping multipart container"
1448                                continue
1449                       
1450                        ## 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"
1451                        #
1452                        inline = self.inline_part(part)
1453
1454                        ## Drop HTML message
1455                        #
1456                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1457                                if part.get_content_type() == 'text/html':
1458                                        if self.DEBUG:
1459                                                print "TD: Skipping alternative HTML message"
1460
1461                                        ALTERNATIVE_MULTIPART = False
1462                                        continue
1463
1464                        ## Inline text parts are where the body is
1465                        #
1466                        if part.get_content_type() == 'text/plain' and inline:
1467                                if self.DEBUG:
1468                                        print 'TD:               Inline body part'
1469
1470                                # Try to decode, if fails then do not decode
1471                                #
1472                                body_text = part.get_payload(decode=1)
1473                                if not body_text:                       
1474                                        body_text = part.get_payload(decode=0)
1475
1476                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1477                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1478
1479                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1480                                        body_text = self.reflow(body_text, delsp == 'yes')
1481       
1482                                if self.STRIP_SIGNATURE:
1483                                        body_text = self.strip_signature(body_text)
1484
1485                                if self.STRIP_QUOTES:
1486                                        body_text = self.strip_quotes(body_text)
1487
1488                                if self.INLINE_PROPERTIES:
1489                                        body_text = self.inline_properties(body_text)
1490
1491                                if self.USE_TEXTWRAP:
1492                                        body_text = self.wrap_text(body_text)
1493
1494                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
1495                                #
1496                                charset = part.get_content_charset()
1497                                if not charset:
1498                                        charset = 'iso-8859-15'
1499
1500                                try:
1501                                        ubody_text = unicode(body_text, charset)
1502
1503                                except UnicodeError, detail:
1504                                        ubody_text = unicode(body_text, 'iso-8859-15')
1505
1506                                except LookupError, detail:
1507                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1508
1509                                if self.VERBATIM_FORMAT:
1510                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1511                                else:
1512                                        message_parts.append('%s' %ubody_text)
1513                        else:
1514                                if self.DEBUG:
1515                                        try:
1516                                                print 'TD:               Filename: %s' % part.get_filename()
1517                                        except UnicodeEncodeError, detail:
1518                                                print 'TD:               Filename: Can not be printed due to non-ascii characters'
1519
1520                                ## Convert 7-bit filename to 8 bits value
1521                                #
1522                                filename = self.email_to_unicode(part.get_filename())
1523                                message_parts.append((filename, part))
1524
1525                return message_parts
1526               
1527        def unique_attachment_names(self, message_parts):
1528                """
1529                """
1530                renamed_parts = []
1531                attachment_names = set()
1532
1533                for item in message_parts:
1534                       
1535                        ## If not an attachment, leave it alone
1536                        #
1537                        if not isinstance(item, tuple):
1538                                renamed_parts.append(item)
1539                                continue
1540                               
1541                        (filename, part) = item
1542
1543                        ## If no filename, use a default one
1544                        #
1545                        if not filename:
1546                                filename = 'untitled-part'
1547
1548                                # Guess the extension from the content type, use non strict mode
1549                                # some additional non-standard but commonly used MIME types
1550                                # are also recognized
1551                                #
1552                                ext = mimetypes.guess_extension(part.get_content_type(), False)
1553                                if not ext:
1554                                        ext = '.bin'
1555
1556                                filename = '%s%s' % (filename, ext)
1557
1558                        ## Discard relative paths for windows/unix in attachment names
1559                        #
1560                        #filename = filename.replace('\\', '/').replace(':', '/')
1561                        filename = filename.replace('\\', '_')
1562                        filename = filename.replace('/', '_')
1563
1564                        #
1565                        # We try to normalize the filename to utf-8 NFC if we can.
1566                        # Files uploaded from OS X might be in NFD.
1567                        # Check python version and then try it
1568                        #
1569                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1570                        #       try:
1571                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1572                        #       except TypeError:
1573                        #               pass
1574
1575                        # Make the filename unique for this ticket
1576                        num = 0
1577                        unique_filename = filename
1578                        dummy_filename, ext = os.path.splitext(filename)
1579
1580                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
1581                                num += 1
1582                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
1583                               
1584                        if self.DEBUG:
1585                                try:
1586                                        print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1587                                except UnicodeEncodeError, detail:
1588                                        print 'Filename can not be printed due to non-ascii characters'
1589
1590                        attachment_names.add(unique_filename)
1591
1592                        renamed_parts.append((filename, unique_filename, part))
1593       
1594                return renamed_parts
1595                       
1596                       
1597        def attachment_exists(self, filename):
1598
1599                if self.DEBUG:
1600                        s = 'TD: attachment already exists: Ticket id : '
1601                        try:
1602                                print "%s%s, Filename : %s" %(s, self.id, filename)
1603                        except UnicodeEncodeError, detail:
1604                                print "%s%s, Filename : Can not be printed due to non-ascii characters" %(s, self.id)
1605
1606                # We have no valid ticket id
1607                #
1608                if not self.id:
1609                        return False
1610
1611                try:
1612                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
1613                        return True
1614                except attachment.ResourceNotFound:
1615                        return False
1616
1617########## TRAC Ticket Text ###########################################################
1618                       
1619        def body_text(self, message_parts):
1620                body_text = []
1621               
1622                for part in message_parts:
1623                        # Plain text part, append it
1624                        if not isinstance(part, tuple):
1625                                body_text.extend(part.strip().splitlines())
1626                                body_text.append("")
1627                                continue
1628                               
1629                        (original, filename, part) = part
1630                        inline = self.inline_part(part)
1631                       
1632                        if part.get_content_maintype() == 'image' and inline:
1633                                body_text.append('[[Image(%s)]]' % filename)
1634                                body_text.append("")
1635                        else:
1636                                body_text.append('[attachment:"%s"]' % filename)
1637                                body_text.append("")
1638                               
1639                body_text = '\r\n'.join(body_text)
1640                return body_text
1641
1642        def html_mailto_link(self, subject):
1643                """
1644                This function returns a HTML mailto tag with the ticket id and author email address
1645                """
1646                if not self.author:
1647                        author = self.email_addr
1648                else:   
1649                        author = self.author
1650
1651                # use urllib to escape the chars
1652                #
1653                s = 'mailto:%s?Subject=%s&Cc=%s' %(
1654                       urllib.quote(self.email_addr),
1655                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1656                           urllib.quote(self.MAILTO_CC)
1657                           )
1658
1659                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)
1660                return s
1661
1662########## TRAC notify section ###########################################################
1663
1664        def notify(self, tkt, new=True, modtime=0):
1665                """
1666                A wrapper for the TRAC notify function. So we can use templates
1667                """
1668                if self.DRY_RUN:
1669                                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
1670                                return
1671                try:
1672                        # create false {abs_}href properties, to trick Notify()
1673                        #
1674                        if not self.VERSION == 0.11:
1675                                self.env.abs_href = Href(self.get_config('project', 'url'))
1676                                self.env.href = Href(self.get_config('project', 'url'))
1677
1678                        tn = TicketNotifyEmail(self.env)
1679
1680                        if self.notify_template:
1681
1682                                if self.VERSION == 0.11:
1683
1684                                        from trac.web.chrome import Chrome
1685
1686                                        if self.notify_template_update and not new:
1687                                                tn.template_name = self.notify_template_update
1688                                        else:
1689                                                tn.template_name = self.notify_template
1690
1691                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1692                                               
1693                                else:
1694
1695                                        tn.template_name = self.notify_template;
1696
1697                        tn.notify(tkt, new, modtime)
1698
1699                except Exception, e:
1700                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
1701
1702
1703
1704########## Parse Config File  ###########################################################
1705
1706def ReadConfig(file, name):
1707        """
1708        Parse the config file
1709        """
1710        if not os.path.isfile(file):
1711                print 'File %s does not exist' %file
1712                sys.exit(1)
1713
1714        config = trac_config.Configuration(file)
1715
1716        # Use given project name else use defaults
1717        #
1718        if name:
1719                sections = config.sections()
1720                if not name in sections:
1721                        print "Not a valid project name: %s" %name
1722                        print "Valid names: %s" %sections
1723                        sys.exit(1)
1724
1725                project =  dict()
1726                for option, value in  config.options(name):
1727                        project[option] = value
1728
1729        else:
1730                # use some trac internals to get the defaults
1731                #
1732                project = config.parser.defaults()
1733
1734        return project
1735
1736
1737if __name__ == '__main__':
1738        # Default config file
1739        #
1740        configfile = '@email2trac_conf@'
1741        project = ''
1742        component = ''
1743        ticket_prefix = 'default'
1744        dry_run = None
1745        verbose = None
1746
1747        ENABLE_SYSLOG = 0
1748
1749
1750        SHORT_OPT = 'chf:np:t:v'
1751        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
1752
1753        try:
1754                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1755        except getopt.error,detail:
1756                print __doc__
1757                print detail
1758                sys.exit(1)
1759       
1760        project_name = None
1761        for opt,value in opts:
1762                if opt in [ '-h', '--help']:
1763                        print __doc__
1764                        sys.exit(0)
1765                elif opt in ['-c', '--component']:
1766                        component = value
1767                elif opt in ['-f', '--file']:
1768                        configfile = value
1769                elif opt in ['-n', '--dry-run']:
1770                        dry_run = True
1771                elif opt in ['-p', '--project']:
1772                        project_name = value
1773                elif opt in ['-t', '--ticket_prefix']:
1774                        ticket_prefix = value
1775                elif opt in ['-v', '--version']:
1776                        verbose = True
1777       
1778        settings = ReadConfig(configfile, project_name)
1779        if not settings.has_key('project'):
1780                print __doc__
1781                print 'No Trac project is defined in the email2trac config file.'
1782                sys.exit(1)
1783       
1784        if component:
1785                settings['component'] = component
1786
1787        # The default prefix for ticket values in email2trac.conf
1788        #
1789        settings['ticket_prefix'] = ticket_prefix
1790        settings['dry_run'] = dry_run
1791        settings['verbose'] = verbose
1792       
1793        if settings.has_key('trac_version'):
1794                version = settings['trac_version']
1795        else:
1796                version = trac_default_version
1797
1798
1799        #debug HvB
1800        #print settings
1801
1802        try:
1803                if version == '0.9':
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                        from trac.Notify import TicketNotifyEmail
1810                elif version == '0.10':
1811                        from trac import attachment
1812                        from trac.env import Environment
1813                        from trac.ticket import Ticket
1814                        from trac.web.href import Href
1815                        from trac import util
1816                        #
1817                        # return  util.text.to_unicode(str)
1818                        #
1819                        # see http://projects.edgewall.com/trac/changeset/2799
1820                        from trac.ticket.notification import TicketNotifyEmail
1821                        from trac import config as trac_config
1822                elif version == '0.11':
1823                        from trac import attachment
1824                        from trac.env import Environment
1825                        from trac.ticket import Ticket
1826                        from trac.web.href import Href
1827                        from trac import config as trac_config
1828                        from trac import util
1829
1830
1831                        #
1832                        # return  util.text.to_unicode(str)
1833                        #
1834                        # see http://projects.edgewall.com/trac/changeset/2799
1835                        from trac.ticket.notification import TicketNotifyEmail
1836                else:
1837                        print 'TRAC version %s is not supported' %version
1838                        sys.exit(1)
1839                       
1840                if settings.has_key('enable_syslog'):
1841                        if SYSLOG_AVAILABLE:
1842                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1843
1844
1845                # Must be set before environment is created
1846                #
1847                if settings.has_key('python_egg_cache'):
1848                        python_egg_cache = str(settings['python_egg_cache'])
1849                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
1850
1851                env = Environment(settings['project'], create=0)
1852
1853                tktparser = TicketEmailParser(env, settings, float(version))
1854                tktparser.parse(sys.stdin)
1855
1856        # Catch all errors ans log to SYSLOG if we have enabled this
1857        # else stdout
1858        #
1859        except Exception, error:
1860                if ENABLE_SYSLOG:
1861                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1862
1863                        etype, evalue, etb = sys.exc_info()
1864                        for e in traceback.format_exception(etype, evalue, etb):
1865                                syslog.syslog(e)
1866
1867                        syslog.closelog()
1868                else:
1869                        traceback.print_exc()
1870
1871                if m:
1872                        tktparser.save_email_for_debug(m, True)
1873
1874                sys.exit(1)
1875# EOB
Note: See TracBrowser for help on using the repository browser.