1
2
3 """ Messaging API
4
5 API to send & receive messages:
6 - currently SMS, Email, RSS & Twitter
7
8 Messages get sent to the Outbox (& Log)
9 From there, the Scheduler tasks collect them & send them
10
11 @copyright: 2009-2019 (c) Sahana Software Foundation
12 @license: MIT
13
14 Permission is hereby granted, free of charge, to any person
15 obtaining a copy of this software and associated documentation
16 files (the "Software"), to deal in the Software without
17 restriction, including without limitation the rights to use,
18 copy, modify, merge, publish, distribute, sublicense, and/or sell
19 copies of the Software, and to permit persons to whom the
20 Software is furnished to do so, subject to the following
21 conditions:
22
23 The above copyright notice and this permission notice shall be
24 included in all copies or substantial portions of the Software.
25
26 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
28 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
30 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
31 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
33 OTHER DEALINGS IN THE SOFTWARE.
34
35 """
36
37 __all__ = ("S3Msg",
38 "S3Compose",
39 )
40
41 import base64
42 import datetime
43 import json
44 import os
45 import re
46 import string
47 import urllib
48 import urllib2
49
50 try:
51 from cStringIO import StringIO
52 except:
53 from StringIO import StringIO
54
55 try:
56 from lxml import etree
57 except ImportError:
58 import sys
59 sys.stderr.write("ERROR: lxml module needed for XML handling\n")
60 raise
61
62 from gluon import current, redirect
63 from gluon.html import *
64
65
66 from s3crud import S3CRUD
67 from s3datetime import s3_decode_iso_datetime
68 from s3forms import S3SQLDefaultForm
69 from s3utils import s3_unicode
70 from s3validators import IS_IN_SET, IS_ONE_OF
71 from s3widgets import S3PentityAutocompleteWidget
72
73 IDENTITYTRANS = ALLCHARS = string.maketrans("", "")
74 NOTPHONECHARS = ALLCHARS.translate(IDENTITYTRANS, string.digits)
75 NOTTWITTERCHARS = ALLCHARS.translate(IDENTITYTRANS,
76 "%s%s_" % (string.digits, string.letters))
77
78 TWITTER_MAX_CHARS = 140
79 TWITTER_HAS_NEXT_SUFFIX = u' \u2026'
80 TWITTER_HAS_PREV_PREFIX = u'\u2026 '
81
82 SENDER = re.compile(r"(.*)\s*\<(.+@.+)\>\s*")
83
84
85 -class S3Msg(object):
86 """ Messaging framework """
87
90
91 T = current.T
92 self.modem = modem
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117 MOBILE = current.deployment_settings.get_ui_label_mobile_phone()
118
119 self.CONTACT_OPTS = {"EMAIL": T("Email"),
120 "FACEBOOK": T("Facebook"),
121 "FAX": T("Fax"),
122 "HOME_PHONE": T("Home Phone"),
123 "RADIO": T("Radio Callsign"),
124 "RSS": T("RSS Feed"),
125 "SKYPE": T("Skype"),
126 "SMS": MOBILE,
127 "TWITTER": T("Twitter"),
128 "WHATSAPP": T("WhatsApp"),
129
130
131 "WORK_PHONE": T("Work phone"),
132 "IRC": T("IRC handle"),
133 "GITHUB": T("Github Repo"),
134 "GCM": T("Google Cloud Messaging"),
135 "LINKEDIN": T("LinkedIn Profile"),
136 "BLOG": T("Blog"),
137 "OTHER": T("Other")
138 }
139
140
141
142 self.MSG_CONTACT_OPTS = {"EMAIL": T("Email"),
143 "SMS": MOBILE,
144 "TWITTER": T("Twitter"),
145 "FACEBOOK": T("Facebook"),
146
147 }
148
149
150 self.GATEWAY_OPTS = {"MODEM": T("Modem"),
151 "SMTP": T("SMTP"),
152 "TROPO": T("Tropo"),
153
154
155 "WEB_API": T("Web API"),
156 }
157
158
159 @staticmethod
161 """
162 Strip out unnecessary characters from the string:
163 +()- & space
164 """
165
166 settings = current.deployment_settings
167 table = current.s3db.msg_sms_outbound_gateway
168
169 if channel_id:
170 row = current.db(table.channel_id == channel_id) \
171 .select(limitby=(0, 1)).first()
172 default_country_code = row["msg_sms_outbound_gateway.default_country_code"]
173 else:
174 default_country_code = settings.get_L10n_default_country_code()
175
176 clean = phone.translate(IDENTITYTRANS, NOTPHONECHARS)
177
178
179 if clean[0] == "0":
180
181 if default_country_code == 39:
182
183 clean = "%s%s" % (default_country_code, clean)
184 else:
185 clean = "%s%s" % (default_country_code,
186 clean.lstrip("0"))
187
188 return clean
189
190
191 @staticmethod
193 """
194 Decode an RFC2047-encoded email header (e.g.
195 "Dominic =?ISO-8859-1?Q?K=F6nig?=") and return it as unicode.
196
197 @param header: the header
198 """
199
200
201 header = re.sub(r"(=\?.*\?=)(?!$)", r"\1 ", header)
202
203
204 from email.header import decode_header
205 decoded = decode_header(header)
206
207
208 return " ".join([s3_unicode(part[0], part[1] or "ASCII")
209 for part in decoded])
210
211
212
213
214 @staticmethod
216 """
217 Helper method to sort messages according to sender priority.
218 """
219
220 s3db = current.s3db
221 db = current.db
222 ptable = s3db.msg_parsing_status
223 mtable = s3db.msg_message
224 stable = s3db.msg_sender
225
226 try:
227
228 pmessage = db(ptable.id == row.id).select(ptable.message_id)
229 m_id = pmessage.message_id
230
231 message = db(mtable.id == m_id).select(mtable.from_address,
232 limitby=(0, 1)).first()
233 sender = message.from_address
234
235 srecord = db(stable.sender == sender).select(stable.priority,
236 limitby=(0, 1)).first()
237
238 return srecord.priority
239 except:
240 import sys
241
242 return sys.maxint
243
244
245 @staticmethod
246 - def parse(channel_id, function_name):
247 """
248 Parse unparsed Messages from Channel with Parser
249 - called from Scheduler
250
251 @param channel_id: Channel
252 @param function_name: Parser
253 """
254
255 from s3parser import S3Parsing
256
257 parser = S3Parsing.parser
258 stable = current.s3db.msg_parsing_status
259 query = (stable.channel_id == channel_id) & \
260 (stable.is_parsed == False)
261 messages = current.db(query).select(stable.id,
262 stable.message_id)
263 for message in messages:
264
265 reply_id = parser(function_name, message.message_id)
266
267 message.update_record(is_parsed=True,
268 reply_id=reply_id)
269 return
270
271
272
273
274 - def compose(self,
275 type = "SMS",
276 recipient_type = None,
277 recipient = None,
278
279
280
281 subject = "",
282 message = "",
283 url = None,
284
285
286 ):
287 """
288 Form to Compose a Message
289
290 @param type: The default message type: None, EMAIL, SMS or TWITTER
291 @param recipient_type: Send to Persons or Groups? (pr_person or pr_group)
292 @param recipient: The pe_id of the person/group to send the message to
293 - this can also be set by setting one of
294 (in priority order, if multiple found):
295 request.vars.pe_id
296 request.vars.person_id @ToDo
297 request.vars.group_id @ToDo
298 request.vars.hrm_id @ToDo
299 @param subject: The default subject text (for Emails)
300 @param message: The default message text
301 @param url: Redirect to the specified URL() after message sent
302 @param formid: If set, allows multiple forms open in different tabs
303 """
304
305 if not url:
306 url = URL(c="msg", f="compose")
307
308
309 auth = current.auth
310 if auth.is_logged_in() or auth.basic():
311 pass
312 else:
313 redirect(URL(c="default", f="user", args="login",
314 vars={"_next" : url}))
315
316
317 if not auth.permission.has_permission("update", c="msg"):
318 current.session.error = current.T("You do not have permission to send messages")
319 redirect(URL(f="index"))
320
321
322 instance = S3Compose()
323 instance.contact_method = type
324 instance.recipient = recipient
325 instance.recipients = None
326 instance.recipient_type = recipient_type
327 instance.subject = subject
328 instance.message = message
329
330 instance.resource = None
331 instance.url = url
332
333
334 form = instance._compose_form()
335
336
337
338 title = current.T("Send Message")
339
340 return dict(form = form,
341 title = title)
342
343
344 @staticmethod
345 - def send(recipient, message, subject=None):
346 """
347 Send a single message to an Address
348
349 @param recipient: "email@address", "+4412345678", "@nick"
350 @param message: message body
351 @param subject: message subject (Email only)
352 """
353
354
355 if recipient.startswith("@"):
356
357 tablename = "msg_twitter"
358 elif "@" in recipient:
359
360 tablename = "msg_email"
361 else:
362
363 tablename = "msg_sms"
364
365
366
367
368 @staticmethod
369 - def send_by_pe_id(pe_id,
370 subject = "",
371 message = "",
372 contact_method = "EMAIL",
373 document_ids = None,
374 from_address = None,
375 system_generated = False,
376 **data):
377 """
378 Send a single message to a Person Entity (or list thereof)
379
380 @ToDo: contact_method = ALL
381 - look up the pr_contact options available for the pe & send via all
382
383 @ToDo: This is not transaction safe
384 - power failure in the middle will cause no message in the outbox
385 """
386
387 s3db = current.s3db
388
389
390 if contact_method == "EMAIL":
391 if not from_address:
392
393
394 from_address = current.deployment_settings.get_mail_sender()
395
396 table = s3db.msg_email
397 _id = table.insert(body=message,
398 subject=subject,
399 from_address=from_address,
400
401 inbound=False,
402 )
403 record = dict(id=_id)
404 s3db.update_super(table, record)
405 message_id = record["message_id"]
406 if document_ids:
407 ainsert = s3db.msg_attachment.insert
408 if not isinstance(document_ids, list):
409 document_ids = [document_ids]
410 for document_id in document_ids:
411 ainsert(message_id=message_id,
412 document_id=document_id,
413 )
414
415 elif contact_method == "SMS":
416 table = s3db.msg_sms
417 _id = table.insert(body=message,
418 from_address=from_address,
419 inbound=False,
420 )
421 record = dict(id=_id)
422 s3db.update_super(table, record)
423 message_id = record["message_id"]
424 elif contact_method == "TWITTER":
425 table = s3db.msg_twitter
426 _id = table.insert(body=message,
427 from_address=from_address,
428 inbound=False,
429 )
430 record = dict(id=_id)
431 s3db.update_super(table, record)
432 message_id = record["message_id"]
433 else:
434
435 raise NotImplementedError
436
437
438 table = s3db.msg_outbox
439 if isinstance(pe_id, list):
440
441 listindex = 0
442 insert = table.insert
443 for _id in pe_id:
444 try:
445 insert(message_id = message_id,
446 pe_id = _id,
447 contact_method = contact_method,
448 system_generated = system_generated)
449 listindex = listindex + 1
450 except:
451 return listindex
452 else:
453 try:
454 table.insert(message_id = message_id,
455 pe_id = pe_id,
456 contact_method = contact_method,
457 system_generated = system_generated)
458 except:
459 return False
460
461
462 current.s3task.async("msg_process_outbox",
463 args = [contact_method])
464
465
466 postp = current.deployment_settings.get_msg_send_postprocess()
467 if postp:
468 postp(message_id, **data)
469
470 return message_id
471
472
474 """
475 Send pending messages from outbox (usually called from scheduler)
476
477 @param contact_method: the output channel (see pr_contact.method)
478
479 @todo: contact_method = "ALL"
480 """
481
482 db = current.db
483 s3db = current.s3db
484
485 lookup_org = False
486 channels = {}
487 outgoing_sms_handler = None
488 channel_id = None
489
490 if contact_method == "SMS":
491
492
493 table = s3db.msg_sms_outbound_gateway
494 etable = db.msg_channel
495 query = (table.deleted == False) & \
496 (table.channel_id == etable.channel_id)
497 rows = db(query).select(table.channel_id,
498 table.organisation_id,
499 etable.instance_type,
500 )
501 if not rows:
502
503
504 raise ValueError("No SMS handler defined!")
505
506 if len(rows) == 1:
507 lookup_org = False
508 row = rows.first()
509 outgoing_sms_handler = row["msg_channel.instance_type"]
510 channel_id = row["msg_sms_outbound_gateway.channel_id"]
511 else:
512 lookup_org = True
513 org_branches = current.deployment_settings.get_org_branches()
514 if org_branches:
515 org_parents = s3db.org_parents
516 for row in rows:
517 channels[row["msg_sms_outbound_gateway.organisation_id"]] = \
518 dict(outgoing_sms_handler = row["msg_channel.instance_type"],
519 channel_id = row["msg_sms_outbound_gateway.channel_id"])
520
521 elif contact_method == "TWITTER":
522 twitter_settings = self.get_twitter_api()
523 if not twitter_settings:
524
525
526 raise ValueError("No Twitter API available!")
527
528 def dispatch_to_pe_id(pe_id,
529 subject,
530 message,
531 outbox_id,
532 message_id,
533 attachments = [],
534 organisation_id = None,
535 contact_method = contact_method,
536 channel_id = channel_id,
537 from_address = None,
538 outgoing_sms_handler = outgoing_sms_handler,
539 lookup_org = lookup_org,
540 channels = channels):
541 """
542 Helper method to send messages by pe_id
543
544 @param pe_id: the pe_id
545 @param subject: the message subject
546 @param message: the message body
547 @param outbox_id: the outbox record ID
548 @param message_id: the message_id
549 @param organisation_id: the organisation_id (for SMS)
550 @param contact_method: the contact method
551 """
552
553
554 table = s3db.pr_contact
555 query = (table.pe_id == pe_id) & \
556 (table.contact_method == contact_method) & \
557 (table.deleted == False)
558 contact_info = db(query).select(table.value,
559 orderby=table.priority,
560 limitby=(0, 1)).first()
561
562 if contact_info:
563 address = contact_info.value
564 if contact_method == "EMAIL":
565 return self.send_email(address,
566 subject,
567 message,
568 sender = from_address,
569 attachments = attachments,
570 )
571 elif contact_method == "SMS":
572 if lookup_org:
573 channel = channels.get(organisation_id)
574 if not channel and \
575 org_branches:
576 orgs = org_parents(organisation_id)
577 for org in orgs:
578 channel = channels.get(org)
579 if channel:
580 break
581 if not channel:
582
583 channel = channels.get(None)
584 if not channel:
585
586 return False
587 outgoing_sms_handler = channel["outgoing_sms_handler"]
588 channel_id = channel["channel_id"]
589 if outgoing_sms_handler == "msg_sms_webapi_channel":
590 return self.send_sms_via_api(address,
591 message,
592 message_id,
593 channel_id)
594 elif outgoing_sms_handler == "msg_sms_smtp_channel":
595 return self.send_sms_via_smtp(address,
596 message,
597 channel_id)
598 elif outgoing_sms_handler == "msg_sms_modem_channel":
599 return self.send_sms_via_modem(address,
600 message,
601 channel_id)
602 elif outgoing_sms_handler == "msg_sms_tropo_channel":
603
604 return self.send_sms_via_tropo(outbox_id,
605 message_id,
606 address,
607 message,
608 channel_id)
609 elif contact_method == "TWITTER":
610 return self.send_tweet(message, address)
611
612 return False
613
614 outbox = s3db.msg_outbox
615
616 query = (outbox.contact_method == contact_method) & \
617 (outbox.status == 1) & \
618 (outbox.deleted == False)
619
620 petable = s3db.pr_pentity
621 left = [petable.on(petable.pe_id == outbox.pe_id)]
622
623 fields = [outbox.id,
624 outbox.message_id,
625 outbox.pe_id,
626 outbox.retries,
627 petable.instance_type,
628 ]
629
630 if contact_method == "EMAIL":
631 mailbox = s3db.msg_email
632 fields.extend([mailbox.subject, mailbox.body, mailbox.from_address])
633 left.append(mailbox.on(mailbox.message_id == outbox.message_id))
634 elif contact_method == "SMS":
635 mailbox = s3db.msg_sms
636 fields.append(mailbox.body)
637 if lookup_org:
638 fields.append(mailbox.organisation_id)
639 left.append(mailbox.on(mailbox.message_id == outbox.message_id))
640 elif contact_method == "TWITTER":
641 mailbox = s3db.msg_twitter
642 fields.append(mailbox.body)
643 left.append(mailbox.on(mailbox.message_id == outbox.message_id))
644 else:
645
646 raise NotImplementedError
647
648 rows = db(query).select(*fields,
649 left=left,
650 orderby=~outbox.retries)
651 if not rows:
652 return
653
654 htable = s3db.table("hrm_human_resource")
655 otable = s3db.org_organisation
656 ptable = s3db.pr_person
657 gtable = s3db.pr_group
658 mtable = db.pr_group_membership
659 ftable = s3db.pr_forum
660 fmtable = db.pr_forum_membership
661
662
663 gleft = [mtable.on((mtable.group_id == gtable.id) & \
664 (mtable.person_id != None) & \
665 (mtable.deleted != True)),
666 ptable.on((ptable.id == mtable.person_id) & \
667 (ptable.deleted != True))
668 ]
669 fleft = [fmtable.on((fmtable.forum_id == ftable.id) & \
670 (fmtable.person_id != None) & \
671 (fmtable.deleted != True)),
672 ptable.on((ptable.id == fmtable.person_id) & \
673 (ptable.deleted != True))
674 ]
675
676 if htable:
677 oleft = [htable.on((htable.organisation_id == otable.id) & \
678 (htable.person_id != None) & \
679 (htable.deleted != True)),
680 ptable.on((ptable.id == htable.person_id) & \
681 (ptable.deleted != True)),
682 ]
683
684 etable = s3db.hrm_training_event
685 ttable = s3db.hrm_training
686 tleft = [ttable.on((ttable.training_event_id == etable.id) & \
687 (ttable.person_id != None) & \
688 (ttable.deleted != True)),
689 ptable.on((ptable.id == ttable.person_id) & \
690 (ptable.deleted != True)),
691 ]
692
693 atable = s3db.table("deploy_alert")
694 if atable:
695 ltable = db.deploy_alert_recipient
696 aleft = [ltable.on(ltable.alert_id == atable.id),
697 htable.on((htable.id == ltable.human_resource_id) & \
698 (htable.person_id != None) & \
699 (htable.deleted != True)),
700 ptable.on((ptable.id == htable.person_id) & \
701 (ptable.deleted != True))
702 ]
703
704
705
706 chainrun = False
707
708
709 organisation_id = None
710 attachment_table = s3db.msg_attachment
711 document_table = s3db.doc_document
712 file_field = document_table.file
713 if file_field.custom_retrieve_file_properties:
714 retrieve_file_properties = file_field.custom_retrieve_file_properties
715 else:
716 retrieve_file_properties = file_field.retrieve_file_properties
717 mail_attachment = current.mail.Attachment
718
719 for row in rows:
720 attachments = []
721 status = True
722 message_id = row.msg_outbox.message_id
723
724 if contact_method == "EMAIL":
725 subject = row["msg_email.subject"] or ""
726 message = row["msg_email.body"] or ""
727 from_address = row["msg_email.from_address"] or ""
728 query = (attachment_table.message_id == message_id) & \
729 (attachment_table.deleted != True) & \
730 (attachment_table.document_id == document_table.id) & \
731 (document_table.deleted != True)
732 arows = db(query).select(file_field)
733 for arow in arows:
734 file = arow.file
735 prop = retrieve_file_properties(file)
736 _file_path = os.path.join(prop["path"], file)
737 attachments.append(mail_attachment(_file_path))
738 elif contact_method == "SMS":
739 subject = None
740 message = row["msg_sms.body"] or ""
741 from_address = None
742 if lookup_org:
743 organisation_id = row["msg_sms.organisation_id"]
744 elif contact_method == "TWITTER":
745 subject = None
746 message = row["msg_twitter.body"] or ""
747 from_address = None
748 else:
749
750 continue
751
752 entity_type = row["pr_pentity"].instance_type
753 if not entity_type:
754 current.log.warning("s3msg", "Entity type unknown")
755 continue
756
757 row = row["msg_outbox"]
758 pe_id = row.pe_id
759 message_id = row.message_id
760
761 if entity_type == "pr_person":
762
763 try:
764 status = dispatch_to_pe_id(
765 pe_id,
766 subject,
767 message,
768 row.id,
769 message_id,
770 organisation_id = organisation_id,
771 from_address = from_address,
772 attachments = attachments,
773 )
774 except:
775 status = False
776
777 elif entity_type == "pr_group":
778
779 gquery = (gtable.pe_id == pe_id)
780 recipients = db(gquery).select(ptable.pe_id, left=gleft)
781 pe_ids = set(r.pe_id for r in recipients)
782 pe_ids.discard(None)
783 if pe_ids:
784 for pe_id in pe_ids:
785 outbox.insert(message_id=message_id,
786 pe_id=pe_id,
787 contact_method=contact_method,
788 system_generated=True)
789 chainrun = True
790 status = True
791
792 elif entity_type == "pr_forum":
793
794 fquery = (ftable.pe_id == pe_id)
795 recipients = db(fquery).select(ptable.pe_id, left=fleft)
796 pe_ids = set(r.pe_id for r in recipients)
797 pe_ids.discard(None)
798 if pe_ids:
799 for pe_id in pe_ids:
800 outbox.insert(message_id=message_id,
801 pe_id=pe_id,
802 contact_method=contact_method,
803 system_generated=True)
804 chainrun = True
805 status = True
806
807 elif htable and entity_type == "org_organisation":
808
809 oquery = (otable.pe_id == pe_id)
810 recipients = db(oquery).select(ptable.pe_id, left=oleft)
811 pe_ids = set(r.pe_id for r in recipients)
812 pe_ids.discard(None)
813 if pe_ids:
814 for pe_id in pe_ids:
815 outbox.insert(message_id=message_id,
816 pe_id=pe_id,
817 contact_method=contact_method,
818 system_generated=True)
819 chainrun = True
820 status = True
821
822 elif entity_type == "hrm_training_event":
823
824 equery = (etable.pe_id == pe_id)
825 recipients = db(equery).select(ptable.pe_id, left=tleft)
826 pe_ids = set(r.pe_id for r in recipients)
827 pe_ids.discard(None)
828 if pe_ids:
829 for pe_id in pe_ids:
830 outbox.insert(message_id=message_id,
831 pe_id=pe_id,
832 contact_method=contact_method,
833 system_generated=True)
834 chainrun = True
835 status = True
836
837 elif atable and entity_type == "deploy_alert":
838
839 aquery = (atable.pe_id == pe_id)
840 recipients = db(aquery).select(ptable.pe_id, left=aleft)
841 pe_ids = set(r.pe_id for r in recipients)
842 pe_ids.discard(None)
843 if pe_ids:
844 for pe_id in pe_ids:
845 outbox.insert(message_id=message_id,
846 pe_id=pe_id,
847 contact_method=contact_method,
848 system_generated=True)
849 chainrun = True
850 status = True
851
852 else:
853
854 row.update_record(status = 4)
855 db.commit()
856 continue
857
858 if status:
859 row.update_record(status = 2)
860 db.commit()
861 else:
862 if row.retries > 0:
863 row.update_record(retries = row.retries - 1)
864 db.commit()
865 elif row.retries is not None:
866 row.update_record(status = 5)
867
868 if chainrun:
869 self.process_outbox(contact_method)
870
871
872
873
874 - def gcm_push(self, title=None, uri=None, message=None, registration_ids=None, channel_id=None):
875 """
876 Push the message relating to google cloud messaging server
877
878 @param title: The title for notification
879 @param message: The message to be sent to GCM server
880 @param api_key: The API key for GCM server
881 @param registration_ids: The list of id that will be notified
882 @param channel_id: The specific channel_id to use for GCM push
883 """
884
885 if not title or not uri or not message or not len(registration_ids):
886 return
887
888 from gcm import GCM
889 gcmtable = current.s3db.msg_gcm_channel
890 if channel_id:
891 query = (gcmtable.channel_id == channel_id)
892 else:
893 query = (gcmtable.enabled == True) & (gcmtable.deleted != True)
894
895 row = current.db(query).select(gcmtable.api_key,
896 limitby=(0, 1)).first()
897 try:
898 gcm = GCM(row.api_key)
899 except Exception as e:
900 current.log.error("No API Key configured for GCM: ", e)
901 else:
902 try:
903
904
905
906 notification = {"title": title,
907 "message": message,
908 "uri": uri,
909 }
910 response = gcm.json_request(registration_ids=registration_ids,
911 data=notification)
912 except Exception as e:
913 current.log.error("Google Cloud Messaging Error", e)
914 else:
915 if "errors" in response:
916 for error, reg_ids in response["errors"].items():
917 for reg_id in reg_ids:
918 current.log.error("Google Cloud Messaging Error: %s for Registration ID %s" % (error, reg_id))
919
920
921
922
923 return
924
925
926
927
928 - def send_email(self,
929 to,
930 subject,
931 message,
932 attachments=None,
933 cc=None,
934 bcc=None,
935 reply_to=None,
936 sender=None,
937 encoding="utf-8",
938
939 ):
940 """
941 Function to send Email
942 - simple Wrapper over Web2Py's Email API
943 """
944
945 if not to:
946 return False
947
948 settings = current.deployment_settings
949
950 default_sender = settings.get_mail_sender()
951 if not default_sender:
952 current.log.warning("Email sending disabled until the Sender address has been set in models/000_config.py")
953 return False
954
955 if not sender:
956 sender = default_sender
957 sender = self.sanitize_sender(sender)
958
959 limit = settings.get_mail_limit()
960 if limit:
961
962 day = datetime.timedelta(hours=24)
963 cutoff = current.request.utcnow - day
964 table = current.s3db.msg_channel_limit
965
966 check = current.db(table.created_on > cutoff).count()
967 if check >= limit:
968 return False
969
970 table.insert()
971
972 result = current.mail.send(to,
973 subject=subject,
974 message=message,
975 attachments=attachments,
976 cc=cc,
977 bcc=bcc,
978 reply_to=reply_to,
979 sender=sender,
980 encoding=encoding,
981
982 headers={},
983
984
985
986 )
987 if not result:
988 current.session.error = current.mail.error
989 else:
990 current.session.error = None
991
992 return result
993
994
995 @staticmethod
997 """
998 Sanitize the email sender string to prevent MIME-encoding
999 of the from-address (RFC2047)
1000
1001 @param: the sender-string
1002 @returns: the sanitized sender-string
1003 """
1004
1005 if not sender:
1006 return sender
1007 match = SENDER.match(sender)
1008 if match:
1009 sender_name, from_address = match.groups()
1010 if any(32 > ord(c) or ord(c) > 127 for c in sender_name):
1011 from email.header import Header
1012 sender_name = Header(sender_name.strip(), "utf-8")
1013 else:
1014 sender_name = sender_name.strip()
1015 sender = "%s <%s>" % (sender_name, from_address.strip())
1016 return sender
1017
1018
1019 - def send_email_by_pe_id(self,
1020 pe_id,
1021 subject="",
1022 message="",
1023 from_address=None,
1024 system_generated=False):
1025 """
1026 API wrapper over send_by_pe_id
1027 """
1028
1029 return self.send_by_pe_id(pe_id,
1030 subject,
1031 message,
1032 "EMAIL",
1033 from_address,
1034 system_generated)
1035
1036
1037
1038
1039
1040
1041
1042
1043 @staticmethod
1045 """
1046 Function to create an OpenGeoSMS
1047
1048 @param: location_id - reference to record in gis_location table
1049 @param: code - the type of OpenGeoSMS:
1050 S = Sahana
1051 SI = Incident Report
1052 ST = Task Dispatch
1053 @param: map: "google" or "osm"
1054 @param: text - the rest of the message
1055
1056 Returns the formatted OpenGeoSMS or None if it can't find
1057 an appropriate location
1058 """
1059
1060 if not location_id:
1061 return text
1062
1063 db = current.db
1064 s3db = current.s3db
1065 table = s3db.gis_location
1066 query = (table.id == location_id)
1067 location = db(query).select(table.lat,
1068 table.lon,
1069
1070
1071 limitby=(0, 1)).first()
1072 if not location:
1073 return text
1074 lat = location.lat
1075 lon = location.lon
1076 if lat is None or lon is None:
1077
1078 return text
1079
1080 code = "GeoSMS=%s" % code
1081
1082 if map == "google":
1083 url = "http://maps.google.com/?q=%f,%f" % (lat, lon)
1084 elif map == "osm":
1085
1086 url = "http://openstreetmap.org?mlat=%f&mlon=%f&zoom=14" % (lat, lon)
1087
1088 opengeosms = "%s&%s\n%s" % (url, code, text)
1089
1090 return opengeosms
1091
1092
1093 @staticmethod
1095 """
1096 Function to parse an OpenGeoSMS
1097 @param: message - Inbound message to be parsed for OpenGeoSMS.
1098 Returns the lat, lon, code and text contained in the message.
1099 """
1100
1101 lat = ""
1102 lon = ""
1103 code = ""
1104 text = ""
1105
1106 words = message.split(" ")
1107 if "http://maps.google.com/?q" in words[0]:
1108
1109 pwords = words[0].split("?q=")[1].split(",")
1110 lat = pwords[0]
1111 lon = pwords[1].split("&")[0]
1112 code = pwords[1].split("&")[1].split("=")[1]
1113 text = ""
1114 for a in range(1, len(words)):
1115 text = text + words[a] + " "
1116
1117
1118 return lat, lon, code, text
1119
1120
1121
1122
1123 - def send_sms_via_api(self,
1124 mobile,
1125 text = "",
1126 message_id = None,
1127 channel_id = None,
1128 ):
1129 """
1130 Function to send SMS via Web API
1131 """
1132
1133 db = current.db
1134 s3db = current.s3db
1135 table = s3db.msg_sms_webapi_channel
1136
1137
1138 if channel_id:
1139 sms_api = db(table.channel_id == channel_id).select(limitby=(0, 1)
1140 ).first()
1141 else:
1142
1143 sms_api = db(table.enabled == True).select(limitby=(0, 1)).first()
1144 if not sms_api:
1145 return False
1146
1147 post_data = {}
1148
1149 parts = sms_api.parameters.split("&")
1150 for p in parts:
1151 post_data[p.split("=")[0]] = p.split("=")[1]
1152
1153 mobile = self.sanitise_phone(mobile, channel_id)
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163 text_latin1 = s3_unicode(text).encode("utf-8") \
1164 .decode("utf-8") \
1165 .encode("iso-8859-1")
1166
1167 post_data[sms_api.message_variable] = text_latin1
1168 post_data[sms_api.to_variable] = str(mobile)
1169
1170 url = sms_api.url
1171 clickatell = "clickatell" in url
1172 if clickatell:
1173 text_len = len(text)
1174 if text_len > 480:
1175 current.log.error("Clickatell messages cannot exceed 480 chars")
1176 return False
1177 elif text_len > 320:
1178 post_data["concat"] = 3
1179 elif text_len > 160:
1180 post_data["concat"] = 2
1181
1182 request = urllib2.Request(url)
1183 query = urllib.urlencode(post_data)
1184 if sms_api.username and sms_api.password:
1185
1186 base64string = base64.encodestring("%s:%s" % (sms_api.username, sms_api.password)).replace("\n", "")
1187 request.add_header("Authorization", "Basic %s" % base64string)
1188 try:
1189 result = urllib2.urlopen(request, query)
1190 except urllib2.HTTPError, e:
1191 current.log.error("SMS message send failed: %s" % e)
1192 return False
1193 else:
1194
1195 output = result.read()
1196 if clickatell:
1197 if output.startswith("ERR"):
1198 current.log.error("Clickatell message send failed: %s" % output)
1199 return False
1200 elif message_id and output.startswith("ID"):
1201
1202 remote_id = output[4:]
1203 db(s3db.msg_sms.message_id == message_id).update(remote_id=remote_id)
1204 elif "mcommons" in url:
1205
1206
1207
1208 if "error" in output:
1209 current.log.error("Mobile Commons message send failed: %s" % output)
1210 return False
1211
1212 return True
1213
1214
1216 """
1217 Function to send SMS via locally-attached Modem
1218 - needs to have the cron/sms_handler_modem.py script running
1219 """
1220
1221 mobile = self.sanitise_phone(mobile, channel_id)
1222
1223
1224 mobile = "+%s" % mobile
1225
1226 try:
1227 self.modem.send_sms(mobile, text)
1228 return True
1229 except KeyError:
1230 current.log.error("s3msg", "Modem not available: need to have the cron/sms_handler_modem.py script running")
1231 return False
1232
1233
1235 """
1236 Function to send SMS via SMTP
1237
1238 NB Different Gateways have different requirements for presence/absence of International code
1239
1240 http://en.wikipedia.org/wiki/List_of_SMS_gateways
1241 http://www.obviously.com/tech_tips/SMS_Text_Email_Gateway.html
1242 """
1243
1244 table = current.s3db.msg_sms_smtp_channel
1245 if channel_id:
1246 query = (table.channel_id == channel_id)
1247 else:
1248 query = (table.enabled == True)
1249 settings = current.db(query).select(limitby=(0, 1)
1250 ).first()
1251 if not settings:
1252 return False
1253
1254 mobile = self.sanitise_phone(mobile, channel_id)
1255
1256 to = "%s@%s" % (mobile,
1257 settings.address)
1258
1259 try:
1260 result = self.send_email(to=to,
1261 subject="",
1262 message= text)
1263 return result
1264 except:
1265 return False
1266
1267
1268 - def send_sms_via_tropo(self,
1269 row_id,
1270 message_id,
1271 recipient,
1272 message,
1273 network = "SMS",
1274 channel_id = None,
1275 ):
1276 """
1277 Send a URL request to Tropo to pick a message up
1278 """
1279
1280 db = current.db
1281 s3db = current.s3db
1282 table = s3db.msg_tropo_channel
1283
1284 base_url = "http://api.tropo.com/1.0/sessions"
1285 action = "create"
1286
1287 if channel_id:
1288 query = (table.channel_id == channel_id)
1289 else:
1290 query = (table.enabled == True)
1291 tropo_settings = db(query).select(table.token_messaging,
1292 limitby=(0, 1)).first()
1293 if tropo_settings:
1294 tropo_token_messaging = tropo_settings.token_messaging
1295
1296 else:
1297 return
1298
1299 if network == "SMS":
1300 recipient = self.sanitise_phone(recipient, channel_id)
1301
1302 try:
1303 s3db.msg_tropo_scratch.insert(row_id = row_id,
1304 message_id = message_id,
1305 recipient = recipient,
1306 message = message,
1307 network = network)
1308 params = urllib.urlencode([("action", action),
1309 ("token", tropo_token_messaging),
1310 ("outgoing", "1"),
1311 ("row_id", row_id)
1312 ])
1313 xml = urllib2.urlopen("%s?%s" % (base_url, params)).read()
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323 except:
1324 pass
1325
1326
1327 return False
1328
1329
1330 - def send_sms_by_pe_id(self,
1331 pe_id,
1332 message="",
1333 from_address=None,
1334 system_generated=False):
1335 """
1336 API wrapper over send_by_pe_id
1337 """
1338
1339 return self.send_by_pe_id(pe_id,
1340 subject = "",
1341 message = message,
1342 contact_method = "SMS",
1343 from_address = from_address,
1344 system_generated = system_generated,
1345 )
1346
1347
1348
1349
1350 @staticmethod
1352 """
1353 Only keep characters that are legal for a twitter account:
1354 letters, digits, and _
1355 """
1356
1357 return account.translate(IDENTITYTRANS, NOTTWITTERCHARS)
1358
1359
1360 @staticmethod
1365 """
1366 Breaks text to <=chunk_size long chunks. Tries to do this at a space.
1367 All chunks, except for last, end with suffix.
1368 All chunks, except for first, start with prefix.
1369 """
1370
1371 from s3 import s3_str
1372 res = []
1373 current_prefix = ""
1374 while text:
1375 if len(current_prefix + text) <= chunk_size:
1376 res.append(current_prefix + text)
1377 return res
1378 else:
1379 c = text[:chunk_size - len(current_prefix) - len(suffix)]
1380 i = c.rfind(" ")
1381 if i > 0:
1382 c = c[:i]
1383 text = text[len(c):].lstrip()
1384 res.append(current_prefix + c.rstrip() + s3_str(suffix))
1385 current_prefix = s3_str(prefix)
1386
1387
1388 @staticmethod
1390 """
1391 Initialize Twitter API
1392 """
1393
1394 try:
1395 import tweepy
1396 except ImportError:
1397 current.log.error("s3msg", "Tweepy not available, so non-Tropo Twitter support disabled")
1398 return None
1399
1400 table = current.s3db.msg_twitter_channel
1401 if not channel_id:
1402
1403 query = (table.enabled == True)
1404 limitby = None
1405 else:
1406 query = (table.channel_id == channel_id)
1407 limitby = (0, 1)
1408
1409 rows = current.db(query).select(table.login,
1410 table.twitter_account,
1411 table.consumer_key,
1412 table.consumer_secret,
1413 table.access_token,
1414 table.access_token_secret,
1415 limitby=limitby
1416 )
1417 if len(rows) == 1:
1418 c = rows.first()
1419 elif not len(rows):
1420 current.log.error("s3msg", "No Twitter channels configured")
1421 return None
1422 else:
1423
1424 rows.exclude(lambda row: row.login != True)
1425 if len(rows) == 1:
1426 c = rows.first()
1427 elif not len(rows):
1428 current.log.error("s3msg", "No Twitter channels configured for login")
1429 return None
1430
1431 if not c.consumer_key:
1432 current.log.error("s3msg", "Twitter channel has no consumer key")
1433 return None
1434
1435 try:
1436 oauth = tweepy.OAuthHandler(c.consumer_key,
1437 c.consumer_secret)
1438 oauth.set_access_token(c.access_token,
1439 c.access_token_secret)
1440 twitter_api = tweepy.API(oauth)
1441 return (twitter_api, c.twitter_account)
1442 except:
1443 return None
1444
1445
1447 """
1448 Function to tweet.
1449 If a recipient is specified then we send via direct message if the recipient follows us.
1450 - falls back to @mention (leaves less characters for the message).
1451 Breaks long text to chunks if needed.
1452
1453 @ToDo: Option to Send via Tropo
1454 """
1455
1456
1457 twitter_settings = self.get_twitter_api()
1458 if not twitter_settings:
1459
1460 return False
1461
1462 import tweepy
1463
1464 twitter_api = twitter_settings[0]
1465 twitter_account = twitter_settings[1]
1466
1467 from_address = twitter_api.me().screen_name
1468
1469 db = current.db
1470 s3db = current.s3db
1471 table = s3db.msg_twitter
1472 otable = s3db.msg_outbox
1473
1474 message_id = None
1475
1476 def log_tweet(tweet, recipient, from_address):
1477
1478 _id = table.insert(body=tweet,
1479 from_address=from_address,
1480 )
1481 record = db(table.id == _id).select(table.id,
1482 limitby=(0, 1)
1483 ).first()
1484 s3db.update_super(table, record)
1485 message_id = record.message_id
1486
1487
1488 otable.insert(message_id = message_id,
1489 address = recipient,
1490 status = 2,
1491 contact_method = "TWITTER",
1492 )
1493 return message_id
1494
1495 if recipient:
1496 recipient = self._sanitise_twitter_account(recipient)
1497 try:
1498 can_dm = recipient == twitter_account or \
1499 twitter_api.get_user(recipient).id in twitter_api.followers_ids(twitter_account)
1500 except tweepy.TweepError:
1501
1502 return False
1503 if can_dm:
1504 chunks = self._break_to_chunks(text)
1505 for c in chunks:
1506 try:
1507
1508
1509 if twitter_api.send_direct_message(screen_name=recipient,
1510 text=c):
1511 message_id = log_tweet(c, recipient, from_address)
1512
1513 except tweepy.TweepError:
1514 current.log.error("Unable to Tweet DM")
1515 else:
1516 prefix = "@%s " % recipient
1517 chunks = self._break_to_chunks(text,
1518 TWITTER_MAX_CHARS - len(prefix))
1519 for c in chunks:
1520 try:
1521 twitter_api.update_status("%s %s" % prefix, c)
1522 except tweepy.TweepError:
1523 current.log.error("Unable to Tweet @mention")
1524 else:
1525 message_id = log_tweet(c, recipient, from_address)
1526 else:
1527 chunks = self._break_to_chunks(text)
1528 for c in chunks:
1529 try:
1530 twitter_api.update_status(c)
1531 except tweepy.TweepError:
1532 current.log.error("Unable to Tweet")
1533 else:
1534 message_id = log_tweet(c, recipient, from_address)
1535
1536
1537 if message_id:
1538 postp = current.deployment_settings.get_msg_send_postprocess()
1539 if postp:
1540 postp(message_id, **data)
1541
1542 return True
1543
1544
1545 - def post_to_facebook(self, text="", channel_id=None, recipient=None, **data):
1546 """
1547 Posts a message on Facebook
1548
1549 https://developers.facebook.com/docs/graph-api
1550 """
1551
1552 db = current.db
1553 s3db = current.s3db
1554 table = s3db.msg_facebook_channel
1555 if not channel_id:
1556
1557 query = (table.enabled == True)
1558 else:
1559 query = (table.channel_id == channel_id)
1560
1561 c = db(query).select(table.app_id,
1562 table.app_secret,
1563 table.page_id,
1564 table.page_access_token,
1565 limitby=(0, 1)
1566 ).first()
1567
1568 import facebook
1569
1570 try:
1571 app_access_token = facebook.get_app_access_token(c.app_id,
1572 c.app_secret)
1573 except:
1574 import sys
1575 message = sys.exc_info()[1]
1576 current.log.error("S3MSG: %s" % message)
1577 return
1578
1579 table = s3db.msg_facebook
1580 otable = s3db.msg_outbox
1581
1582 message_id = None
1583
1584 def log_facebook(post, recipient, from_address):
1585
1586 _id = table.insert(body=post,
1587 from_address=from_address,
1588 )
1589 record = db(table.id == _id).select(table.id,
1590 limitby=(0, 1)
1591 ).first()
1592 s3db.update_super(table, record)
1593 message_id = record.message_id
1594
1595
1596 otable.insert(message_id = message_id,
1597 address = recipient,
1598 status = 2,
1599 contact_method = "FACEBOOK",
1600 )
1601 return message_id
1602
1603 graph = facebook.GraphAPI(app_access_token)
1604
1605 page_id = c.page_id
1606 if page_id:
1607 graph = facebook.GraphAPI(c.page_access_token)
1608 graph.put_object(page_id, "feed", message=text)
1609 else:
1610
1611
1612 raise NotImplementedError
1613
1614 message_id = log_facebook(text, recipient, channel_id)
1615
1616
1617 if message_id:
1618 postp = current.deployment_settings.get_msg_send_postprocess()
1619 if postp:
1620 postp(message_id, **data)
1621
1622
1623 - def poll(self, tablename, channel_id):
1624 """
1625 Poll a Channel for New Messages
1626 """
1627
1628 channel_type = tablename.split("_", 2)[1]
1629
1630 function_name = "poll_%s" % channel_type
1631 try:
1632 fn = getattr(S3Msg, function_name)
1633 except:
1634 error = "Unsupported Channel: %s" % channel_type
1635 current.log.error(error)
1636 return error
1637
1638 result = fn(channel_id)
1639 return result
1640
1641
1642 @staticmethod
1644 """
1645 This is a simple mailbox polling script for the Messaging Module.
1646 It is normally called from the scheduler.
1647
1648 @ToDo: If there is a need to collect from non-compliant mailers
1649 then suggest using the robust Fetchmail to collect & store
1650 in a more compliant mailer!
1651 @ToDo: If delete_from_server is false, we don't want to download the
1652 same messages repeatedly. Perhaps record time of fetch runs
1653 (or use info from the scheduler_run table), compare w/ message
1654 timestamp, as a filter. That may not be completely accurate,
1655 so could check msg_email for messages close to the last
1656 fetch time. Or just advise people to have a dedicated account
1657 to which email is sent, that does not also need to be read
1658 by humans. Or don't delete the fetched mail until the next run.
1659 """
1660
1661 db = current.db
1662 s3db = current.s3db
1663
1664 table = s3db.msg_email_channel
1665
1666 query = (table.channel_id == channel_id)
1667 channel = db(query).select(table.username,
1668 table.password,
1669 table.server,
1670 table.protocol,
1671 table.use_ssl,
1672 table.port,
1673 table.delete_from_server,
1674 limitby=(0, 1)).first()
1675 if not channel:
1676 return "No Such Email Channel: %s" % channel_id
1677
1678 import email
1679
1680 import socket
1681
1682 from dateutil import parser
1683 date_parse = parser.parse
1684
1685 username = channel.username
1686 password = channel.password
1687 host = channel.server
1688 protocol = channel.protocol
1689 ssl = channel.use_ssl
1690 port = int(channel.port)
1691 delete = channel.delete_from_server
1692
1693 mtable = db.msg_email
1694 minsert = mtable.insert
1695 stable = db.msg_channel_status
1696 sinsert = stable.insert
1697 atable = s3db.msg_attachment
1698 ainsert = atable.insert
1699 dtable = db.doc_document
1700 dinsert = dtable.insert
1701 store = dtable.file.store
1702 update_super = s3db.update_super
1703
1704 parser = s3db.msg_parser_enabled(channel_id)
1705 if parser:
1706 ptable = db.msg_parsing_status
1707 pinsert = ptable.insert
1708
1709
1710 def parse_email(message):
1711 """
1712 Helper to parse the mail
1713 """
1714
1715
1716 msg = email.message_from_string(message)
1717
1718 sender = msg["from"]
1719 subject = msg.get("subject", "")
1720 date_sent = msg.get("date", None)
1721
1722 raw = msg.as_string()
1723
1724
1725 attachments = []
1726
1727 body = ""
1728 for part in msg.walk():
1729 if part.get_content_maintype() == "multipart":
1730
1731 continue
1732 filename = part.get_filename()
1733 if not filename:
1734
1735 if not body:
1736
1737 body = part.get_payload(decode=True)
1738 continue
1739 attachments.append((filename, part.get_payload(decode=True)))
1740
1741
1742 data = dict(channel_id=channel_id,
1743 from_address=sender,
1744 subject=subject[:78],
1745 body=body,
1746 raw=raw,
1747 inbound=True,
1748 )
1749 if date_sent:
1750 data["date"] = date_parse(date_sent)
1751 _id = minsert(**data)
1752 record = dict(id=_id)
1753 update_super(mtable, record)
1754 message_id = record["message_id"]
1755 for a in attachments:
1756
1757
1758
1759 filename = s3_unicode(a[0][:92]).encode("ascii", "ignore")
1760 fp = StringIO()
1761 fp.write(a[1])
1762 fp.seek(0)
1763 newfilename = store(fp, filename)
1764 fp.close()
1765 document_id = dinsert(name=filename,
1766 file=newfilename)
1767 update_super(dtable, dict(id=document_id))
1768 ainsert(message_id=message_id,
1769 document_id=document_id)
1770 if parser:
1771 pinsert(message_id=message_id,
1772 channel_id=channel_id)
1773
1774 dellist = []
1775 if protocol == "pop3":
1776 import poplib
1777
1778 try:
1779 if ssl:
1780 p = poplib.POP3_SSL(host, port)
1781 else:
1782 p = poplib.POP3(host, port)
1783 except socket.error, e:
1784 error = "Cannot connect: %s" % e
1785 current.log.error(error)
1786
1787 sinsert(channel_id=channel_id,
1788 status=error)
1789 return error
1790 except poplib.error_proto, e:
1791
1792 current.log.error("Email poll failed: %s" % e)
1793 return
1794
1795 try:
1796
1797 p.apop(username, password)
1798 except poplib.error_proto:
1799
1800 try:
1801 p.user(username)
1802 p.pass_(password)
1803 except poplib.error_proto, e:
1804 error = "Login failed: %s" % e
1805 current.log.error(error)
1806
1807 sinsert(channel_id=channel_id,
1808 status=error)
1809 return error
1810
1811 mblist = p.list()[1]
1812 for item in mblist:
1813 number, octets = item.split(" ")
1814
1815 lines = p.retr(number)[1]
1816 parse_email("\n".join(lines))
1817 if delete:
1818
1819 dellist.append(number)
1820
1821 for number in dellist:
1822 p.dele(number)
1823 p.quit()
1824
1825 elif protocol == "imap":
1826 import imaplib
1827
1828 try:
1829 if ssl:
1830 M = imaplib.IMAP4_SSL(host, port)
1831 else:
1832 M = imaplib.IMAP4(host, port)
1833 except socket.error, e:
1834 error = "Cannot connect: %s" % e
1835 current.log.error(error)
1836
1837 sinsert(channel_id=channel_id,
1838 status=error)
1839 return error
1840
1841 try:
1842 M.login(username, password)
1843 except M.error, e:
1844 error = "Login failed: %s" % e
1845 current.log.error(error)
1846
1847 sinsert(channel_id=channel_id,
1848 status=error)
1849
1850 db.commit()
1851 return error
1852
1853
1854 M.select()
1855
1856 typ, data = M.search(None, "ALL")
1857 mblist = data[0].split()
1858 for number in mblist:
1859 typ, msg_data = M.fetch(number, "(RFC822)")
1860 for response_part in msg_data:
1861 if isinstance(response_part, tuple):
1862 parse_email(response_part[1])
1863 if delete:
1864
1865 dellist.append(number)
1866
1867 for number in dellist:
1868 typ, response = M.store(number, "+FLAGS", r"(\Deleted)")
1869 M.close()
1870 M.logout()
1871
1872
1873 @staticmethod
1875 """
1876 Fetches the inbound SMS from Mobile Commons API
1877 http://www.mobilecommons.com/mobile-commons-api/rest/#ListIncomingMessages
1878 """
1879
1880 db = current.db
1881 s3db = current.s3db
1882 table = s3db.msg_mcommons_channel
1883 query = (table.channel_id == channel_id)
1884 channel = db(query).select(table.url,
1885 table.campaign_id,
1886 table.username,
1887 table.password,
1888 table.query,
1889 table.timestmp,
1890 limitby=(0, 1)).first()
1891 if not channel:
1892 return "No Such MCommons Channel: %s" % channel_id
1893
1894 url = channel.url
1895 username = channel.username
1896 password = channel.password
1897 _query = channel.query
1898 timestamp = channel.timestmp
1899
1900 url = "%s?campaign_id=%s" % (url, channel.campaign_id)
1901 if timestamp:
1902 url = "%s&start_time=%s" % (url, timestamp)
1903 if _query:
1904 url = "%s&query=%s" % (url, _query)
1905
1906
1907 passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
1908 passman.add_password(None, url, username, password)
1909
1910
1911 authhandler = urllib2.HTTPBasicAuthHandler(passman)
1912 opener = urllib2.build_opener(authhandler)
1913 urllib2.install_opener(opener)
1914
1915
1916
1917 db(query).update(timestmp = current.request.utcnow)
1918
1919 try:
1920 _response = urllib2.urlopen(url)
1921 except urllib2.HTTPError, e:
1922 return "Error: %s" % e.code
1923 else:
1924 sms_xml = _response.read()
1925 tree = etree.XML(sms_xml)
1926 messages = tree.findall(".//message")
1927
1928 mtable = s3db.msg_sms
1929 minsert = mtable.insert
1930 update_super = s3db.update_super
1931 decode = s3_decode_iso_datetime
1932
1933
1934 parser = s3db.msg_parser_enabled(channel_id)
1935 if parser:
1936 ptable = db.msg_parsing_status
1937 pinsert = ptable.insert
1938
1939 for message in messages:
1940 sender_phone = message.find("phone_number").text
1941 body = message.find("body").text
1942 received_on = decode(message.find("received_at").text)
1943 _id = minsert(channel_id = channel_id,
1944 sender_phone = sender_phone,
1945 body = body,
1946 received_on = received_on,
1947 )
1948 record = dict(id=_id)
1949 update_super(mtable, record)
1950 if parser:
1951 pinsert(message_id = record["message_id"],
1952 channel_id = channel_id)
1953
1954 return "OK"
1955
1956
1957 @staticmethod
1959 """
1960 Fetches the inbound SMS from Twilio API
1961 http://www.twilio.com/docs/api/rest
1962 """
1963
1964 db = current.db
1965 s3db = current.s3db
1966 table = s3db.msg_twilio_channel
1967 query = (table.channel_id == channel_id)
1968 channel = db(query).select(table.account_sid,
1969 table.auth_token,
1970 table.url,
1971 limitby=(0, 1)).first()
1972 if not channel:
1973 return "No Such Twilio Channel: %s" % channel_id
1974
1975
1976
1977 account_sid = channel.account_sid
1978 url = "%s/%s/SMS/Messages.json" % (channel.url, account_sid)
1979
1980
1981 passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
1982 passman.add_password(None, url, account_sid, channel.auth_token)
1983
1984
1985 authhandler = urllib2.HTTPBasicAuthHandler(passman)
1986 opener = urllib2.build_opener(authhandler)
1987 urllib2.install_opener(opener)
1988
1989 try:
1990 smspage = urllib2.urlopen(url)
1991 except urllib2.HTTPError, e:
1992 error = "Error: %s" % e.code
1993 current.log.error(error)
1994
1995 S3Msg.update_channel_status(channel_id,
1996 status=error,
1997 period=(300, 3600))
1998 return error
1999 else:
2000 sms_list = json.loads(smspage.read())
2001 messages = sms_list["sms_messages"]
2002
2003
2004 stable = db.msg_twilio_sid
2005 sids = db(stable.id > 0).select(stable.sid)
2006 downloaded_sms = [s.sid for s in sids]
2007
2008 mtable = s3db.msg_sms
2009 minsert = mtable.insert
2010 sinsert = stable.insert
2011 update_super = s3db.update_super
2012
2013
2014 parser = s3db.msg_parser_enabled(channel_id)
2015 if parser:
2016 ptable = db.msg_parsing_status
2017 pinsert = ptable.insert
2018
2019 for sms in messages:
2020 if (sms["direction"] == "inbound") and \
2021 (sms["sid"] not in downloaded_sms):
2022 sender = "<" + sms["from"] + ">"
2023 _id = minsert(channel_id=channel_id,
2024 body=sms["body"],
2025 status=sms["status"],
2026 from_address=sender,
2027 received_on=sms["date_sent"])
2028 record = dict(id=_id)
2029 update_super(mtable, record)
2030 message_id = record["message_id"]
2031 sinsert(message_id = message_id,
2032 sid=sms["sid"])
2033 if parser:
2034 pinsert(message_id = message_id,
2035 channel_id = channel_id)
2036 return "OK"
2037
2038
2039 @staticmethod
2041 """
2042 Fetches all new messages from a subscribed RSS Feed
2043 """
2044
2045 db = current.db
2046 s3db = current.s3db
2047 table = s3db.msg_rss_channel
2048 query = (table.channel_id == channel_id)
2049 channel = db(query).select(table.date,
2050 table.etag,
2051 table.url,
2052 table.content_type,
2053 table.username,
2054 table.password,
2055 limitby=(0, 1)).first()
2056 if not channel:
2057 return "No Such RSS Channel: %s" % channel_id
2058
2059
2060 import feedparser
2061
2062 username = channel.username
2063 password = channel.password
2064 if username and password:
2065
2066 base64string = base64.encodestring("%s:%s" % (username, password)).replace("\n", "")
2067 request_headers = {"Authorization": "Basic %s" % base64string}
2068 else:
2069
2070
2071 request_headers = None
2072
2073 if channel.content_type:
2074
2075 response_headers = {"content-type": "application/xml"}
2076 else:
2077 response_headers = None
2078
2079 if channel.etag:
2080
2081
2082 d = feedparser.parse(channel.url,
2083 etag=channel.etag,
2084 request_headers=request_headers,
2085 response_headers=response_headers,
2086 )
2087 elif channel.date:
2088 d = feedparser.parse(channel.url,
2089 modified=channel.date.utctimetuple(),
2090 request_headers=request_headers,
2091 response_headers=response_headers,
2092 )
2093 else:
2094
2095 d = feedparser.parse(channel.url,
2096 request_headers=request_headers,
2097 response_headers=response_headers,
2098 )
2099 if d.bozo:
2100
2101 S3Msg.update_channel_status(channel_id,
2102 status = "ERROR: %s" % d.bozo_exception.message,
2103 period = (300, 3600),
2104 )
2105 return
2106
2107
2108 now = current.request.utcnow
2109 data = dict(date=now)
2110 etag = d.get("etag", None)
2111 if etag:
2112 data["etag"] = etag
2113 db(query).update(**data)
2114
2115 from time import mktime, struct_time
2116 gis = current.gis
2117 geocode_r = gis.geocode_r
2118 hierarchy_level_keys = gis.hierarchy_level_keys
2119 utcfromtimestamp = datetime.datetime.utcfromtimestamp
2120 gtable = db.gis_location
2121 ginsert = gtable.insert
2122 mtable = db.msg_rss
2123 minsert = mtable.insert
2124 ltable = db.msg_rss_link
2125 linsert = ltable.insert
2126 update_super = s3db.update_super
2127
2128
2129 parser = s3db.msg_parser_enabled(channel_id)
2130 if parser:
2131 ptable = db.msg_parsing_status
2132 pinsert = ptable.insert
2133
2134 entries = d.entries
2135 if entries:
2136
2137 count_old = db(mtable.id > 0).count()
2138 for entry in entries:
2139 link = entry.get("link", None)
2140
2141
2142
2143 exists = db(mtable.from_address == link).select(mtable.id,
2144 mtable.location_id,
2145 mtable.message_id,
2146 limitby=(0, 1)
2147 ).first()
2148 if exists:
2149 location_id = exists.location_id
2150 else:
2151 location_id = None
2152
2153 title = entry.get("title")
2154
2155 content = entry.get("content", None)
2156 if content:
2157 content = content[0].value
2158 else:
2159 content = entry.get("description", None)
2160
2161
2162
2163 date_published = entry.get("published_parsed", entry.get("updated_parsed"))
2164 if isinstance(date_published, struct_time):
2165 date_published = utcfromtimestamp(mktime(date_published))
2166 else:
2167 date_published = now
2168
2169 tags = entry.get("tags", None)
2170 if tags:
2171 tags = [t.term.encode("utf-8") for t in tags]
2172
2173 location = False
2174 lat = entry.get("geo_lat", None)
2175 lon = entry.get("geo_long", None)
2176 if lat is None or lon is None:
2177
2178 georss = entry.get("georss_point", None)
2179 if georss:
2180 location = True
2181 lat, lon = georss.split(" ")
2182 else:
2183 location = True
2184 if location:
2185 try:
2186 query = (gtable.lat == lat) &\
2187 (gtable.lon == lon)
2188 lexists = db(query).select(gtable.id,
2189 limitby=(0, 1),
2190 orderby=gtable.level,
2191 ).first()
2192 if lexists:
2193 location_id = lexists.id
2194 else:
2195 data = dict(lat=lat,
2196 lon=lon,
2197 )
2198 results = geocode_r(lat, lon)
2199 if isinstance(results, dict):
2200 for key in hierarchy_level_keys:
2201 v = results.get(key, None)
2202 if v:
2203 data[key] = v
2204
2205
2206
2207 location_id = ginsert(**data)
2208 data["id"] = location_id
2209 gis.update_location_tree(data)
2210 except:
2211
2212 pass
2213
2214
2215 links = entry.get("links", [])
2216 if exists:
2217 db(mtable.id == exists.id).update(channel_id = channel_id,
2218 title = title,
2219 from_address = link,
2220 body = content,
2221 author = entry.get("author", None),
2222 date = date_published,
2223 location_id = location_id,
2224 tags = tags,
2225
2226 )
2227 if links:
2228 query_ = (ltable.rss_id == exists.id) & (ltable.deleted != True)
2229 for link_ in links:
2230 url_ = link_["url"]
2231 type_ = link_["type"]
2232 query = query_ & (ltable.url == url_) & \
2233 (ltable.type == type_)
2234 dbset = db(query)
2235 row = dbset.select(ltable.id, limitby=(0, 1)).first()
2236 if row:
2237 dbset.update(rss_id = exists.id,
2238 url = url_,
2239 type = type_,
2240 )
2241 else:
2242 linsert(rss_id = exists.id,
2243 url = url_,
2244 type = type_,
2245 )
2246 if parser:
2247 pinsert(message_id = exists.message_id,
2248 channel_id = channel_id)
2249
2250 else:
2251 _id = minsert(channel_id = channel_id,
2252 title = title,
2253 from_address = link,
2254 body = content,
2255 author = entry.get("author", None),
2256 date = date_published,
2257 location_id = location_id,
2258 tags = tags,
2259
2260 )
2261 record = dict(id=_id)
2262 update_super(mtable, record)
2263 for link_ in links:
2264 linsert(rss_id = _id,
2265 url = link_["url"],
2266 type = link_["type"],
2267 )
2268 if parser:
2269 pinsert(message_id = record["message_id"],
2270 channel_id = channel_id)
2271
2272 if entries:
2273
2274 count_new = db(mtable.id > 0).count()
2275 if count_new == count_old:
2276
2277
2278 S3Msg.update_channel_status(channel_id,
2279 status="+1",
2280 period=(300, 3600))
2281
2282 return "OK"
2283
2284
2285 @staticmethod
2287 """
2288 Function to call to fetch tweets into msg_twitter table
2289 - called via Scheduler or twitter_inbox controller
2290
2291 http://tweepy.readthedocs.org/en/v3.3.0/api.html
2292 """
2293
2294 try:
2295 import tweepy
2296 except ImportError:
2297 current.log.error("s3msg", "Tweepy not available, so non-Tropo Twitter support disabled")
2298 return False
2299
2300 db = current.db
2301 s3db = current.s3db
2302
2303
2304 twitter_settings = S3Msg.get_twitter_api(channel_id)
2305 if twitter_settings:
2306
2307 dm = True
2308 else:
2309
2310 dm = False
2311 table = s3db.msg_twitter_channel
2312 channel = db(table.channel_id == channel_id).select(table.twitter_account,
2313 limitby=(0, 1)
2314 ).first()
2315 screen_name = channel.twitter_account
2316
2317 twitter_settings = S3Msg.get_twitter_api()
2318 if twitter_settings is None:
2319
2320 return False
2321
2322 twitter_api = twitter_settings[0]
2323
2324 table = s3db.msg_twitter
2325
2326
2327 query = (table.channel_id == channel_id) & \
2328 (table.inbound == True)
2329 latest = db(query).select(table.msg_id,
2330 orderby=~table.date,
2331 limitby=(0, 1)
2332 ).first()
2333
2334 try:
2335 if dm:
2336 if latest:
2337 messages = twitter_api.direct_messages(since_id=latest.msg_id)
2338 else:
2339 messages = twitter_api.direct_messages()
2340 else:
2341 if latest:
2342 messages = twitter_api.user_timeline(screen_name=screen_name,
2343 since_id=latest.msg_id)
2344 else:
2345 messages = twitter_api.user_timeline(screen_name=screen_name)
2346 except tweepy.TweepError as e:
2347 error = e.message
2348 if isinstance(error, (tuple, list)):
2349
2350 error = e.message[0]["message"]
2351 current.log.error("Unable to get the Tweets for the user: %s" % error)
2352 return False
2353
2354 messages.reverse()
2355
2356 tinsert = table.insert
2357 update_super = s3db.update_super
2358 for message in messages:
2359 if dm:
2360 from_address = message.sender_screen_name
2361 to_address = message.recipient_screen_name
2362 else:
2363 from_address = message.author.screen_name
2364 to_address = message.in_reply_to_screen_name
2365 _id = tinsert(channel_id = channel_id,
2366 body = message.text,
2367 from_address = from_address,
2368 to_address = to_address,
2369 date = message.created_at,
2370 inbound = True,
2371 msg_id = message.id,
2372 )
2373 update_super(table, dict(id=_id))
2374
2375 return True
2376
2377
2378 @staticmethod
2380 """
2381 Update the Status for a Channel
2382 """
2383
2384 db = current.db
2385
2386
2387 stable = current.s3db.msg_channel_status
2388 query = (stable.channel_id == channel_id)
2389 old_status = db(query).select(stable.status,
2390 limitby=(0, 1)
2391 ).first()
2392 if old_status:
2393
2394 if status and status[0] == "+":
2395
2396 old_status = old_status.status
2397 try:
2398 old_status = int(old_status)
2399 except (ValueError, TypeError):
2400 new_status = status
2401 else:
2402 new_status = old_status + int(status[1:])
2403 else:
2404 new_status = status
2405 db(query).update(status = new_status)
2406 else:
2407
2408 stable.insert(channel_id = channel_id,
2409 status = status)
2410 if period:
2411
2412 ttable = db.scheduler_task
2413 args = '["msg_rss_channel", %s]' % channel_id
2414 query = ((ttable.function_name == "msg_poll") & \
2415 (ttable.args == args) & \
2416 (ttable.status.belongs(["RUNNING", "QUEUED", "ALLOCATED"])))
2417 exists = db(query).select(ttable.id,
2418 ttable.period,
2419 limitby=(0, 1)).first()
2420 if not exists:
2421 return
2422 old_period = exists.period
2423 max_period = period[1]
2424 if old_period < max_period:
2425 new_period = old_period + period[0]
2426 new_period = min(new_period, max_period)
2427 db(ttable.id == exists.id).update(period=new_period)
2428
2429
2430 @staticmethod
2432 """
2433 Fetch Results for a Twitter Search Query
2434 """
2435
2436 try:
2437 import TwitterSearch
2438 except ImportError:
2439 error = "Unresolved dependency: TwitterSearch required for fetching results from twitter keyword queries"
2440 current.log.error("s3msg", error)
2441 current.session.error = error
2442 redirect(URL(f="index"))
2443
2444 db = current.db
2445 s3db = current.s3db
2446
2447
2448 table = s3db.msg_twitter_channel
2449
2450 settings = db(table.id > 0).select(table.consumer_key,
2451 table.consumer_secret,
2452 table.access_token,
2453 table.access_token_secret,
2454 limitby=(0, 1)).first()
2455
2456 if not settings:
2457 error = "Twitter Search requires an account configuring"
2458 current.log.error("s3msg", error)
2459 current.session.error = error
2460 redirect(URL(f="twitter_channel"))
2461
2462 qtable = s3db.msg_twitter_search
2463 rtable = db.msg_twitter_result
2464 search_query = db(qtable.id == search_id).select(qtable.id,
2465 qtable.keywords,
2466 qtable.lang,
2467 qtable.count,
2468 qtable.include_entities,
2469 limitby=(0, 1)).first()
2470
2471 tso = TwitterSearch.TwitterSearchOrder()
2472 tso.set_keywords(search_query.keywords.split(" "))
2473 tso.set_language(search_query.lang)
2474
2475
2476 tso.set_count(int(search_query.count))
2477 tso.set_include_entities(search_query.include_entities)
2478
2479 try:
2480 ts = TwitterSearch.TwitterSearch(
2481 consumer_key = settings.consumer_key,
2482 consumer_secret = settings.consumer_secret,
2483 access_token = settings.access_token,
2484 access_token_secret = settings.access_token_secret
2485 )
2486 except TwitterSearch.TwitterSearchException as e:
2487 return(str(e))
2488
2489 from dateutil import parser
2490 date_parse = parser.parse
2491
2492 gtable = db.gis_location
2493
2494 rtable.location_id.requires = None
2495 update_super = s3db.update_super
2496
2497 for tweet in ts.search_tweets_iterable(tso):
2498 user = tweet["user"]["screen_name"]
2499 body = tweet["text"]
2500 tweet_id = tweet["id_str"]
2501 lang = tweet["lang"]
2502 created_at = date_parse(tweet["created_at"])
2503 lat = None
2504 lon = None
2505 if tweet["coordinates"]:
2506 lat = tweet["coordinates"]["coordinates"][1]
2507 lon = tweet["coordinates"]["coordinates"][0]
2508 location_id = gtable.insert(lat=lat, lon=lon)
2509 else:
2510 location_id = None
2511 _id = rtable.insert(from_address = user,
2512 search_id = search_id,
2513 body = body,
2514 tweet_id = tweet_id,
2515 lang = lang,
2516 date = created_at,
2517
2518 location_id = location_id,
2519 )
2520 update_super(rtable, dict(id=_id))
2521
2522
2523 db(qtable.id == search_id).update(is_searched = True)
2524
2525 return "OK"
2526
2527
2528 @staticmethod
2530 """ Process results of twitter search with KeyGraph."""
2531
2532 import subprocess
2533 import tempfile
2534
2535 db = current.db
2536 s3db = current.s3db
2537 curpath = os.getcwd()
2538 preprocess = S3Msg.preprocess_tweet
2539
2540 def generateFiles():
2541
2542 dirpath = tempfile.mkdtemp()
2543 os.chdir(dirpath)
2544
2545 rtable = s3db.msg_twitter_search_results
2546 tweets = db(rtable.deleted == False).select(rtable.body)
2547 tweetno = 1
2548 for tweet in tweets:
2549 filename = "%s.txt" % tweetno
2550 f = open(filename, "w")
2551 f.write(preprocess(tweet.body))
2552 tweetno += 1
2553
2554 return dirpath
2555
2556 tpath = generateFiles()
2557 jarpath = os.path.join(curpath, "static", "KeyGraph", "keygraph.jar")
2558 resultpath = os.path.join(curpath, "static", "KeyGraph", "results", "%s.txt" % search_id)
2559 return subprocess.call(["java", "-jar", jarpath, tpath , resultpath])
2560
2561
2562 @staticmethod
2564 """
2565 Preprocesses tweets to remove URLs,
2566 RTs, extra whitespaces and replace hashtags
2567 with their definitions.
2568 """
2569
2570 tagdef = S3Msg.tagdef
2571 tweet = tweet.lower()
2572 tweet = re.sub(r"((www\.[\s]+)|(https?://[^\s]+))", "", tweet)
2573 tweet = re.sub(r"@[^\s]+", "", tweet)
2574 tweet = re.sub(r"[\s]+", " ", tweet)
2575 tweet = re.sub(r"#([^\s]+)", lambda m:tagdef(m.group(0)), tweet)
2576 tweet = tweet.strip('\'"')
2577
2578 return tweet
2579
2580
2581 @staticmethod
2583 """
2584 Returns the definition of a hashtag.
2585 """
2586
2587 hashtag = hashtag.split("#")[1]
2588
2589 turl = "http://api.tagdef.com/one.%s.json" % hashtag
2590 try:
2591 hashstr = urllib2.urlopen(turl).read()
2592 hashdef = json.loads(hashstr)
2593 except:
2594 return hashtag
2595 else:
2596 return hashdef["defs"]["def"]["text"]
2597
2600 """ RESTful method for messaging """
2601
2602
2604 """
2605 API entry point
2606
2607 @param r: the S3Request instance
2608 @param attr: controller attributes for the request
2609 """
2610
2611 if r.http in ("GET", "POST"):
2612 output = self.compose(r, **attr)
2613 else:
2614 r.error(405, current.ERROR.BAD_METHOD)
2615 return output
2616
2617
2619 """
2620 Generate a form to send a message
2621
2622 @param r: the S3Request instance
2623 @param attr: controller attributes for the request
2624 """
2625
2626 T = current.T
2627 auth = current.auth
2628
2629 self.url = url = r.url()
2630
2631
2632 if auth.is_logged_in() or auth.basic():
2633 pass
2634 else:
2635 redirect(URL(c="default", f="user", args="login",
2636 vars={"_next": url}))
2637
2638 if not current.deployment_settings.has_module("msg"):
2639 current.session.error = T("Cannot send messages if Messaging module disabled")
2640 redirect(URL(f="index"))
2641
2642 if not auth.permission.has_permission("update", c="msg"):
2643 current.session.error = T("You do not have permission to send messages")
2644 redirect(URL(f="index"))
2645
2646
2647
2648
2649 self.contact_method = None
2650 self.recipient = None
2651 self.recipients = None
2652 self.recipient_type = None
2653 self.subject = None
2654 self.message = None
2655
2656 form = self._compose_form()
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667 if self.method == "compose":
2668 output = dict(form=form)
2669 else:
2670 r.error(405, current.ERROR.BAD_METHOD)
2671
2672
2673 if r.representation == "html":
2674 title = self.crud_string(self.tablename, "title_compose")
2675 if not title:
2676 title = T("Send Message")
2677
2678
2679
2680
2681
2682
2683 if attr.get("rheader"):
2684 rheader = attr["rheader"](r)
2685 if rheader:
2686 output["rheader"] = rheader
2687
2688 output["title"] = title
2689
2690
2691
2692 current.response.view = self._view(r, "create.html")
2693
2694 return output
2695
2696
2698 """
2699 Set the sender
2700 Route the message
2701 """
2702
2703 post_vars = current.request.post_vars
2704 settings = current.deployment_settings
2705
2706 if settings.get_mail_default_subject():
2707 system_name_short = "%s - " % settings.get_system_name_short()
2708 else:
2709 system_name_short = ""
2710
2711 if settings.get_mail_auth_user_in_subject():
2712 user = current.auth.user
2713 if user:
2714 authenticated_user = "%s %s - " % (user.first_name,
2715 user.last_name)
2716 else:
2717 authenticated_user = ""
2718
2719 post_vars.subject = authenticated_user + system_name_short + post_vars.subject
2720 contact_method = post_vars.contact_method
2721
2722 recipients = self.recipients
2723 if not recipients:
2724 if not post_vars.pe_id:
2725 if contact_method != "TWITTER":
2726 current.session.error = current.T("Please enter the recipient(s)")
2727 redirect(self.url)
2728 else:
2729
2730 if current.msg.send_tweet(post_vars.body):
2731 current.session.confirmation = current.T("Check outbox for the message status")
2732 else:
2733 current.session.error = current.T("Error sending message!")
2734 redirect(self.url)
2735 else:
2736 recipients = post_vars.pe_id
2737
2738 if current.msg.send_by_pe_id(recipients,
2739 post_vars.subject,
2740 post_vars.body,
2741 contact_method):
2742 current.session.confirmation = current.T("Check outbox for the message status")
2743 redirect(self.url)
2744 else:
2745 if current.mail.error:
2746
2747 current.session.error = "%s: %s" % (current.T("Error sending message"),
2748 current.mail.error)
2749 else:
2750 current.session.error = current.T("Error sending message!")
2751 redirect(self.url)
2752
2753
2979
2980
2981