]> git.phdru.name Git - m_lib.git/blob - m_lib/net/sms.py
9b7339001f1801fc0340b4adc3f6c008a160830a
[m_lib.git] / m_lib / net / sms.py
1 """SMS Transports by phd, corvin, r_sabitov"""
2 # -*- coding: koi8-r -*-
3
4
5 import socket
6
7 from m_lib.opstring import koi2win
8 from m_lib.rus.rus2lat import rus2lat
9
10
11 debug_level = 0
12 use_syslog = 0
13
14 def debug(level, message):
15    if level <= debug_level:
16       if use_syslog:
17          import syslog
18          syslog.syslog(message)
19       import sys
20       sys.stderr.write("%s\n" % message)
21
22
23 smsSize = 160
24
25 #
26 # Base transport classes
27 #
28
29 class Transport(object):
30    def __init__(self, phone, message):
31       self.phone = phone
32       self.message = message
33
34    def calcText(self, maxsize=smsSize, transFunction=None):
35       "Recode text on demand and truncate the result"
36
37       text = self.message
38       if transFunction:
39          text = transFunction(text)
40       text = text[:maxsize - 1]
41       return text
42
43    def calcExpiration(self):
44       from mx import DateTime
45       return DateTime.now() + DateTime.DateTimeDelta(0, 4)
46
47 class CP1251(object):
48    "Mixin that converts input text from koi8 to cp1251"
49
50    def calcText(self, maxsize=smsSize):
51       text = super(CP1251, self).calcText(maxsize=maxsize)
52       return koi2win(text)
53
54 #
55 # HTTP transport
56 #
57
58 class HTTPTransport(Transport):
59    "Base class for HTTP transport"
60
61    HTTPHeaders = {
62       'User-Agent': 'Mozilla/5.0 (X11; U; Linux 2.2.19 i686; en-US; rv:0.9) Gecko/20010507',
63       'Accept': '*/*'
64    }
65
66    referer = ''
67
68    def post(self, dict):
69       import urllib, urlparse
70       _none, host, uri, _none, _none, _none = urlparse.urlparse(self.url)
71       postdata = urllib.urlencode(dict)
72
73       methodFunc = getattr(self, self.method)
74
75       try:
76          reply = methodFunc(postdata, host, uri)
77       except socket.error:
78          ret = 0
79       else:
80          html = reply[2].fp.read()
81          debug(2, html)
82
83          if html.find(self.ok_match) >= 0:
84             ret = 1
85          else:
86             ret = 0
87
88
89       _debug = 'msg to %s via %s (%s)' % \
90             (self.phone, self.__class__.__name__, ret)
91       debug(1, _debug)
92
93       return ret
94
95    def GET(self, postdata, host, uri, port=80, proxy_host=None, proxy_port=None):
96       "HTTP method GET"
97
98       if postdata:
99          uri = uri + "?" + postdata
100
101       import httplib
102       if proxy_host:
103          http = httplib.HTTP(proxy_host, proxy_port)
104          http.set_debuglevel(debug_level)
105          http.putrequest("GET", 'http://%s%s' % (host, uri))
106       else:
107          http = httplib.HTTP(host, port)
108          http.set_debuglevel(debug_level)
109          http.putrequest("GET", uri)
110
111       http.putheader("Host", host)
112       if self.referer:
113          http.putheader("Referer", self.referer)
114
115       for name, val in self.HTTPHeaders.items():
116          http.putheader(name, val)
117
118       http.endheaders()
119
120       reply = http.getreply()
121       reply[2].fp = http.getfile()
122       return reply
123
124    def POST(self, postdata, host, uri, port=80, proxy_host=None, proxy_port=None):
125       "HTTP method POST"
126
127       import httplib
128       if proxy_host:
129          http = httplib.HTTP(proxy_host, proxy_port)
130          http.set_debuglevel(debug_level)
131          http.putrequest("POST", 'http://%s%s' % (host, uri))
132       else:
133          http = httplib.HTTP(host, port)
134          http.set_debuglevel(debug_level)
135          http.putrequest("POST", uri)
136
137       http.putheader("Host", host)
138       if self.referer:
139          http.putheader("Referer", self.referer)
140       http.putheader('Content-Type', 'application/x-www-form-urlencoded')
141       http.putheader('Content-Length', str(len(postdata)))
142
143       for name, val in self.HTTPHeaders.items():
144          http.putheader(name, val)
145
146       http.endheaders()
147       http.send(postdata)
148
149       reply = http.getreply()
150       reply[2].fp = http.getfile()
151       return reply
152
153 class CP1251HTTPTransport(CP1251, HTTPTransport):
154    pass
155
156 #
157 # SNPP transport
158 #
159
160 class SNPPTransport(Transport):
161    "Base class for SNPP transport"
162
163    host = 'localhost'
164    port = 444
165    ok_match = '250 Message Sent Successfully'
166
167    def post(self, dict):
168       # raw snpp hack w/o error checking
169       try:
170          sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
171          sock.connect(('www.extel-gsm.com', 4444))
172          sock_file = sock.makefile('r')
173
174          # 220 Extel Mobile Communications. SNPP Gateway v.0.2.15; Wed May 30 00:43:56 2001
175          # 220 SNPP Gateway Ready
176          debug(2, sock_file.readline())
177          debug(2, sock_file.readline())
178
179          sock.send('PAGE %s\r\n' % dict['phone'])
180          # 250 Pager ID Accepted
181          debug(2, sock_file.readline())
182
183          sock.send('DATA\r\n')
184          # 354 Begin Input; End with <CRLF>'.'<CRLF>
185          debug(2, sock_file.readline())
186
187          sock.send('%s\r\n' % dict['message'])
188          sock.send('.\r\n')
189          # 250 Message OK
190          debug(2, sock_file.readline())
191
192          sock.send('SEND\r\n')
193          # 250 Message Sent Successfully. Msg ID = 692274
194          reply = sock_file.readline()
195          debug(2, reply)
196
197          sock.send('QUIT\r\n')
198          sock.close()
199       except:
200          ret = 0
201       else:
202          if reply.find(self.ok_match) >= 0:
203             ret = 1
204          else:
205             ret = 0
206
207       _debug = 'msg to %s via %s (%s)' % \
208             (self.phone, self.__class__.__name__, ret)
209       debug(1, _debug)
210       return ret
211
212 #
213 # E-mail trnsport
214 #
215
216 class EMailTransport(Transport):
217    def __init__(self, phone, message, mail_from=None, gate=None):
218       Transport.__init__(self, phone, message)
219
220       if not mail_from:
221          import getpass
222          try:
223             realuser = getpass.getuser()
224          except AttributeError:
225             # Not all systems have os.environ or getpw...
226             realuser = 'nobody'
227          thishost = socket.getfqdn()
228          mail_from = "%s@%s" % (realuser, thishost)
229          debug(1, mail_from)
230       self.mail_from = mail_from
231
232       self.gate = gate
233
234    def send(self):
235       message = """\
236 From: %s
237 To: %s@%s
238 Subject: SMS
239
240 %s
241 """ % (self.mail_from, self.phone, self.gate, self.message)
242       self.post(message)
243       debug(2, message)
244       return 1
245
246 class SendmailTransport(EMailTransport):
247    def __init__(self, phone, message, mail_from=None, gate=None):
248       EMailTransport.__init__(self, phone, message, mail_from, gate)
249
250       sendmail = self.find_sendmail()
251       if not sendmail:
252          raise ValueError, "cannot find sendmail binary"
253       self.sendmail = sendmail
254
255    def find_sendmail(self):
256       import os
257       for sendmail in ("/usr/lib/sendmail", "/usr/sbin/sendmail"):
258          if os.path.exists(sendmail):
259             return sendmail
260       return None
261
262    def post(self, message):
263       cmd = "%s -oem -oi '%s@%s'" % (self.sendmail, self.phone, self.gate)
264       debug(1, cmd)
265       import os
266       sendmail = os.popen(cmd, 'w')
267       sendmail.write(message)
268       sendmail.close()
269
270 class SMTPTransport(EMailTransport):
271    def __init__(self, phone, message, mail_from=None, gate=None, mail_relay="localhost"):
272       EMailTransport.__init__(self, phone, message, mail_from, gate)
273       self.mail_relay = mail_relay
274
275    def post(self, message):
276       debug(1, self.mail_relay)
277       import smtplib
278       smtp = smtplib.SMTP(self.mail_relay)
279       smtp.sendmail(self.mail_from, "%s@%s" % (self.phone, self.gate), message)
280       smtp.close()
281
282
283 #
284 # Real transports
285 #
286
287 class MTSru(HTTPTransport):
288    "Russian provider MTS.ru"
289
290    method = "POST"
291    url = "http://www.mts.ru:5051/cgi-bin/cgi.exe"
292    referer = ""
293    ok_match = 'Ваше сообщение отправлено'
294
295    def send(self):
296       dict = {}
297       text = self.calcText()
298       time = self.calcExpiration()
299
300       dict['function'] = 'sms_send'
301       #dict['MMObjectType'] = '0'
302       #dict['MMObjectID'] = ''
303       phone = self.phone
304       if phone[:4] == "8902":
305          phone = "8916%s" % phone[4:]
306       dict['To'] = phone
307       dict['Msg'] = text
308       dict['Hour'] = time.strftime('%k').strip()
309       dict['Min'] = time.strftime('%M')
310       dict['Day'] = time.strftime('%e').strip()
311       dict['Mon'] = str(time.month)
312       dict['Year'] = time.strftime('%Y')
313       dict['count'] = str(len(text))
314       dict['Lang'] = '2'
315
316       return self.post(dict)
317
318    def POST(self, postdata, host, uri, port=80, proxy_host=None, proxy_port=None):
319       postdata = "MMObjectType=0&MMObjectID=&" + postdata
320       return HTTPTransport.POST(self, postdata, host, uri, port, proxy_host, proxy_port)
321
322
323 class MTSGSMcom(HTTPTransport):
324    url = "http://www.mtsgsm.com/sms/sent.html"
325    referer = "http://www.mtsgsm.com/sms/"
326    method = "GET"
327    ok_match = 'Ваше сообщение отправлено'
328
329    def send(self):
330       dict = {}
331       text = self.calcText()
332       time = self.calcExpiration()
333
334       dict['Posted'] = '1'
335       dict['To'] = self.phone
336       dict['Msg'] = text
337       dict['count'] = str(len(text))
338       dict['SMSHour'] = time.strftime('%k').strip()
339       dict['SMSMinute'] = time.strftime('%M')
340       dict['SMSDay'] = time.strftime('%e').strip()
341       dict['SMSMonth'] = str(time.month - 1)
342       dict['SMSYear'] = time.strftime('%Y')
343
344       return self.post(dict)
345
346
347 class BeeOnLine(CP1251HTTPTransport):
348    "Russian provider BeeOnLine.ru"
349
350    method = "POST"
351    url = "http://www.beeonline.ru/portal/comm/send_sms/simple_send_sms.sms"
352    referer = "http://www.beeonline.ru/portal/comm/send_sms/simple_send_sms.sms"
353    crap = 'BOL '
354    ok_match = koi2win('Ваше сообщение отправлено')
355
356    def __init__(self, phone, message,
357          mode="GSM",      # mode can be either GSM or DAMPS
358          transliterate=1, # turn transliteration of/off
359          reply_to=''):      # send reply to this e-mail
360       Transport.__init__(self, phone, message)
361       if mode not in ('GSM', 'DAMPS'):
362          raise ValueError, "mode (%s) must be either 'GSM' or 'DAMPS'" % mode
363       self.mode = mode
364       self.transliterate = transliterate
365       self.reply_to = reply_to
366
367    def send(self):
368       dict = {}
369       text = self.calcText(smsSize - len(self.crap))
370
371       # hidden
372       #dict['deferto'] = ''
373       #dict['adv_year'] = ''
374       dict['send'] = 'send'
375       dict['destination_number_from'] = 'ordinary' # number, not a BEEpost
376
377       prf = self.phone[:4]
378       if self.mode == "GSM":
379          dict['network_code'] = '3'
380       elif prf == '7095':
381          dict['network_code'] = '2'
382       elif prf == '7901':
383          dict['network_code'] = '1'
384       else:
385          raise RuntimeError, "incorrect combination of mode (%s) and prefix (%s)" % (self.mode, prf)
386
387       dict['phone'] = self.phone[4:]
388       dict['message'] = text
389       dict['mlength'] = str(smsSize - len(self.crap) - len(text))
390
391       if self.mode == "GSM" and not self.transliterate:
392          dict['translit'] = '1' # turn transliteration OFF! :)
393
394       if self.reply_to:
395          dict['send_email'] = '1'
396          dict['reply_addr'] = self.reply_to
397
398       return self.post(dict)
399
400 class BeeOnLineSMS(BeeOnLine):
401    "Russian provider BeeOnLine.ru"
402
403    url = "http://www.beeonline.ru/servlet/send/sms/"
404
405    def send(self):
406       dict = {}
407       text = self.calcText(smsSize - len(self.crap))
408
409       dict['prf'] = self.phone[:4]
410       dict['phone'] = self.phone[4:]
411       dict['termtype'] = self.mode[0]
412       dict['message'] = text
413
414       if self.mode == "GSM" and not self.transliterate:
415          dict['translit'] = '1' # turn transliteration OFF! :)
416
417       dict['number_sms'] = "number_sms_send"
418       return self.post(dict)
419
420
421 class Pcom(HTTPTransport):
422    "Russian provider PCOM.ru"
423
424    method = "POST"
425    url = "http://www.pcom.ru/online.phtml"
426    referer = "http://www.pcom.ru/online.phtml"
427    crap = ''
428    ok_match = koi2win('поставлено в очередь')
429
430    def calcText(self, maxsize=smsSize):
431       "force translitertaion"
432
433       return HTTPTransport.calcText(self, maxsize=maxsize,
434                            transFunction=rus2lat)
435
436    def send(self):
437       dict = {}
438       text = self.calcText(120 - len(self.crap))
439       expiration = self.calcExpiration()
440       from mx import DateTime
441       now = DateTime.now()
442
443       dict['ACTION'] = 'SENDSMS'
444
445       dict['SMS_START_HOUR'] = now.strftime('%H')
446       dict['SMS_START_MINUTE'] = now.strftime('%M')
447       dict['SMS_START_DAY'] = now.strftime('%d')
448       dict['SMS_START_MONTH'] = now.strftime('%m')
449       dict['SMS_START_YEAR'] = now.strftime('%Y')
450
451       dict['SMS_STOP_HOUR'] = expiration.strftime('%H')
452       dict['SMS_STOP_MINUTE'] = expiration.strftime('%M')
453       dict['SMS_STOP_DAY'] = expiration.strftime('%d')
454       dict['SMS_STOP_MONTH'] = expiration.strftime('%m')
455       dict['SMS_STOP_YEAR'] = expiration.strftime('%Y')
456
457       dict['prefix'] = self.phone[1:4]
458       dict['DN'] = self.phone[4:]
459       dict['MSG'] = text
460
461       return self.post(dict)
462
463
464 class ExtelGsmCom(SNPPTransport):
465    "Russian provider Extel-GSM.com"
466
467    host = 'www.extel-gsm.com'
468    port = 4444
469    ok_match = '250 Message Sent Successfully'
470    crap = ''
471
472    prefix = '0119'
473
474    def calcText(self, maxsize=smsSize):
475       "force translitertaion"
476
477       return SNPPTransport.calcText(self, maxsize=maxsize,
478                            transFunction=rus2lat)
479
480    def send(self):
481       dict = {}
482       text = self.calcText(smsSize - len(self.crap))
483       phone = self.phone[5:]
484
485       # 0112 (city code) must be replaced with (0119)
486       dict['phone'] = '+7%s%s' % (self.prefix, phone)
487       dict['message'] = text
488
489       return self.post(dict)
490
491
492 class MegafoneMoscow(CP1251HTTPTransport):
493    "Rissian provider megafonmoscow.ru"
494
495    method = "POST"
496    url = "http://www.megafonmoscow.ru/rus/sms.xpml"
497    referer = "http://www.megafonmoscow.ru/rus/sms.xpml"
498    ok_match = koi2win('Сообщение успешно отправлено')
499
500    def send(self):
501       dict = {}
502       text = self.calcText()
503
504       dict['prefix'] = self.phone[:4]
505       dict['addr'] = self.phone[4:]
506       dict['message'] = text
507       dict['send'] = "     Send     "
508
509       if hasattr(self, 'transliterate') and self.transliterate:
510          dict['transliterate'] = '1'
511
512       return self.post(dict)
513
514
515 class SkyLinkMsk(CP1251HTTPTransport):
516    "Russian provider SkyLink.Msk.ru"
517
518    method = "POST"
519    url = "http://skylink.msk.ru/inc/sendsms.php"
520    ok_match = koi2win('Сообщение было отправлено')
521
522    def send(self):
523       dict = {}
524       text = self.calcText()
525
526       phone = self.phone
527       if phone[0] == '7':
528          phone = '8' + phone[1:]
529       dict['dest'] = phone
530       dict['smsbody'] = text
531
532       return self.post(dict)
533
534
535 BEELINE = BeeOnLineSMS
536 #BEELINE = Whizdiary
537 MTS = MTSru
538 #MTS = MTSGSMcom
539 #MTS = Whizdiary
540 SONET = Pcom
541 EXTEL = ExtelGsmCom
542 Megafone = MegafoneMoscow
543 SkyLink = SkyLinkMsk
544
545
546 Prefix2Provider = {
547    "903": BEELINE,
548    "905": BEELINE,
549    "906": BEELINE,
550
551    "910": MTS,
552    "916": MTS,
553
554    "926": Megafone
555 }
556
557
558 class SMSError(Exception): pass
559
560 def Phone2Provider(phone):
561    prefix = phone[1:4]
562
563    if prefix in ("095", "901"): # 901 is being used by Beeline and SkyLink
564       raise SMSError, "unknown provider for phone %s" % phone
565
566    if Prefix2Provider.has_key(prefix):
567       return Prefix2Provider[prefix]
568
569    raise SMSError, "bad prefix for phone %s" % phone