d169f2be29c72aaefe59865771e1bfc0f48cefde
[imap-fix-internaldate] / 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, configparser
21 import logging
22 from mail_date_parser import MailDateParser
23 from mail_iterator import MailIterator
24 from caching_data import CachingData
25
26 def main():
27     """Iterates through csv list of users and synchronize their messages"""
28
29     parser = argparse.ArgumentParser(description="Fix the INTERNALDATE field on IMAP servers. "
30                                                 "Small tool to fix the IMAP internaldate "
31                                                 "in case it's too much off compared to the last date "
32                                                 "stored in the received lines.")
33     parser.add_argument('-u', '--update', dest='test_mode', action='store_false',
34                         default=True, help='update all e-mails and exit test mode')
35     args = parser.parse_args()
36
37     # config and logging setup
38     config = load_configuration()
39     prepare_logger(config)
40
41     date_parser = MailDateParser()
42     caching_data = CachingData()
43     logging.warning("Cache version %s loaded.", caching_data.version)
44     if(args.test_mode):
45         logging.info("Testing mode initiated. No message will be modified on the server.")
46     else:
47         logging.info("Update mode initiated. Messages will be modified.")
48     user_reader = csv.DictReader(open("userdata.csv", "r"), delimiter=',')
49
50     # server name is stored in the config
51     server = config.get('basic_settings', 'imap_server')
52     # tolerance is now in seconds
53     tolerance = config.getint('basic_settings', 'tolerance') * 60
54
55     for user in user_reader:
56         try:
57             session = MailIterator(server, user['username'], user['password'])
58         except UserWarning as ex:
59             logging.error(ex)
60             continue
61         for mailbox in session:
62             try:
63                 box = caching_data.retrieve_cached_mailbox(mailbox[0], mailbox[1], user['username'])
64                 mail_ids = session.fetch_messages()
65                 new_ids = box.synchronize(mail_ids, tolerance)
66                 logging.info("%s new messages out of %s found in %s.", len(new_ids), len(mail_ids), box.name)
67             except UserWarning as ex:
68                 logging.error(ex)
69                 continue
70             for mid in new_ids:
71                 try:
72                     fetched_internal_date = session.fetch_internal_date(mid)
73                     internal_date = date_parser.extract_internal_date(fetched_internal_date)
74                     fetched_received_date = session.fetch_received_date(mid)
75                     received_date = date_parser.extract_received_date(fetched_received_date)
76                     if(received_date==""):
77                         logging.debug("No received date could be found in message uid: %s - mailbox: %s - user: %s.",
78                                         mid.decode('iso-8859-1'), box.name, box.owner)
79                         box.no_received_field += 1
80                         continue
81                 except UserWarning as ex:
82                     logging.error(ex)
83                     continue
84                 if(date_parser.compare_dates(received_date, internal_date, tolerance)):
85                     #print(received_date, internal_date)
86                     if(args.test_mode==0):
87                         try:
88                             session.update_message(mid, box.name, received_date)
89                         except UserWarning as ex:
90                             logging.error(ex)
91                             continue
92                     else:
93                         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.",
94                                         mid.decode('iso-8859-1'), box.name, box.owner,
95                                         internal_date.strftime("%d %b %Y %H:%M:%S"),
96                                         received_date.strftime("%d %b %Y %H:%M:%S"),
97                                         fetched_received_date.split("Received:")[1])
98                     # count total emails for every user and mailbox
99                     box.date_conflicts += 1
100             # if all messages were successfully fixed confirm caching
101             if(not args.test_mode):
102                 box.confirm_change()
103
104         # final report on date conflicts
105         caching_data.report_conflicts()
106
107 def load_configuration():
108     """Loads the script configuration from a file or creates such."""
109     config = configparser.RawConfigParser()    
110     success = config.read('confscript.cfg')
111     if(len(success)==0):
112         config.add_section('basic_settings')
113         config.set('basic_settings', 'file_log_level', logging.INFO)
114         config.set('basic_settings', 'console_log_level', logging.INFO)
115         config.set('basic_settings', 'imap_server', 'imap.company.com')
116         config.set('basic_settings', 'tolerance', 30)
117         #config.set('Basic settings', 'bool', 'true')
118         with open('confscript.cfg', 'w') as configfile:
119             config.write(configfile)
120     return config
121
122 def prepare_logger(config):
123     """Sets up the logging functionality"""
124     
125     # reset the log
126     with open('fix_imap_internaldate.log', 'w'):
127         pass
128     
129     # add basic configuration
130     logging.basicConfig(filename='fix_imap_internaldate.log',
131                         format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
132                         level=config.getint('basic_settings', 'file_log_level'))
133     
134     # add a handler for a console output
135     console = logging.StreamHandler()
136     console.setLevel(config.getint('basic_settings', 'console_log_level'))
137     formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
138     console.setFormatter(formatter)
139     logging.getLogger('').addHandler(console)
140     return
141
142
143
144 if(__name__ == "__main__"):
145     main()