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 return time.strptime(strg[:-3], "%Y%m%d-%H%M%S")[0:6] + (int(z),)
94 return time.strftime("%Y%m%d-%H%M%S", time.gmtime(t))
96 return time.strftime("%Y%m%d-%H%M%S", tm[0:6]+(0,0,0)) + ("%+03d" % tm[6])
100 def __init__(self, **a):
101 if len(a) == 1 and 'line' in a:
102 # line read from a file, with 5 fields.
103 # stamp, source, time, correspondent, text
104 line = a['line'].split()
105 self.stamp = parse_time(line[0])
106 self.source = line[1]
107 self.time = parse_ltime(line[2])
108 self.correspondents = []
109 for c in line[3].split(','):
111 self.correspondents.append(urllib.unquote(c))
112 self.set_corresponent()
118 self.text = urllib.unquote(txt)
120 # multipart: <1>text...<2>text...<3><4>
121 m = re.findall('<(\d+)>([^<]*)', txt)
123 for (pos, strg) in m:
125 while len(parts) < p:
128 parts[p-1] = urllib.unquote(strg)
132 self.stamp = int(time.time())
134 lt = time.localtime()
135 z = time.timezone/15/60
138 self.time = time.localtime()[0:6] + (z,)
139 self.correspondents = []
151 # time can be a GSM string: 09/02/09,09:56:28+44 (ymd,HMS+z)
152 # or a tuple (y,m,d,H,M,S,z)
153 if type(a[k]) == str:
156 tm = time.strptime(t, "%y/%m/%d,%H:%M:%S")
157 self.time = tm[0:6] + (int(z),)
158 elif k == 'to' or k == 'sender':
159 if self.source == None:
161 self.source = 'LOCAL'
164 self.correspondents = [ a[k] ]
165 elif k == 'correspondents':
166 self.correspondents = a[k]
179 if self.source == None:
180 self.source = 'LOCAL'
182 print 'part', part[0], part[1]
183 self.parts = [None for x in range(part[1])]
184 self.parts[part[0]-1] = self.text
186 self.set_corresponent()
188 self.month_re = re.compile("^[0-9]{6}$")
190 def reduce_parts(self):
191 def reduce_pair(a,b):
193 b = "...part of message missing..."
197 self.text = reduce(reduce_pair, self.parts)
200 def set_corresponent(self):
201 if len(self.correspondents) == 1:
202 self.correspondent = self.correspondents[0]
203 elif len(self.correspondents) == 0:
204 self.correspondent = "Unsent"
206 self.correspondent = "Multiple"
209 fmt = "%s %s %s %s " % (format_time(self.stamp), self.source,
210 format_ltime(self.time),
211 self.format_correspondents())
213 return fmt + urllib.quote(self.text)
215 for i in range(len(self.parts)):
216 fmt += ("<%d>" % (i+1)) + urllib.quote(self.parts[i]) if self.parts[i] else ""
219 def format_correspondents(self):
221 for i in self.correspondents:
223 r += ',' + urllib.quote(i)
231 def __init__(self, dir):
232 self.month_re = re.compile("^[0-9]{6}$")
233 self.cached_month = None
237 self.drafts = self.load_list('draft')
238 self.newmesg = self.load_list('newmesg')
240 def load_list(self, name, update = None, *args):
244 f = open(self.dirname + '/' + name, 'r+')
249 fcntl.lockf(f, fcntl.LOCK_EX)
251 l.append(parse_time(ln.strip()))
255 if update and update(l, *args):
256 f2 = open(self.dirname + '/' + name + '.new', 'w')
258 f2.write(format_time(t)+"\n")
260 os.rename(self.dirname + '/' + name + '.new',
261 self.dirname + '/' + name)
265 def load_month(self, f):
266 # load the messages from f, which is open for read
272 if m.stamp in self.drafts:
274 elif m.stamp in self.newmesg:
278 def store_month(self, l, m):
279 dm = self.dirname + '/' + m
280 f = open(dm+'.new', 'w')
282 f.write(l[s].format() + "\n")
284 os.rename(dm+'.new', dm)
285 if not m in self.files:
289 if self.cached_month == m:
292 def store(self, sms):
295 # This is part of a multipart.
296 # If there already exists part of this
297 # merge them together
299 times = self.load_list('multipart-' + sms.ref)
301 orig = self.load(times[0])
302 if orig and orig.parts:
303 for i in range(len(sms.parts)):
304 if sms.parts[i] == None and i < len(orig.parts):
305 sms.parts[i] = orig.parts[i]
309 m = time.strftime("%Y%m", time.gmtime(sms.stamp))
311 f = open(self.dirname + '/' + m, "r+")
313 f = open(self.dirname + '/' + m, "w+")
323 fcntl.lockf(f, fcntl.LOCK_EX)
324 l = self.load_month(f)
325 while sms.stamp in l:
328 self.store_month(l, m);
336 os.unlink(self.dirname + '/multipart-' + sms.ref)
340 def replacewith(l, tm):
345 self.load_list('multipart-' + sms.ref, replacewith, sms.stamp)
347 f = open(self.dirname +'/multipart-' + sms.ref, 'w')
348 fcntl.lockf(f, fcntl.LOCK_EX)
349 f.write(format_time(sms.stamp) + '\n')
352 if sms.state == 'NEW' or sms.state == 'DRAFT':
354 if sms.state == 'DRAFT':
356 f = open(self.dirname +'/' + s, 'a')
357 fcntl.lockf(f, fcntl.LOCK_EX)
358 f.write(format_time(sms.stamp) + '\n')
360 elif sms.state != None:
365 for f in os.listdir(self.dirname):
366 if self.month_re.match(f):
371 def lookup(self, lasttime = None, state = None):
373 lasttime = int(time.time())
375 return self.getmesgs(lasttime)
377 self.drafts = self.load_list('draft')
380 self.newmesg = self.load_list('newmesg')
386 self.cached_month = None
398 def getmesgs(self, last):
401 t = parse_time(m + '01-000000')
404 mon = self.load_month(open(self.dirname + '/' + m))
409 rv.sort(cmp = lambda x,y:cmp(y.stamp, x.stamp))
414 m = time.strftime("%Y%m", time.gmtime(t))
415 if not m in self.files:
417 if self.cached_month != m:
418 self.cached_month = m
419 self.cache = self.load_month(open(self.dirname + '/' + m))
424 def delete(self, msg):
425 if isinstance(msg, SMSmesg):
429 m = time.strftime("%Y%m", time.gmtime(tm))
431 f = open(self.dirname + '/' + m, "r+")
435 fcntl.lockf(f, fcntl.LOCK_EX)
436 l = self.load_month(f)
439 self.store_month(l, m);
448 self.drafts = self.load_list('draft', del1, tm)
449 self.newmesg = self.load_list('newmesg', del1, tm)
451 def setstate(self, msg, state):
460 if tm in self.drafts and state != 'DRAFT':
461 self.drafts = self.load_list('draft', del1, tm)
462 if tm in self.newmesg and state != 'NEW':
463 self.newmesg = self.load_list('newmesg', del1, tm)
465 if tm not in self.drafts and state == 'DRAFT':
466 f = open(self.dirname +'/draft', 'a')
467 fcntl.lockf(f, fcntl.LOCK_EX)
468 f.write(format_time(sms.stamp) + '\n')
470 self.drafts.append(tm)
472 self.drafts.reverse()
474 if tm not in self.newmesg and state == 'NEW':
475 f = open(self.dirname +'/newmesg', 'a')
476 fcntl.lockf(f, fcntl.LOCK_EX)
477 f.write(format_time(sms.stamp) + '\n')
479 self.newmesg.append(tm)
481 self.newmesg.reverse()