1f1cf746897a2d9331f9713c68d3107e40c22348
[imap-fix-internaldate] / src / fix_imap_internaldate.py
1 '''
2 fix_imap_internaldate.py - Fix the INTERNALDATE field on IMAP servers
3
4 Copyright (c) 2012 Intra2net AG
5 Author: Plamen Dimitrov
6
7 This program is free software: you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
11
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 GNU General Public License for more details.
16 '''
17
18 import sys
19 import csv
20 import argparse
21 # python version handling
22 try:
23     import configparser
24 except ImportError:
25     print("This module needs python version 3 or later.")
26     sys.exit()
27 import logging
28 from mail_date_parser import MailDateParser
29 from mail_iterator import MailIterator
30 from caching_data import CachingData
31
32 def main():
33     """Interprets command arguments and initializes configuration and logger.
34         Then begins mail synchronization."""
35
36     # parse arguments
37     parser = argparse.ArgumentParser(description="Fix the INTERNALDATE field on IMAP servers. "
38                                                 "Small tool to fix the IMAP internaldate "
39                                                 "in case it's too much off compared to the last date "
40                                                 "stored in the received lines.")
41     parser.add_argument('-u', '--update', dest='test_mode', action='store_false',
42                         default=True, help='update all e-mails and exit test mode')
43
44     # config and logging setup
45     config = load_configuration()
46     prepare_logger(config)
47     args = parser.parse_args()
48     if(args.test_mode):
49         logging.info("Testing mode initiated. No message will be modified on the server.")
50     else:
51         logging.info("Update mode initiated. Messages will be modified.")
52
53     # proceed to main functionality
54     try:
55         synchronize_csv(config, args.test_mode)
56     except KeyboardInterrupt:
57         logging.info("Script was interrupted by the user.")
58
59     return
60
61 def load_configuration():
62     """Loads the script configuration from a file or creates such."""
63     config = configparser.RawConfigParser()    
64     success = config.read('confscript.cfg')
65     if(len(success)==0):
66         config.add_section('basic_settings')
67         config.set('basic_settings', 'file_log_level', logging.INFO)
68         config.set('basic_settings', 'console_log_level', logging.INFO)
69         config.set('basic_settings', 'imap_server', 'imap.company.com')
70         config.set('basic_settings', 'tolerance', 30)
71         with open('confscript.cfg', 'w') as configfile:
72             config.write(configfile)
73             configfile.write("# 0 NOTSET, 10 DEBUG, 20 INFO, 30 WARNING, 40 ERROR, 50 CRITICAL")
74     return config
75
76 def prepare_logger(config):
77     """Sets up the logging functionality"""
78     
79     # reset the log
80     with open('fix_imap_internaldate.log', 'w'):
81         pass
82     
83     # add basic configuration
84     logging.basicConfig(filename='fix_imap_internaldate.log',
85                         format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
86                         level=config.getint('basic_settings', 'file_log_level'))
87     
88     # add a handler for a console output
89     console = logging.StreamHandler()
90     console.setLevel(config.getint('basic_settings', 'console_log_level'))
91     formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
92     console.setFormatter(formatter)
93     logging.getLogger('').addHandler(console)
94     return
95
96 def synchronize_csv(config, test_mode):
97     """Iterates through csv list of users and synchronizes their messages."""
98
99     # initialize loop permanent data
100     caching_data = CachingData()  
101     date_parser = MailDateParser()
102     server = config.get('basic_settings', 'imap_server')
103     tolerance = config.getint('basic_settings', 'tolerance') * 60
104
105     # iterate through the users in the csv data
106     user_reader = csv.DictReader(open("userdata.csv", "r"), delimiter=',')
107     for user in user_reader:
108         try:
109             session = MailIterator(server, user['username'], user['password'])
110         except UserWarning as ex:
111             logging.error(ex)
112             continue
113         for mailbox in session:
114             try:
115                 box = caching_data.retrieve_cached_mailbox(mailbox[0], mailbox[1], user['username'])
116                 mail_ids = session.fetch_messages()
117                 new_ids = box.synchronize(mail_ids, tolerance)
118                 logging.info("%s new messages out of %s found in %s.", len(new_ids), len(mail_ids), box.name)
119             except UserWarning as ex:
120                 logging.error(ex)
121                 continue
122             for mid in new_ids:
123                 try:
124                     fetched_internal_date = session.fetch_internal_date(mid)
125                     internal_date = date_parser.extract_internal_date(fetched_internal_date)
126                     fetched_received_date = session.fetch_received_date(mid)
127                     received_date = date_parser.extract_received_date(fetched_received_date)
128                     if(received_date==""):
129                         logging.debug("No received date could be found in message uid: %s - mailbox: %s - user: %s.",
130                                         mid.decode('iso-8859-1'), box.name, box.owner)
131                         box.no_received_field += 1
132                         continue
133                 except UserWarning as ex:
134                     logging.error(ex)
135                     continue
136                 if(date_parser.compare_dates(received_date, internal_date, tolerance)):
137                     logging.warning("Date conflict found in message uid: %s - mailbox: %s - user: %s.\nInternal date %s is different from received date %s from RECEIVED header:\n%s.",
138                                     mid.decode('iso-8859-1'), box.name, box.owner,
139                                     internal_date.strftime("%d %b %Y %H:%M:%S"),
140                                     received_date.strftime("%d %b %Y %H:%M:%S"),
141                                     fetched_received_date.split("Received:")[1])
142                     if(not test_mode):
143                         try:
144                             session.update_message(mid, box.name, received_date)
145                         except UserWarning as ex:
146                             logging.error(ex)
147                             continue
148
149                     # count total emails for every user and mailbox
150                     box.date_conflicts += 1
151
152             # if all messages were successfully fixed confirm caching
153             if(not test_mode):
154                 box.confirm_change()
155     
156         # final report on date conflicts
157         caching_data.report_conflicts()
158     return
159
160 if(__name__ == "__main__"):
161     main()