1
#!/usr/bin/env python2.5
2
# -*- coding: utf-8 -*-
3
""" Useful functions that shouldn't be in the UI code
4
5
fMMS - MMS for fremantle
6
Copyright (C) 2010 Nick Leppänen Larsson <frals@frals.se>
7
8
@license: GNU GPLv2, see COPYING file.
9
"""
10
import logging
11
import logging.config
12
13
logging.config.fileConfig('/opt/fmms/logger.conf')
14
log = logging.getLogger('fmms.%s' % __name__)
15
16
import os
17
import array
18
import re
19
import time
20
import urlparse
21
import subprocess
22
import gettext
23
import socket
24
25
import dbus
26
27
import fmms_config as fMMSconf
28
import dbhandler as DBHandler
29
from mms.message import MMSMessage
30
from mms import mms_pdu
31
from wappushhandler import MMSSender
32
33
#TODO: constants.py?
34
MSG_DIRECTION_IN = 0
35
MSG_DIRECTION_OUT = 1
36
MSG_UNREAD = 0
37
MSG_READ = 1
38
39
_ = gettext.gettext
40
gettext.bindtextdomain('fmms','/opt/fmms/share/locale/')
41
gettext.textdomain('fmms')
42
43
class fMMS_controller():
44
	
45
	def __init__(self):
46
		""" initialize """
47
		self.config = fMMSconf.fMMS_config()
48
		self._mmsdir = self.config.get_mmsdir()
49
		self._pushdir = self.config.get_pushdir()
50
		self._outdir = self.config.get_outdir()
51
		self.store = DBHandler.DatabaseHandler(self)
52
		self.ui = False
53
	
54
	def clean_url(self, url):
55
		m = re.search(r"http\:\/\/(?i)", url)
56
		if m:
57
			url = url.replace(m.group(0), "http://")
58
		return url
59
	
60
	def get_host_from_url(self, url):
61
		""" gets the hostname from an url """
62
		# change HTTP:// etc to lowercase because
63
		# havoc connector depends on it
64
		url = self.clean_url(url)
65
		if not url.startswith("http://"):
66
			url = "http://%s" % url
67
68
		ret = urlparse.urlparse(url)
69
		ret = ret[1].split(":")[0]
70
		return ret
71
	
72
	def convert_to_real_ip(self, indata):
73
		""" converts a ip with leading zeroes to a real ip """
74
		# Check if it looks like an IP
75
		if re.search(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", indata):
76
			ip = indata.split(".")
77
			dot = "."
78
			final = []
79
			for octet in ip:
80
				final.append(str(int(octet)))
81
			indata = dot.join(final)
82
		return indata
83
	
84
	def convert_timeformat(self, intime, format, hideToday=False):
85
		mtime = intime
86
		try:
87
			mtime = time.strptime(mtime, "%Y-%m-%d %H:%M:%S")
88
		except ValueError, e:
89
			#log.info("timeconversion stage1 failed: %s %s", type(e), e)
90
			try:
91
				mtime = time.strptime(mtime)
92
			except ValueError, e:
93
				#log.info("timeconversion stage2 failed: %s %s", type(e), e)
94
				pass
95
		except Exception, e:
96
			#log.exception("Could not convert timestamp: %s %s", type(e), e)
97
			pass
98
		
99
		# TODO: check if hideToday == true
100
		# TODO: remove date if date == today
101
		try:
102
			mtime = time.strftime("%Y-%m-%d | %H:%M", mtime)
103
		except:
104
			log.info("stftime failed: %s %s", type(e), e)
105
			mtime = intime
106
		return mtime
107
	
108
	def send_mms(self, to, subject, message, attachment, sender):
109
		try:
110
			sender = MMSSender(to, subject, message, attachment, sender, setupConn=True, controller=self)
111
		except:
112
			msg = "%s" % (gettext.ldgettext('hildon-common-strings', "sfil_ni_operation_failed"))
113
			return (-1, msg)
114
		try:
115
			(status, reason, output, parsed) = sender.sendMMS()
116
117
			if parsed == True and "Response-Status" in output:
118
				if output['Response-Status'] == "Ok":
119
					log.info("message seems to have sent AOK!")
120
					return (0, "OK")
121
122
			errstr = gettext.ldgettext('hildon-common-strings', "sfil_ni_operation_failed")
123
			message = str(status) + "_" + str(reason)
124
			reply = str(output)
125
			msg = "%s\nMMSC: %s\nBODY: %s" % (errstr, message, reply)
126
			return (-1, msg)
127
		except socket.error, exc:
128
			log.exception("sender failed due to connection error")
129
			errstr = gettext.ldgettext('hildon-common-strings', "sfil_ni_operation_failed")
130
			errhelp = _("Please check your settings.")
131
			msg = "%s\n%s\n%s" % (errstr, exc, errhelp)
132
			return (-1, msg)
133
			#raise
134
		except Exception, exc:
135
			log.exception("Sender failed.")
136
			msg = "%s\n%s" % (gettext.ldgettext('hildon-common-strings', "sfil_ni_operation_failed"), exc)
137
			return (-1, msg)
138
			#raise
139
	
140
	def decode_mms_from_push(self, binarydata):
141
		""" decodes the given mms """
142
		decoder = mms_pdu.MMSDecoder()
143
		wsplist = decoder.decodeCustom(binarydata)
144
		sndr, url, trans_id = None, None, None
145
146
		try:
147
			url = wsplist["Content-Location"]
148
			log.info("content-location: %s", url)
149
			trans_id = wsplist["Transaction-Id"]
150
			trans_id = str(trans_id)
151
			log.info("transid: %s", trans_id)
152
		except Exception, e:
153
			log.exception("no content-location/transid in push; aborting: %s %s", type(e), e)
154
			bus = dbus.SystemBus()
155
			proxy = bus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications')
156
			interface = dbus.Interface(proxy, dbus_interface='org.freedesktop.Notifications')
157
			interface.SystemNoteInfoprint(gettext.ldgettext('modest', "mail_ni_ui_folder_get_msg_folder_error"))
158
			raise
159
		try:
160
			sndr = wsplist["From"]
161
			log.info("Sender: %s", sndr)
162
		except Exception, e:
163
			log.exception("No sender value defined: %s %s", type(e), e)
164
			sndr = "Unknown sender"
165
166
		self.save_binary_push(binarydata, trans_id)
167
		return (wsplist, sndr, url, trans_id)
168
	
169
	def save_binary_push(self, binarydata, transaction):
170
		""" saves the binary push message """
171
		data = array.array('B')
172
		for b in binarydata:
173
			data.append(b)
174
175
		try:
176
			fp = open(self._pushdir + transaction, 'wb')
177
			fp.write(data)
178
			log.info("saved binary push: %s", fp)
179
			fp.close()
180
		except Exception, e:
181
			log.exception("failed to save binary push")
182
			raise
183
	
184
	def save_push_message(self, data):
185
		""" Gets the decoded data as a list (preferably from decode_mms_from_push)
186
		"""
187
		pushid = self.store.insert_push_message(data)
188
		return pushid
189
	
190
	def get_push_list(self, types=None):
191
		""" gets a list of all push messages """
192
		return self.store.get_push_list()
193
	
194
	def is_fetched_push_by_transid(self, transactionid):
195
		return self.store.is_mms_downloaded(transactionid)
196
	
197
	def read_push_as_list(self, transactionid):
198
		return self.store.get_push_message(transactionid)
199
	
200
	def save_binary_mms(self, data, transaction):
201
		dirname = self._mmsdir + transaction
202
		if not os.path.isdir(dirname):
203
			os.makedirs(dirname)
204
		
205
		fp = open(dirname + "/message", 'wb')
206
		fp.write(data)
207
		log.info("saved binary mms %s", fp)
208
		fp.close()
209
		return dirname
210
		
211
	def save_binary_outgoing_mms(self, data, transaction):
212
		transaction = str(transaction)
213
		dirname = self._outdir + transaction
214
		if not os.path.isdir(dirname):
215
			os.makedirs(dirname)
216
217
		fp = open(dirname + "/message", 'wb')
218
		fp.write(data)
219
		log.info("saved binary mms %s", fp)
220
		fp.close()
221
		return dirname
222
	
223
	def decode_binary_mms(self, path):
224
		""" decodes and saves the binary mms"""
225
		# Decode the specified file
226
		# This also creates all the parts as files in path
227
		log.info("decode_binary_mms running: %s", str(path))
228
		try:
229
			message = MMSMessage.fromFile(path + "/message")
230
		except Exception, e:
231
			log.exception("decode binary failed:", type(e), e)
232
			raise
233
		log.info("returning message!")
234
		return message
235
	
236
	def get_filepath_for_mms_transid(self, filename):
237
		return self.store.get_filepath_for_mms_transid(filename).replace("/message", "")
238
	
239
	def is_mms_read(self, transactionid):
240
		return self.store.is_message_read(transactionid)
241
	
242
	def mark_mms_read(self, transactionid):
243
		self.store.mark_message_read(transactionid)
244
	
245
	def store_mms_message(self, pushid, message, transactionId=None):
246
		if transactionId:
247
			message.headers['Transaction-Id'] = transactionId
248
		mmsid = self.store.insert_mms_message(pushid, message)
249
		return mmsid
250
	
251
	def store_outgoing_mms(self, message):
252
		mmsid = self.store.insert_mms_message(0, message, DBHandler.MSG_DIRECTION_OUT)
253
		return mmsid
254
		
255
	def store_outgoing_push(self, wsplist):
256
		pushid = self.store.insert_push_send(wsplist)
257
		return pushid
258
		
259
	def link_push_mms(self, pushid, mmsid):
260
		self.store.link_push_mms(pushid, mmsid)
261
	
262
	def get_direction_mms(self, transid):
263
		return self.store.get_direction_mms(transid)
264
		
265
	def get_replyuri_from_transid(self, transid):
266
		uri = self.store.get_replyuri_from_transid(transid)
267
		try:
268
			uri = uri.replace("/TYPE=PLMN", "")
269
			return uri
270
		except Exception, e:
271
			log.exception("failed to get replyuri, got: %s (%s)", uri, uri.__class__)
272
			return ""
273
	
274
	def get_mms_from_push(self, transactionid):
275
		plist = self.store.get_push_message(transactionid)
276
		#trans_id = plist['Transaction-Id']
277
		# lets reuse the transactionid we already got
278
		trans_id = transactionid
279
		pushid = plist['PUSHID']
280
		url = plist['Content-Location']
281
		
282
		from wappushhandler import PushHandler
283
		p = PushHandler()
284
		path = p._get_mms_message(url, trans_id, self)
285
		log.info("path: %s", path)
286
		message = self.decode_binary_mms(path)
287
		log.info("storing mms...%s", trans_id)
288
		mmsid = self.store_mms_message(pushid, message)
289
		
290
	def get_mms_attachments(self, transactionid, allFiles=False):
291
		return self.store.get_mms_attachments(transactionid, allFiles)
292
	
293
	def get_mms_headers(self, transactionid):
294
		return self.store.get_mms_headers(transactionid)
295
	
296
	def delete_mms_message(self, fname):
297
		fullpath = self.store.get_filepath_for_mms_transid(fname)
298
		if fullpath:
299
			fullpath = fullpath.replace("/message", "")
300
		else:
301
			fullpath = self._mmsdir + fname
302
303
		log.info("fullpath: %s", fullpath)
304
		if os.path.isdir(fullpath):
305
			log.info("starting deletion of %s", fullpath)
306
			filelist = os.listdir(fullpath)
307
			log.info("removing: %s", filelist)
308
			for fn in filelist:
309
				try:
310
					fullfn = fullpath + "/" + fn
311
					os.remove(fullfn)
312
				except:
313
					log.info("failed to remove: %s", fullfn)
314
			try:
315
				log.info("trying to remove: %s", fullpath)
316
				os.rmdir(fullpath)
317
			except OSError, e:
318
				log.exception("failed to remove: %s %s", type(e), e)
319
				raise
320
		
321
		self.store.delete_mms_message(fname)
322
		
323
	def delete_push_message(self, fname):
324
		fullpath = self.store.get_filepath_for_push_transid(fname)
325
		if not fullpath:
326
			fullpath = self._pushdir + fname
327
328
		log.info("fullpath: %s", fullpath)
329
		if os.path.isfile(fullpath):
330
			log.info("removing: %s", fullpath)
331
			try:
332
				os.remove(fullpath)
333
			except Exception, e:
334
				log.exception("%s %s", type(e), e)
335
				raise
336
		self.store.delete_push_message(fname)
337
		
338
	def wipe_message(self, transactionid):
339
		self.delete_mms_message(transactionid)
340
		self.delete_push_message(transactionid)
341
	
342
	def save_draft(self, rcpt, text, attachment):
343
		if not rcpt:
344
			rcpt = ""
345
		if not text:
346
			text = ""
347
		if not attachment:
348
			attachment = ""
349
		self.store.save_draft(rcpt, text, attachment)
350
351
	def get_draft(self):
352
		return self.store.get_draft()
353
354
	def validate_phonenumber_email(self, nr):
355
		nr = str(nr)
356
		nr = nr.replace("+", "")
357
		nr = nr.replace(" ", "")
358
		if re.search(r"(\D)+", nr) == None or "@" in nr or ";" in nr:
359
			return True
360
		else:
361
		 	return False
362
		
363
	def get_mcc_mnc(self):
364
		""" Gets the SIM cards MMC/MNC """
365
		bus = dbus.SystemBus()
366
		phone = dbus.Interface(bus.get_object("com.nokia.phone.SIM", "/com/nokia/phone/SIM"), "Phone.Sim")
367
		hplmn = phone.read_hplmn()
368
		(mcc, thirdbytes, mnc) = hplmn[0]
369
		# extract 4 lowest bits (as integer)
370
		mcc1 = int(mcc) & 0xF
371
		# and 4 highest bits (as integer)
372
		mcc2 = int(mcc) >> 4
373
		# 4 lowest bits of the "united" byte is mcc3
374
		mcc3 = int(thirdbytes) & 0xF
375
		# 4 lowest bits of mnc is mnc1
376
		mnc1 = int(mnc) & 0xF
377
		# 4 highest bits of mnc is mnc2
378
		mnc2 = int(mnc) >> 4
379
		# if 4 highest bits of "united" byte is
380
		# 0xf only 2 digits are used for mnc
381
		mnc3 = int(thirdbytes) >> 4
382
383
		if mnc3 == 0xf:
384
			final_mnc = "%s%s" % (mnc1, mnc2)
385
		else:
386
			final_mnc = "%s%s%s" % (mnc1, mnc2, mnc3)
387
388
		final_mcc = "%s%s%s" % (mcc1, mcc2, mcc3)
389
		
390
		return final_mcc, final_mnc
391
392
	def get_current_connection_iap_id(self):
393
		bus = dbus.SystemBus()
394
		icd = dbus.Interface(bus.get_object("com.nokia.icd", "/com/nokia/icd"), "com.nokia.icd")
395
		(iap, ign, ign, ign, ign, ign, ign) = icd.get_statistics()
396
		return iap
397
398
	def disconnect_current_connection(self):
399
		args = "DISCONNECT"
400
		retcode = subprocess.call(["/opt/fmms/fmms_magic", args])
401
402
	def get_operator_display_name(self):
403
		bus = dbus.SystemBus()
404
		phone = dbus.Interface(bus.get_object("com.nokia.phone.SIM", "/com/nokia/phone/SIM"), "Phone.Sim")
405
		(providername, ign, ign2, err) = phone.get_service_provider_name()
406
		return providername
407
408
	def get_settings_from_file(self, mcc, mnc, displayname):
409
		fn = open("/etc/operator_settings", 'r')
410
		mcc = str(int(mcc))
411
		mnc = str(int(mnc))
412
		for line in fn:
413
			"""
414
			0 : MCC
415
			1 : MNC
416
			2 : SERVICE PROVIDER NAME
417
			3 : ACCESS TYPE
418
			4 : IAP NAME
419
			5 : GPRS ACCESSPOINT NAME
420
			6 : GPRS AUTOLOGIN (X = YES)
421
			7 : USERNAME
422
			8 : PASSWORD
423
			9 : <empty>
424
			10 : <empty>
425
			11 : GPRS MMS/WAP GATEWAY PROXY
426
			12 : GPRS MMS/WAP GATEWAY PORT
427
			13 : IP ADDRESS (IF NOT FETCHED FROM SERVER)
428
			14 : PRIMARY DNS ADDRESS (IF NOT FETCHED FROM SERVER)
429
			15 : SECONDARY DNS ADDRESS (IF AVAILABLE)
430
			16 : <empty>
431
			"""
432
			row = line.split('\t')
433
			if row[0] == mcc and row[1] == mnc and row[3] == 'MMS' and displayname.lower() in row[2].lower():
434
				settings = {}
435
				settings['apn'] = row[5]
436
				settings['user'] = row[7]
437
				settings['pass'] = row[8]
438
				settings['proxy'] = row[11]
439
				settings['proxyport'] = row[12]
440
				settings['ip'] = row[13]
441
				settings['pdns'] = row[14]
442
				settings['sdns'] = row[15]
443
				settings['mmsc'] = row[17]
444
				return settings
445
		return None
446
447
	def get_apn_settings_automatically(self):
448
		(mcc, mnc) = self.get_mcc_mnc()
449
		operatorname = self.get_operator_display_name()
450
		settings = self.get_settings_from_file(mcc, mnc, operatorname)
451
		
452
		if self.are_we_tele2_se(mcc, mnc, operatorname):
453
			settings = self.are_we_tele2_se(mcc, mnc, operatorname)
454
		
455
		log.info("Settings loaded automatically. MCC: %s MNC: %s Operatorname: %s" % (mcc, mnc, operatorname))
456
		log.info("Settings loaded automatically: %s" % settings)
457
		return settings
458
	
459
	def are_we_tele2_se(self, mcc, mnc, operatorname):
460
		mcc = str(int(mcc))
461
		mnc = str(int(mnc))
462
		if mcc == "240" and mnc == "7":
463
			if "Tele2" in operatorname:
464
				settings = {}
465
				settings['apn'] = "internet.tele2.se"
466
				settings['user'] = ""
467
				settings['pass'] = ""
468
				settings['proxy'] = "130.244.202.30"
469
				settings['proxyport'] = "8080"
470
				settings['mmsc'] = "http://mmsc.tele2.se"
471
				settings['pdns'] = "0.0.0.0"
472
				settings['sdns'] = "0.0.0.0"
473
				settings['ip'] = "0.0.0.0"
474
				return settings
475
		return False
476
477
	def reset_all_settings(self):
478
		self.config.reset_all_settings()
479
480
481
class Locker:
482
	def __init__(self, fn):
483
		self.fn = fn
484
		self.fd = None
485
		self.pid = os.getpid()
486
		self.failcounter = 0
487
	
488
	def lock(self):
489
		try:
490
			self.fd = os.open(self.fn, os.O_CREAT | os.O_EXCL | os.O_RDWR)
491
			os.write(self.fd, "%d" % self.pid)
492
			return 1
493
		except OSError:
494
			# we failed to lock
495
			self.fd = None
496
			self.failcounter += 1
497
			# after 5 unsuccessful locks we check the owner
498
			# is still alive
499
			if self.failcounter > 4:
500
				pid = open(self.fn, 'r').read()
501
				try:
502
					os.kill(int(pid), 0)
503
				except OSError, e:
504
					# no such process, remove the lock
505
					os.remove(self.fn)
506
			return 0
507
	
508
	def unlock(self):
509
		if not self.fd:
510
			return 0
511
		try:
512
			os.close(self.fd)
513
			os.remove(self.fn)
514
			return 1
515
		except OSError:
516
			return 0
517
			
518
	def __del__(self):
519
		# deconstructor, make sure lock is released
520
		self.unlock()
521
522
if __name__ == '__main__':
523
	c = fMMS_controller()