# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2010 Nexedi SA and Contributors. All Rights Reserved. # Francois-Xavier Algrain <fxalgrain@tiolive.com> # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsability of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # garantees and support are strongly adviced to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################## """Receive or send SMS""" #Import python module import urllib from lxml import etree from DateTime import DateTime #Import Zope module from AccessControl import ClassSecurityInfo, \ Unauthorized from AccessControl.SecurityManagement import getSecurityManager, \ setSecurityManager, \ newSecurityManager import zope.interface from zLOG import LOG, INFO from Products.ERP5Type import Permissions, PropertySheet, interfaces from Products.ERP5Type.XMLObject import XMLObject from Products import ERP5Security #Product Module from Products.ERP5ShortMessage.Errors import SMSGatewayError class EssendexGateway(XMLObject): """Base of SMS an Gateway. You can use push notification for delivered and new message notification.""" meta_type='Essendex Gateway' portal_type = 'Essendex Gateway' security = ClassSecurityInfo() add_permission = Permissions.AddPortalContent zope.interface.implements(interfaces.ISmsGateway) # Declarative security security = ClassSecurityInfo() security.declareObjectProtected(Permissions.AccessContentsInformation) # Declarative properties property_sheets = ( PropertySheet.Base , PropertySheet.XMLObject , PropertySheet.Reference , PropertySheet.SMSGateway ) api_url = "https://www.esendex.com/secure/messenger/formpost" security.declarePublic('getAllowedMessageType') def getAllowedMessageType(self): """List of all message type""" return ['text', 'binary', 'smartMessage', 'unicode'] security.declarePrivate("_fetchPageAsDict") def _fetchPageAsDict(self,page): """Page result is like Key=value in text format. We transform it to a more powerfull dictionnary""" result = {} index = 0 #Read all lines for line in page.readlines(): #Look is the line have multi key/value parts = line.split('&') if len(parts) == 1: data = parts[0].split('=') #Remove \n et \r from value result[data[0]] = urllib.unquote(data[1].replace('\r','').replace('\n','')) else: #Mutil values subresult = {} for part in parts: data = part.split('=') subresult[data[0]] = urllib.unquote(data[1].replace('\r','').replace('\n','')) result[index] = subresult #Increment index for next index += 1 return result security.declarePrivate("_transformPhoneUrlToGatewayNumber") def _transformPhoneUrlToGatewayNumber(self,phone): """Transform url of phone number to a valid phone number (gateway side)""" phone = phone.replace('tel:', '').replace('+','').replace('(0)','').replace('-','') # Check that phone number can not be something not existing assert not(phone.startswith('99000')) return phone security.declarePrivate("_parsePhoneNumber") def _parsePhoneNumber(self,number): """Convert phone number for erp5 compliance""" return "+%s(%s)-%s" % (number[0:2],0,number[2:]) security.declarePrivate("_parsePhoneNumber") def _parseDate(self, string): """Convert a string (like 2011-05-03 10:23:16Z) to a DateTime""" return DateTime(string.replace('Z', ' GTM+2')) def _convertTimeDeltaToSeconds(self, timedelta): """ Convert a timedelta to seconds """ return timedelta.seconds + (timedelta.days * 24 * 60 * 60) security.declareProtected(Permissions.ManagePortal, 'send') def send(self, text,recipient,sender=None, sender_title=None, message_type="text",test=False,**kw): """Send a message. Parameters: text -- message recipient -- phone url of destination_reference. Could be a list sender -- phone url of source sender_title -- Use it as source if the gateway has title mode enable message_type -- Only 'text' is available today test -- Force the test mode Kw Parameters: validity_period -- Validity Period of SMS (default,0) Return message id (or list if multiple recipient) """ if message_type not in self.getAllowedMessageType(): raise ValueError, "Type of message in not allowed" validity_period = kw.get('validity_period',0) if not isinstance(recipient, str): recipient = ",".join([self._transformPhoneUrlToGatewayNumber(x) for x in recipient]) else: recipient = self._transformPhoneUrlToGatewayNumber(recipient) base_url = self.api_url + "/SendSMS.aspx" params = {'Username': self.getGatewayUser(), 'Password': self.getGatewayPassword(), 'Account': self.getGatewayAccount(), 'Recipient': recipient, 'Body': text, 'Type': message_type.capitalize(), 'ValidityPeriod': validity_period, 'PlainText': 1, } if sender_title and self.isTitleMode(): params['Originator'] = sender_title elif sender: params['Originator'] = self._transformPhoneUrlToGatewayNumber(sender) elif self.getDefaultSender(): params['Originator'] = self.getDefaultSender() if test or self.isSimulationMode(): params['Test'] = 1 LOG("EssendexGateway", INFO, params) params = urllib.urlencode(params) page = urllib.urlopen(base_url, params) result = self._fetchPageAsDict(page) if result['Result'] == "OK": message_ids = result.get('MessageIDs', "") #If a message is sent to multiple recipients, multiple IDs are returned #each seperated by a comma. return message_ids.split(",") elif result['Result'] == "Error": #we get an error when call the gateway raise SMSGatewayError, urllib.unquote(result.get('Message', "Impossible to send the SMS")) elif result['Result'] == "Test": #just a test, no message id return None else: raise ValueError("Unknown result", 0, result) security.declareProtected(Permissions.ManagePortal, 'getMessageStatus') def getMessageStatus(self, message_id): """Retrive the status of a message""" base_url = self.api_url + "/QueryStatus.aspx" params = {'Username': self.getGatewayUser(), 'Password': self.getGatewayPassword(), 'Account': self.getGatewayAccount(), 'PlainText': 1, 'MessageID': message_id, } params = urllib.urlencode(params) page = urllib.urlopen(base_url, params) result = self._fetchPageAsDict(page) if result['Result'] == "OK": return result.get('MessageStatus').lower() elif result['Result'] == "Error": #we get an error when call the gateway raise SMSGatewayError, urllib.unquote(result.get('Message', "Impossible to get the message status")) security.declarePublic('receive') def receive(self,REQUEST): """Receive push notification""" #XML is stored is BODY of request datas = REQUEST['BODY'] if not datas: raise SMSGatewayError, "Impossible to notify nothing" #Get current user sm = getSecurityManager() try: #Use SUPER_USER portal_membership = self.getPortalObject().portal_membership newSecurityManager(None, portal_membership.getMemberById(ERP5Security.SUPER_USER)) #Parse XML root = etree.fromstring(datas) #Choice action corresponding to the notification type notification_type = root.tag #Parse text XML Element to dict xml = {} for child in root.getchildren(): xml[child.tag] = child.text #Check Account id if xml['AccountId'] != self.getGatewayAccountId(): raise Unauthorized, 'Bad accound id (%s)' % xml['AccountId'] if notification_type == 'InboundMessage': self.notifyReception(xml) elif notification_type == 'MessageDelivered': self.notifyDelivery(xml) elif notification_type == 'MessageError': raise SMSGatewayError, "'MessageError' notification is not implemented (%s)" % str(kw) elif notification_type == 'SubscriptionEvent': raise SMSGatewayError, "'MessageError' notification is not implemented (%s)" % str(kw) else: raise SMSGatewayError, "Unknow '%s' notification (%s)" % (notification_type, str(kw)) finally: #Restore orinal user setSecurityManager(sm) security.declareProtected(Permissions.ManagePortal, 'notifyReception') def notifyReception(self, xml): """The gateway inform what we ha a new message. root: lxml Element""" """ <InboundMessage> <Id>{guid-of-push-notification}</Id> <MessageId>{guid-of-inbound-message}</MessageId> <AccountId>{guid-of-esendex-account-for-message}</AccountId> <MessageText>{Message text of inbound message}</MessageText> <From>{phone number of sender of the message}</From> <To>{phone number of the recipient of the inbound message (the virtual number of the esendex account in use)}</To> </InboundMessage> """ #Create the new sms in activities self.activate(activity='SQLQueue', priority=1).SMSTool_pushNewSMS( message_id=xml['MessageId'], sender=self._parsePhoneNumber(xml['From']), recipient=self._parsePhoneNumber(xml['To']), text_content=xml['MessageText'], message_type='text/plain', reception_date=DateTime(), mode="push") security.declareProtected(Permissions.ManagePortal, 'notifyDelivery') def notifyDelivery(self, xml): """Handle delivery info xml: lxml Element""" """ <MessageDelivered> <Id>{guid-of-push-notification}</Id> <MessageId>{guid-of-inbound-message}</MessageId> <AccountId>{guid-of-esendex-account-for-message}</AccountId> <OccurredAt>{the UTC DateTime (yyyy-MM-ddThh:mm:ss) that the message was delivered to the recipient}</OccurredAt> </MessageDelivered> """ #Convert date to DateTime xml['OccurredAt'] = DateTime(xml['OccurredAt'][0:19]) self.activate(activity='SQLQueue').SMSTool_setMessageDelivery( portal_type="Short Message", destination_reference=xml['MessageId'], delivery_date=xml['OccurredAt']) def pullLastMessageList(self, start_date=None, stop_date=None): """Get last messsages on the gateway""" if start_date is not None or stop_date is not None: base_url = self.api_url + "/GetInboxMessage.aspx" else: base_url = self.api_url + "/GetLatestInboxMessages.aspx" params = {'Username': self.getGatewayUser(), 'Password': self.getGatewayPassword(), 'Account': self.getGatewayAccount(), 'PlainText': 1, } if start_date is not None: params['StartDate'] = start_date.strftime('%d/%m/%Y %H:%M:%S') if stop_date is not None: params['EndDate'] = stop_date.strftime('%d/%m/%Y %H:%M:%S') if self.isSimulationMode(): params['Test'] = 1 LOG("EssendexGateway", INFO, params) params = urllib.urlencode(params) page = urllib.urlopen(base_url, params) result = self._fetchPageAsDict(page) if result['Result'] == "OK": #Push all message type_mapping = {'Text': 'text/plain'} now == DateTime() for key, value in result.items(): if type(key) == int: reception_date = self._parseDate(value['ReceivedAt']) #Take only message received more than 10s if self._convertTimeDeltaToSeconds(now - reception_date) > 10: self.activate(activity='SQLQueue',priority=2).SMSTool_pushNewSMS( message_id=value['ID'], sender=self._parsePhoneNumber(value['Originator']), recipient=self._parsePhoneNumber(value['Recipient']), text_content=value['Body'], message_type=type_mapping[value['Type']], reception_date=reception_date, mode="pull") elif result['Result'] == "Test": LOG("EssendexGateway", INFO, result) elif result['Result'] == "Error": #we get an error when call the gateway raise SMSGatewayError, urllib.unquote(result.get('Message', "Impossible to get last message list"))