3 # - trim newmesg and draft when possible.
4 # - remove old multipart files
6 # Store SMS messages is a bunch of files, one per month.
7 # Each message is stored on one line with space separated .
8 # URL encoding (%XX) is used to quote white space, unprintables etc
10 # - time stamp that we first saw the message. This is in UTC.
11 # This is the primary key. If a second message is seen in the same second,
12 # we quietly add 1 to the second.
13 # - Source, one of 'LOCAL' for locally composed, 'GSM' for recieved via GSM
14 # or maybe 'EMAIL' if received via email??
15 # - Time message was sent, Localtime with -TZ. For GSM messages this comes with the
16 # message. For 'LOCAL' it might be '-', or will be the time we succeeded
18 # time is stored as a tupple (Y m d H M S Z) where Z is timezone in multiples
20 # - The correspondent: sender if GSM, recipient if LOCAL, or '-' if not sent.
21 # This might be a comma-separated list of recipients.
22 # - The text of the message
24 # Times are formatted %Y%m%d-%H%M%S and local time has a GSM TZ suffix.
25 # GSM TZ is from +48 to -48 in units of 15 minutes. (0 is +00)
27 # We never modify a message once it has been stored.
28 # If we have a draft that we edit and send, we delete the draft and
29 # create a new sent-message
30 # If we forward a message, we will then have two copies.
32 # New messages are not distinguished by a flag (which would have to be cleared)
33 # but by being in a separate list of new messages.
34 # We havea list of 'new' messages and a list of 'draft' messages.
36 # Multi-part messages are accumulated as they are received. The quoted message
37 # contains <N>text for each part of the message.
38 # e.g. <1><2>nd%20so%20no.....<3>
39 # if we only have part 2 of 3.
40 # For each incomplete message there is a file (like 'draft' and 'newmesg') named
41 # for the message which provides an index to each incomplete message.
42 # It will be named e.g. 'multipart-1C' when 1C is the message id.
44 # This module defines 2 classes:
46 # This holds a message and so has timestamp, source, time, correspondent
47 # and text fields. These are decoded.
48 # SMSmesg also has 'state' which can be one of "NEW", "DRAFT" or None
49 # Finally it might have a 'ref' and a 'part' which is a tuple (this,max)
50 # This is only used when storing the message to link it up with
54 # This represents a collection of messages in a directory (one file per month)
55 # and provides lists of 'NEW' and 'DRAFT' messages.
57 # store(SMSmesg, NEW|DRAFT|) -> None
58 # stores the message and sets the timestamp
59 # lookup(latest-time, NEW|DRAFT|ALL) -> (earlytime, [SMSmesg])
60 # collects a list of messages in reverse time order with times no later
61 # than 'latest-time'. Only consider NEW or DRAFT or ALL messages.
62 # The list may not be complete (typically one month at a time are returnned)
63 # If you want more, call again with 'earlytime' as 'latest-time').
65 # delete the given message (based on the timestamp only)
66 # setstate(SMSmesg, NEW|DRAFT|None)
67 # update the 'new' and 'draft' lists or container, or not container, this
72 import os, fcntl, re, time, urllib
75 # like time.mktime, but tm is UTC
76 # So we want a 't' where
77 # time.gmtime(t)[0:6] == tm[0:6]
78 estimate = time.mktime(tm) - time.timezone
79 t2 = time.gmtime(estimate)
80 while t2[0:6] < tm[0:6]:
82 t2 = time.gmtime(estimate)
83 while t2[0:6] > tm[0:6]:
85 t2 = time.gmtime(estimate)
89 return int(umktime(time.strptime(strg, "%Y%m%d-%H%M%S")))
90 def parse_ltime(strg):
92 if strg[-2] == '+' or strg[-2] == '-':
95 return time.strptime(strg[:-n], "%Y%m%d-%H%M%S")[0:6] + (int(z),)
97 return time.strftime("%Y%m%d-%H%M%S", time.gmtime(t))
99 return time.strftime("%Y%m%d-%H%M%S", tm[0:6]+(0,0,0)) + ("%+03d" % tm[6])
103 def __init__(self, **a):
104 if len(a) == 1 and 'line' in a:
105 # line read from a file, with 5 fields.
106 # stamp, source, time, correspondent, text
107 line = a['line'].split()
108 self.stamp = parse_time(line[0])
109 self.source = line[1]
110 self.time = parse_ltime(line[2])
111 self.correspondents = []
112 for c in line[3].split(','):
114 self.correspondents.append(urllib.unquote(c))
115 self.set_corresponent()
121 self.text = urllib.unquote(txt)
123 # multipart: <1>text...<2>text...<3><4>
124 m = re.findall('<(\d+)>([^<]*)', txt)
126 for (pos, strg) in m:
128 while len(parts) < p:
131 parts[p-1] = urllib.unquote(strg)
135 self.stamp = int(time.time())
137 lt = time.localtime()
138 z = time.timezone/15/60
141 self.time = time.localtime()[0:6] + (z,)
142 self.correspondents = []
154 # time can be a GSM string: 09/02/09,09:56:28+44 (ymd,HMS+z)
155 # or a tuple (y,m,d,H,M,S,z)
156 if type(a[k]) == str:
159 tm = time.strptime(t, "%y/%m/%d,%H:%M:%S")
160 self.time = tm[0:6] + (int(z),)
161 elif k == 'to' or k == 'sender':
162 if self.source == None:
164 self.source = 'LOCAL'
167 self.correspondents = [ a[k] ]
168 elif k == 'correspondents':
169 self.correspondents = a[k]
182 if self.source == None:
183 self.source = 'LOCAL'
185 print 'part', part[0], part[1]
186 self.parts = [None for x in range(part[1])]
187 self.parts[part[0]-1] = self.text
189 self.set_corresponent()
191 self.month_re = re.compile("^[0-9]{6}$")
193 def reduce_parts(self):
194 def reduce_pair(a,b):
196 b = "...part of message missing..."
200 self.text = reduce(reduce_pair, self.parts)
203 def set_corresponent(self):
204 if len(self.correspondents) == 1:
205 self.correspondent = self.correspondents[0]
206 elif len(self.correspondents) == 0:
207 self.correspondent = "Unsent"
209 self.correspondent = "Multiple"
212 fmt = "%s %s %s %s " % (format_time(self.stamp), self.source,
213 format_ltime(self.time),
214 self.format_correspondents())
216 return fmt + urllib.quote(self.text)
218 for i in range(len(self.parts)):
219 fmt += ("<%d>" % (i+1)) + urllib.quote(self.parts[i]) if self.parts[i] else ""
222 def format_correspondents(self):
224 for i in self.correspondents:
226 r += ',' + urllib.quote(i)
234 def __init__(self, dir):
235 self.month_re = re.compile("^[0-9]{6}$")
236 self.cached_month = None
240 self.drafts = self.load_list('draft')
241 self.newmesg = self.load_list('newmesg')
243 def load_list(self, name, update = None, *args):
247 f = open(self.dirname + '/' + name, 'r+')
252 fcntl.lockf(f, fcntl.LOCK_EX)
254 l.append(parse_time(ln.strip()))
258 if update and update(l, *args):
259 f2 = open(self.dirname + '/' + name + '.new', 'w')
261 f2.write(format_time(t)+"\n")
263 os.rename(self.dirname + '/' + name + '.new',
264 self.dirname + '/' + name)
268 def load_month(self, f):
269 # load the messages from f, which is open for read
275 if m.stamp in self.drafts:
277 elif m.stamp in self.newmesg:
281 def store_month(self, l, m):
282 dm = self.dirname + '/' + m
283 f = open(dm+'.new', 'w')
285 f.write(l[s].format() + "\n")
287 os.rename(dm+'.new', dm)
288 if not m in self.files:
292 if self.cached_month == m:
295 def store(self, sms):
298 # This is part of a multipart.
299 # If there already exists part of this
300 # merge them together
302 times = self.load_list('multipart-' + sms.ref)
304 orig = self.load(times[0])
305 if orig and orig.parts:
306 for i in range(len(sms.parts)):
307 if sms.parts[i] == None and i < len(orig.parts):
308 sms.parts[i] = orig.parts[i]
312 m = time.strftime("%Y%m", time.gmtime(sms.stamp))
314 f = open(self.dirname + '/' + m, "r+")
316 f = open(self.dirname + '/' + m, "w+")
326 fcntl.lockf(f, fcntl.LOCK_EX)
327 l = self.load_month(f)
328 while sms.stamp in l:
331 self.store_month(l, m);
339 os.unlink(self.dirname + '/multipart-' + sms.ref)
343 def replacewith(l, tm):
348 self.load_list('multipart-' + sms.ref, replacewith, sms.stamp)
350 f = open(self.dirname +'/multipart-' + sms.ref, 'w')
351 fcntl.lockf(f, fcntl.LOCK_EX)
352 f.write(format_time(sms.stamp) + '\n')
355 if sms.state == 'NEW' or sms.state == 'DRAFT':
357 if sms.state == 'DRAFT':
359 f = open(self.dirname +'/' + s, 'a')
360 fcntl.lockf(f, fcntl.LOCK_EX)
361 f.write(format_time(sms.stamp) + '\n')
363 elif sms.state != None:
368 for f in os.listdir(self.dirname):
369 if self.month_re.match(f):
374 def lookup(self, lasttime = None, state = None):
376 lasttime = int(time.time())
378 return self.getmesgs(lasttime)
380 self.drafts = self.load_list('draft')
383 self.newmesg = self.load_list('newmesg')
389 self.cached_month = None
401 def getmesgs(self, last):
404 t = parse_time(m + '01-000000')
407 mon = self.load_month(open(self.dirname + '/' + m))
412 rv.sort(cmp = lambda x,y:cmp(y.stamp, x.stamp))
417 m = time.strftime("%Y%m", time.gmtime(t))
418 if not m in self.files:
420 if self.cached_month != m:
421 self.cached_month = m
422 self.cache = self.load_month(open(self.dirname + '/' + m))
427 def delete(self, msg):
428 if isinstance(msg, SMSmesg):
432 m = time.strftime("%Y%m", time.gmtime(tm))
434 f = open(self.dirname + '/' + m, "r+")
438 fcntl.lockf(f, fcntl.LOCK_EX)
439 l = self.load_month(f)
442 self.store_month(l, m);
451 self.drafts = self.load_list('draft', del1, tm)
452 self.newmesg = self.load_list('newmesg', del1, tm)
454 def setstate(self, msg, state):
463 if tm in self.drafts and state != 'DRAFT':
464 self.drafts = self.load_list('draft', del1, tm)
465 if tm in self.newmesg and state != 'NEW':
466 self.newmesg = self.load_list('newmesg', del1, tm)
468 if tm not in self.drafts and state == 'DRAFT':
469 f = open(self.dirname +'/draft', 'a')
470 fcntl.lockf(f, fcntl.LOCK_EX)
471 f.write(format_time(sms.stamp) + '\n')
473 self.drafts.append(tm)
475 self.drafts.reverse()
477 if tm not in self.newmesg and state == 'NEW':
478 f = open(self.dirname +'/newmesg', 'a')
479 fcntl.lockf(f, fcntl.LOCK_EX)
480 f.write(format_time(sms.stamp) + '\n')
482 self.newmesg.append(tm)
484 self.newmesg.reverse()