source: trunk/email2trac.py.in @ 350

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

email2trac.py.in:

  • added some code that will replace the / and \ chars for attachments filenames with _. These chars are forbidden for Unix/Windows? environments. Alse when something goes wrong with an attachment. The error is displayed in the changelog of the ticket, see #196
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 44.9 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 348 2010-04-12 16:04:24Z 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                        ## skip some fields like resolution
846                        #
847                        if name in [ 'resolution' ]:
848                                continue
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                ## Set status ticket
886                #
887                ticket['status'] = 'new'
888
889
890
891        def new_ticket(self, msg, subject, spam, set_fields = None):
892                """
893                Create a new ticket
894                """
895                if self.DEBUG:
896                        print "TD: new_ticket"
897
898                tkt = Ticket(self.env)
899
900                self.set_reply_fields(tkt, msg)
901
902                self.set_ticket_fields(tkt)
903
904                # Old style setting for component, will be removed
905                #
906                if spam:
907                        tkt['component'] = 'Spam'
908
909                elif self.parameters.has_key('component'):
910                        tkt['component'] = self.parameters['component']
911
912                if not msg['Subject']:
913                        tkt['summary'] = u'(No subject)'
914                else:
915                        tkt['summary'] = subject
916
917
918                if set_fields:
919                        rest, keywords = string.split(set_fields, '?')
920
921                        if keywords:
922                                update_fields = self.str_to_dict(keywords)
923                                self.update_ticket_fields(tkt, update_fields)
924
925                # produce e-mail like header
926                #
927                head = ''
928                if self.EMAIL_HEADER > 0:
929                        head = self.email_header_txt(msg)
930
931                message_parts = self.get_message_parts(msg)
932
933                # Must we update some ticket fields properties via body_text
934                #
935                if self.properties:
936                                self.update_ticket_fields(tkt, self.properties)
937
938                if self.DEBUG:
939                        print 'TD: self.get_message_parts ',
940                        print message_parts
941
942                message_parts = self.unique_attachment_names(message_parts)
943                if self.DEBUG:
944                        print 'TD: self.unique_attachment_names',
945                        print message_parts
946               
947                if self.EMAIL_HEADER > 0:
948                        message_parts.insert(0, self.email_header_txt(msg))
949                       
950                body_text = self.body_text(message_parts)
951
952                tkt['description'] = body_text
953
954                #when = int(time.time())
955                #
956                utc = UTC()
957                when = datetime.now(utc)
958
959                if not self.DRY_RUN:
960                        self.id = tkt.insert()
961       
962                changed = False
963                comment = ''
964
965                # some routines in trac are dependend on ticket id     
966                # like alternate notify template
967                #
968                if self.notify_template:
969                        tkt['id'] = self.id
970                        changed = True
971
972                ## Rewrite the description if we have mailto enabled
973                #
974                if self.MAILTO:
975                        changed = True
976                        comment = u'\nadded mailto line\n'
977                        mailto = self.html_mailto_link( m['Subject'])
978
979                        tkt['description'] = u'%s\r\n%s%s\r\n' \
980                                %(head, mailto, body_text)
981       
982                ## Save the attachments to the ticket   
983                #
984                error_with_attachments =  self.attach_attachments(message_parts)
985
986                if error_with_attachments:
987                        changed = True
988                        comment = '%s\n%s\n' %(comment, error_with_attachments)
989
990                if changed:
991                        if self.DRY_RUN:
992                                print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
993                        else:
994                                tkt.save_changes(tkt['reporter'], comment)
995                                #print tkt.get_changelog(self.db, when)
996
997                if self.notification and not spam:
998                        self.notify(tkt, True)
999
1000
1001        def attach_attachments(self, message_parts, update=False):
1002                '''
1003                save any attachments as files in the ticket's directory
1004                '''
1005                if self.VERBOSE:
1006                        print "VB: attach_attachments()"
1007
1008                if self.DRY_RUN:
1009                        print "DRY_RUN: no attachments attached to tickets"
1010                        return ''
1011
1012                count = 0
1013
1014                # Get Maxium attachment size
1015                #
1016                max_size = int(self.get_config('attachment', 'max_size'))
1017                status   = None
1018               
1019                for item in message_parts:
1020                        # Skip body parts
1021                        if not isinstance(item, tuple):
1022                                continue
1023                               
1024                        (original, filename, part) = item
1025                        #
1026                        # Must be tuneables HvB
1027                        #
1028                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
1029                        text = part.get_payload(decode=1)
1030                        if not text:
1031                                text = '(None)'
1032                        fd.write(text)
1033                        fd.close()
1034
1035                        # get the file_size
1036                        #
1037                        stats = os.lstat(path)
1038                        file_size = stats[stat.ST_SIZE]
1039
1040                        # Check if the attachment size is allowed
1041                        #
1042                        if (max_size != -1) and (file_size > max_size):
1043                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1044                                        %(status, original, file_size, max_size)
1045
1046                                os.unlink(path)
1047                                continue
1048                        else:
1049                                count = count + 1
1050                                       
1051                        # Insert the attachment
1052                        #
1053                        fd = open(path, 'rb')
1054                        att = attachment.Attachment(self.env, 'ticket', self.id)
1055
1056                        # This will break the ticket_update system, the body_text is vaporized
1057                        # ;-(
1058                        #
1059                        if not update:
1060                                att.author = self.author
1061                                att.description = self.email_to_unicode('Added by email2trac')
1062
1063                        try:
1064                                att.insert(filename, fd, file_size)
1065                        except OSError, detail:
1066                                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
1067
1068                        # Remove the created temporary filename
1069                        #
1070                        fd.close()
1071                        os.unlink(path)
1072
1073                ## return error
1074                #
1075                return status
1076
1077########## Fullblog functions  ###########################################################
1078
1079        def blog(self, id):
1080                """
1081                The blog create/update function
1082                """
1083                # import the modules
1084                #
1085                from tracfullblog.core import FullBlogCore
1086                from tracfullblog.model import BlogPost, BlogComment
1087                from trac.test import Mock, MockPerm
1088
1089                # instantiate blog core
1090                blog = FullBlogCore(self.env)
1091                req = Mock(authname='anonymous', perm=MockPerm(), args={})
1092
1093                if id:
1094
1095                        # update blog
1096                        #
1097                        comment = BlogComment(self.env, id)
1098                        comment.author = self.author
1099
1100                        message_parts = self.get_message_parts(m)
1101                        comment.comment = self.body_text(message_parts)
1102
1103                        blog.create_comment(req, comment)
1104
1105                else:
1106                        # create blog
1107                        #
1108                        import time
1109                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
1110
1111                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
1112                       
1113                        post.author = self.author
1114                        post.title = self.email_to_unicode(m['Subject'])
1115
1116                        message_parts = self.get_message_parts(m)
1117                        post.body = self.body_text(message_parts)
1118                       
1119                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
1120
1121
1122
1123########## MAIN function  ###########################################################
1124
1125        def parse(self, fp):
1126                global m
1127
1128                m = email.message_from_file(fp)
1129               
1130                if not m:
1131                        if self.DEBUG:
1132                                print "TD: This is not a valid email message format"
1133                        return
1134                       
1135                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1136                try:
1137                        m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1138                except AttributeError, detail:
1139                        pass
1140
1141                if self.DEBUG > 1:        # save the entire e-mail message text
1142                        self.save_email_for_debug(m, True)
1143
1144                self.db = self.env.get_db_cnx()
1145                self.get_sender_info(m)
1146
1147                if not self.email_header_acl('white_list', self.email_addr, True):
1148                        if self.DEBUG > 1 :
1149                                print 'Message rejected : %s not in white list' %(self.email_addr)
1150                        return False
1151
1152                if self.email_header_acl('black_list', self.email_addr, False):
1153                        if self.DEBUG > 1 :
1154                                print 'Message rejected : %s in black list' %(self.email_addr)
1155                        return False
1156
1157                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
1158                        if self.DEBUG > 1 :
1159                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
1160                        return False
1161
1162                # If drop the message
1163                #
1164                if self.spam(m) == 'drop':
1165                        return False
1166
1167                elif self.spam(m) == 'spam':
1168                        spam_msg = True
1169                else:
1170                        spam_msg = False
1171
1172                if self.get_config('notification', 'smtp_enabled') in ['true']:
1173                        self.notification = 1
1174                else:
1175                        self.notification = 0
1176
1177
1178                # Check if  FullBlogPlugin is installed
1179                #
1180                blog_enabled = None
1181                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1182                        blog_enabled = True
1183
1184                if not m['Subject']:
1185                        subject  = 'No Subject'
1186                else:
1187                        subject  = self.email_to_unicode(m['Subject'])         
1188
1189                #
1190                # [hic] #1529: Re: LRZ
1191                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
1192                #
1193                TICKET_RE = re.compile(r"""
1194                        (?P<blog>blog:(?P<blog_id>\w*))
1195                        |(?P<new_fields>[#][?].*)
1196                        |(?P<reply>[#][\d]+:)
1197                        |(?P<reply_fields>[#][\d]+\?.*?:)
1198                        """, re.VERBOSE)
1199
1200                # Find out if this is a ticket or a blog
1201                #
1202                result =  TICKET_RE.search(subject)
1203
1204                if result:
1205                        if result.group('blog'):
1206                                if blog_enabled:
1207                                        self.blog(result.group('blog_id'))
1208                                else:
1209                                        if self.DEBUG:
1210                                                print "Fullblog plugin is not installed"
1211                                                return
1212
1213                        # update ticket + fields
1214                        #
1215                        if result.group('reply_fields') and self.TICKET_UPDATE:
1216                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
1217
1218                        # Update ticket
1219                        #
1220                        elif result.group('reply') and self.TICKET_UPDATE:
1221                                self.ticket_update(m, result.group('reply'), spam_msg)
1222
1223                        # New ticket + fields
1224                        #
1225                        elif result.group('new_fields'):
1226                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1227
1228                # Create ticket
1229                #
1230                else:
1231                        self.new_ticket(m, subject, spam_msg)
1232
1233########## BODY TEXT functions  ###########################################################
1234
1235        def strip_signature(self, text):
1236                """
1237                Strip signature from message, inspired by Mailman software
1238                """
1239                body = []
1240                for line in text.splitlines():
1241                        if line == '-- ':
1242                                break
1243                        body.append(line)
1244
1245                return ('\n'.join(body))
1246
1247        def reflow(self, text, delsp = 0):
1248                """
1249                Reflow the message based on the format="flowed" specification (RFC 3676)
1250                """
1251                flowedlines = []
1252                quotelevel = 0
1253                prevflowed = 0
1254
1255                for line in text.splitlines():
1256                        from re import match
1257                       
1258                        # Figure out the quote level and the content of the current line
1259                        m = match('(>*)( ?)(.*)', line)
1260                        linequotelevel = len(m.group(1))
1261                        line = m.group(3)
1262
1263                        # Determine whether this line is flowed
1264                        if line and line != '-- ' and line[-1] == ' ':
1265                                flowed = 1
1266                        else:
1267                                flowed = 0
1268
1269                        if flowed and delsp and line and line[-1] == ' ':
1270                                line = line[:-1]
1271
1272                        # If the previous line is flowed, append this line to it
1273                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1274                                flowedlines[-1] += line
1275                        # Otherwise, start a new line
1276                        else:
1277                                flowedlines.append('>' * linequotelevel + line)
1278
1279                        prevflowed = flowed
1280                       
1281
1282                return '\n'.join(flowedlines)
1283
1284        def strip_quotes(self, text):
1285                """
1286                Strip quotes from message by Nicolas Mendoza
1287                """
1288                body = []
1289                for line in text.splitlines():
1290                        if line.startswith(self.EMAIL_QUOTE):
1291                                continue
1292                        body.append(line)
1293
1294                return ('\n'.join(body))
1295
1296        def inline_properties(self, text):
1297                """
1298                Parse text if we use inline keywords to set ticket fields
1299                """
1300                if self.DEBUG:
1301                        print 'TD: inline_properties function'
1302
1303                properties = dict()
1304                body = list()
1305
1306                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1307
1308                for line in text.splitlines():
1309                        match = INLINE_EXP.match(line)
1310                        if match:
1311                                keyword, value = match.groups()
1312                                self.properties[keyword] = value.strip()
1313                                if self.DEBUG:
1314                                        print "TD: inline properties: %s : %s" %(keyword,value)
1315                        else:
1316                                body.append(line)
1317                               
1318                return '\n'.join(body)
1319
1320
1321        def wrap_text(self, text, replace_whitespace = False):
1322                """
1323                Will break a lines longer then given length into several small
1324                lines of size given length
1325                """
1326                import textwrap
1327
1328                LINESEPARATOR = '\n'
1329                reformat = ''
1330
1331                for s in text.split(LINESEPARATOR):
1332                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1333                        if tmp:
1334                                reformat = '%s\n%s' %(reformat,tmp)
1335                        else:
1336                                reformat = '%s\n' %reformat
1337
1338                return reformat
1339
1340                # Python2.4 and higher
1341                #
1342                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1343                #
1344
1345########## EMAIL attachements functions ###########################################################
1346
1347        def inline_part(self, part):
1348                """
1349                """
1350                if self.VERBOSE:
1351                        print "VB: inline_part()"
1352
1353                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1354
1355        def get_message_parts(self, msg):
1356                """
1357                parses the email message and returns a list of body parts and attachments
1358                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1359                """
1360                if self.VERBOSE:
1361                        print "VB: get_message_parts()"
1362
1363                message_parts = list()
1364       
1365                ALTERNATIVE_MULTIPART = False
1366
1367                for part in msg.walk():
1368                        if self.DEBUG:
1369                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
1370                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
1371
1372                        ## Check content type
1373                        #
1374                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
1375
1376                                if self.DEBUG:
1377                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
1378
1379                                continue
1380
1381                        ## Catch some mulitpart execptions
1382                        #
1383                        if part.get_content_type() == 'multipart/alternative':
1384                                ALTERNATIVE_MULTIPART = True
1385                                continue
1386
1387                        ## Skip multipart containers
1388                        #
1389                        if part.get_content_maintype() == 'multipart':
1390                                if self.DEBUG:
1391                                        print "TD: Skipping multipart container"
1392                                continue
1393                       
1394                        ## 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"
1395                        #
1396                        inline = self.inline_part(part)
1397
1398                        ## Drop HTML message
1399                        #
1400                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1401                                if part.get_content_type() == 'text/html':
1402                                        if self.DEBUG:
1403                                                print "TD: Skipping alternative HTML message"
1404
1405                                        ALTERNATIVE_MULTIPART = False
1406                                        continue
1407
1408                        ## Inline text parts are where the body is
1409                        #
1410                        if part.get_content_type() == 'text/plain' and inline:
1411                                if self.DEBUG:
1412                                        print 'TD:               Inline body part'
1413
1414                                # Try to decode, if fails then do not decode
1415                                #
1416                                body_text = part.get_payload(decode=1)
1417                                if not body_text:                       
1418                                        body_text = part.get_payload(decode=0)
1419
1420                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1421                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1422
1423                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1424                                        body_text = self.reflow(body_text, delsp == 'yes')
1425       
1426                                if self.STRIP_SIGNATURE:
1427                                        body_text = self.strip_signature(body_text)
1428
1429                                if self.STRIP_QUOTES:
1430                                        body_text = self.strip_quotes(body_text)
1431
1432                                if self.INLINE_PROPERTIES:
1433                                        body_text = self.inline_properties(body_text)
1434
1435                                if self.USE_TEXTWRAP:
1436                                        body_text = self.wrap_text(body_text)
1437
1438                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
1439                                #
1440                                charset = part.get_content_charset()
1441                                if not charset:
1442                                        charset = 'iso-8859-15'
1443
1444                                try:
1445                                        ubody_text = unicode(body_text, charset)
1446
1447                                except UnicodeError, detail:
1448                                        ubody_text = unicode(body_text, 'iso-8859-15')
1449
1450                                except LookupError, detail:
1451                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1452
1453                                if self.VERBATIM_FORMAT:
1454                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1455                                else:
1456                                        message_parts.append('%s' %ubody_text)
1457                        else:
1458                                if self.DEBUG:
1459                                        try:
1460                                                print 'TD:               Filename: %s' % part.get_filename()
1461                                        except UnicodeEncodeError, detail:
1462                                                print 'TD:               Filename: Can not be printed due to non-ascii characters'
1463
1464                                ## Convert 7-bit filename to 8 bits value
1465                                #
1466                                filename = self.email_to_unicode(part.get_filename())
1467                                message_parts.append((filename, part))
1468
1469                return message_parts
1470               
1471        def unique_attachment_names(self, message_parts):
1472                """
1473                """
1474                renamed_parts = []
1475                attachment_names = set()
1476
1477                for item in message_parts:
1478                       
1479                        ## If not an attachment, leave it alone
1480                        #
1481                        if not isinstance(item, tuple):
1482                                renamed_parts.append(item)
1483                                continue
1484                               
1485                        (filename, part) = item
1486
1487                        ## If no filename, use a default one
1488                        #
1489                        if not filename:
1490                                filename = 'untitled-part'
1491
1492                                # Guess the extension from the content type, use non strict mode
1493                                # some additional non-standard but commonly used MIME types
1494                                # are also recognized
1495                                #
1496                                ext = mimetypes.guess_extension(part.get_content_type(), False)
1497                                if not ext:
1498                                        ext = '.bin'
1499
1500                                filename = '%s%s' % (filename, ext)
1501
1502                        ## Discard relative paths for windows/unix in attachment names
1503                        #
1504                        #filename = filename.replace('\\', '/').replace(':', '/')
1505                        filename = filename.replace('\\', '_')
1506                        filename = filename.replace('/', '_')
1507
1508                        #
1509                        # We try to normalize the filename to utf-8 NFC if we can.
1510                        # Files uploaded from OS X might be in NFD.
1511                        # Check python version and then try it
1512                        #
1513                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1514                        #       try:
1515                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1516                        #       except TypeError:
1517                        #               pass
1518
1519                        # Make the filename unique for this ticket
1520                        num = 0
1521                        unique_filename = filename
1522                        dummy_filename, ext = os.path.splitext(filename)
1523
1524                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
1525                                num += 1
1526                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
1527                               
1528                        if self.DEBUG:
1529                                try:
1530                                        print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1531                                except UnicodeEncodeError, detail:
1532                                        print 'Filename can not be printed due to non-ascii characters'
1533
1534                        attachment_names.add(unique_filename)
1535
1536                        renamed_parts.append((filename, unique_filename, part))
1537       
1538                return renamed_parts
1539                       
1540                       
1541        def attachment_exists(self, filename):
1542
1543                if self.DEBUG:
1544                        s = 'TD: attachment already exists: Ticket id : '
1545                        try:
1546                                print "%s%s, Filename : %s" %(s, self.id, filename)
1547                        except UnicodeEncodeError, detail:
1548                                print "%s%s, Filename : Can not be printed due to non-ascii characters" %(s, self.id)
1549
1550                # We have no valid ticket id
1551                #
1552                if not self.id:
1553                        return False
1554
1555                try:
1556                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
1557                        return True
1558                except attachment.ResourceNotFound:
1559                        return False
1560
1561########## TRAC Ticket Text ###########################################################
1562                       
1563        def body_text(self, message_parts):
1564                body_text = []
1565               
1566                for part in message_parts:
1567                        # Plain text part, append it
1568                        if not isinstance(part, tuple):
1569                                body_text.extend(part.strip().splitlines())
1570                                body_text.append("")
1571                                continue
1572                               
1573                        (original, filename, part) = part
1574                        inline = self.inline_part(part)
1575                       
1576                        if part.get_content_maintype() == 'image' and inline:
1577                                body_text.append('[[Image(%s)]]' % filename)
1578                                body_text.append("")
1579                        else:
1580                                body_text.append('[attachment:"%s"]' % filename)
1581                                body_text.append("")
1582                               
1583                body_text = '\r\n'.join(body_text)
1584                return body_text
1585
1586        def html_mailto_link(self, subject):
1587                """
1588                This function returns a HTML mailto tag with the ticket id and author email address
1589                """
1590                if not self.author:
1591                        author = self.email_addr
1592                else:   
1593                        author = self.author
1594
1595                # use urllib to escape the chars
1596                #
1597                s = 'mailto:%s?Subject=%s&Cc=%s' %(
1598                       urllib.quote(self.email_addr),
1599                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1600                           urllib.quote(self.MAILTO_CC)
1601                           )
1602
1603                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)
1604                return s
1605
1606########## TRAC notify section ###########################################################
1607
1608        def notify(self, tkt, new=True, modtime=0):
1609                """
1610                A wrapper for the TRAC notify function. So we can use templates
1611                """
1612                if self.DRY_RUN:
1613                                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
1614                                return
1615                try:
1616                        # create false {abs_}href properties, to trick Notify()
1617                        #
1618                        if not self.VERSION == 0.11:
1619                                self.env.abs_href = Href(self.get_config('project', 'url'))
1620                                self.env.href = Href(self.get_config('project', 'url'))
1621
1622                        tn = TicketNotifyEmail(self.env)
1623
1624                        if self.notify_template:
1625
1626                                if self.VERSION == 0.11:
1627
1628                                        from trac.web.chrome import Chrome
1629
1630                                        if self.notify_template_update and not new:
1631                                                tn.template_name = self.notify_template_update
1632                                        else:
1633                                                tn.template_name = self.notify_template
1634
1635                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1636                                               
1637                                else:
1638
1639                                        tn.template_name = self.notify_template;
1640
1641                        tn.notify(tkt, new, modtime)
1642
1643                except Exception, e:
1644                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
1645
1646
1647
1648########## Parse Config File  ###########################################################
1649
1650def ReadConfig(file, name):
1651        """
1652        Parse the config file
1653        """
1654        if not os.path.isfile(file):
1655                print 'File %s does not exist' %file
1656                sys.exit(1)
1657
1658        config = trac_config.Configuration(file)
1659
1660        # Use given project name else use defaults
1661        #
1662        if name:
1663                sections = config.sections()
1664                if not name in sections:
1665                        print "Not a valid project name: %s" %name
1666                        print "Valid names: %s" %sections
1667                        sys.exit(1)
1668
1669                project =  dict()
1670                for option, value in  config.options(name):
1671                        project[option] = value
1672
1673        else:
1674                # use some trac internals to get the defaults
1675                #
1676                project = config.parser.defaults()
1677
1678        return project
1679
1680
1681if __name__ == '__main__':
1682        # Default config file
1683        #
1684        configfile = '@email2trac_conf@'
1685        project = ''
1686        component = ''
1687        ticket_prefix = 'default'
1688        dry_run = None
1689        verbose = None
1690
1691        ENABLE_SYSLOG = 0
1692
1693
1694        SHORT_OPT = 'chf:np:t:v'
1695        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
1696
1697        try:
1698                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1699        except getopt.error,detail:
1700                print __doc__
1701                print detail
1702                sys.exit(1)
1703       
1704        project_name = None
1705        for opt,value in opts:
1706                if opt in [ '-h', '--help']:
1707                        print __doc__
1708                        sys.exit(0)
1709                elif opt in ['-c', '--component']:
1710                        component = value
1711                elif opt in ['-f', '--file']:
1712                        configfile = value
1713                elif opt in ['-n', '--dry-run']:
1714                        dry_run = True
1715                elif opt in ['-p', '--project']:
1716                        project_name = value
1717                elif opt in ['-t', '--ticket_prefix']:
1718                        ticket_prefix = value
1719                elif opt in ['-v', '--version']:
1720                        verbose = True
1721       
1722        settings = ReadConfig(configfile, project_name)
1723        if not settings.has_key('project'):
1724                print __doc__
1725                print 'No Trac project is defined in the email2trac config file.'
1726                sys.exit(1)
1727       
1728        if component:
1729                settings['component'] = component
1730
1731        # The default prefix for ticket values in email2trac.conf
1732        #
1733        settings['ticket_prefix'] = ticket_prefix
1734        settings['dry_run'] = dry_run
1735        settings['verbose'] = verbose
1736       
1737        if settings.has_key('trac_version'):
1738                version = settings['trac_version']
1739        else:
1740                version = trac_default_version
1741
1742
1743        #debug HvB
1744        #print settings
1745
1746        try:
1747                if version == '0.9':
1748                        from trac import attachment
1749                        from trac.env import Environment
1750                        from trac.ticket import Ticket
1751                        from trac.web.href import Href
1752                        from trac import util
1753                        from trac.Notify import TicketNotifyEmail
1754                elif version == '0.10':
1755                        from trac import attachment
1756                        from trac.env import Environment
1757                        from trac.ticket import Ticket
1758                        from trac.web.href import Href
1759                        from trac import util
1760                        #
1761                        # return  util.text.to_unicode(str)
1762                        #
1763                        # see http://projects.edgewall.com/trac/changeset/2799
1764                        from trac.ticket.notification import TicketNotifyEmail
1765                        from trac import config as trac_config
1766                elif version == '0.11':
1767                        from trac import attachment
1768                        from trac.env import Environment
1769                        from trac.ticket import Ticket
1770                        from trac.web.href import Href
1771                        from trac import config as trac_config
1772                        from trac import util
1773
1774
1775                        #
1776                        # return  util.text.to_unicode(str)
1777                        #
1778                        # see http://projects.edgewall.com/trac/changeset/2799
1779                        from trac.ticket.notification import TicketNotifyEmail
1780                else:
1781                        print 'TRAC version %s is not supported' %version
1782                        sys.exit(1)
1783                       
1784                if settings.has_key('enable_syslog'):
1785                        if SYSLOG_AVAILABLE:
1786                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1787
1788
1789                # Must be set before environment is created
1790                #
1791                if settings.has_key('python_egg_cache'):
1792                        python_egg_cache = str(settings['python_egg_cache'])
1793                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
1794
1795                env = Environment(settings['project'], create=0)
1796
1797                tktparser = TicketEmailParser(env, settings, float(version))
1798                tktparser.parse(sys.stdin)
1799
1800        # Catch all errors ans log to SYSLOG if we have enabled this
1801        # else stdout
1802        #
1803        except Exception, error:
1804                if ENABLE_SYSLOG:
1805                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1806
1807                        etype, evalue, etb = sys.exc_info()
1808                        for e in traceback.format_exception(etype, evalue, etb):
1809                                syslog.syslog(e)
1810
1811                        syslog.closelog()
1812                else:
1813                        traceback.print_exc()
1814
1815                if m:
1816                        tktparser.save_email_for_debug(m, True)
1817
1818                sys.exit(1)
1819# EOB
Note: See TracBrowser for help on using the repository browser.