source: trunk/email2trac.py.in @ 352

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

Applied patch for ticket prefix enhancement, closes #195

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