File Coverage

blib/lib/Net/SMS/BulkSMS.pm
Criterion Covered Total %
statement 24 135 17.7
branch 0 62 0.0
condition 0 28 0.0
subroutine 8 18 44.4
pod 5 10 50.0
total 37 253 14.6


line stmt bran cond sub pod time code
1             package Net::SMS::BulkSMS;
2              
3 1     1   69747 use 5.006;
  1         4  
  1         40  
4 1     1   7 use strict;
  1         2  
  1         36  
5 1     1   5 use warnings;
  1         7  
  1         56  
6              
7             our $VERSION = '1.02';
8              
9 1     1   5 use Carp;
  1         2  
  1         384  
10 1     1   2290 use HTTP::Request::Common;
  1         34567  
  1         101  
11 1     1   1208 use LWP::UserAgent;
  1         26847  
  1         44  
12 1     1   1043 use MIME::Base64;
  1         968  
  1         96  
13 1     1   1108 use POSIX qw(strftime);
  1         7708  
  1         9  
14              
15              
16             sub new
17             {
18 0     0 1   my $class = shift;
19              
20 0           my $this = {
21             test => 0,
22             test_form_url => "http://setyoururlhere/cgi-bin/form.pl/",
23             username => undef,
24             password => undef,
25             max_recipients => 200,
26             num_retries => 3,
27             signature => "",
28             signature_datetime => 1,
29             sender => "BulksmsCoUk",
30             url =>
31             {
32             base => 'http://www.bulksms.co.uk:7512/eapi/1.0/',
33             get_credits => "get_credits.mc",
34             send_sms => "send_sms.mc",
35             quote_sms => "quote_sms.mc",
36             get_report => "get_report.mc",
37             phonebook_public_add_member => "phonebook/public_add_member",
38             phonebook_public_remove_member => "phonebook/public_remove_member",
39             },
40              
41             @_,
42              
43             max_msglen => 160,
44             max_senderlen => 11,
45             max_length => 160,
46             VERSION => "0.1",
47             };
48 0           bless($this,$class);
49 0           $this->init;
50 0           $this;
51             }
52              
53             sub init
54             {
55 0     0 0   my $this = shift;
56 0 0         if ( $this->{test} )
57             {
58 0           $this->{url}->{base} = $this->{test_form_url};
59 0           $this->{username} = encode_base64("test");
60 0           $this->{password} = encode_base64("1234");
61             }
62 0 0 0       confess "need auth info" unless $this->{password} && $this->{username};
63 0           $this->{username} = decode_base64($this->{username});
64 0           $this->{password} = decode_base64($this->{password});
65 0           1;
66             }
67              
68             sub transaction
69             {
70 0     0 0   my $this = shift;
71 0 0         ref($this) or confess;
72 0           my %arg = ( txn => undef, username => $this->{username}, password => $this->{password}, @_ );
73              
74             # construct URL to call web API
75 0           my $url = $this->{url}->{base};
76 0 0         confess unless $url;
77 0 0         $url .= "/" unless $url =~ m/\/$/;
78 0 0         confess "expected txn" unless $arg{txn};
79 0   0       $url .= $this->{url}->{$arg{txn}} || confess "suburl for transaction $arg{txn} not defined";
80 0           delete $arg{txn};
81              
82             # enforce 11 char limit on sender name/phone
83 0 0         if ( defined $arg{sender} )
84             {
85 0           my $count = length($arg{sender});
86 0   0       my $max_length = $this->{max_senderlen} || 11;
87 0 0         return("Sender length $count too long (over $max_length characters)",0) if $count > $max_length;
88             }
89              
90             # enforce 160 char limit on message length
91 0 0         if ( defined $arg{message} )
92             {
93 0           $arg{message} =~ s/^\s+//; # trim spaces leading and trailing
94 0           $arg{message} =~ s/\s+$//;
95 0           my $count = length($arg{message});
96 0   0       my $max_length = $this->{max_msglen} || 160;
97 0 0         return("Message length $count too long (over $max_length characters including signature)",0) if $count > $max_length;
98             }
99              
100             # special check on phone number array argument if present
101 0 0         if ( defined $arg{msisdn} )
102             {
103             # strip white space
104 0           my $s = $arg{msisdn};
105 0           $s =~ s/\s//g;
106 0           $arg{msisdn} = $s;
107             # split to individual phone numbers on comma
108 0           my @telno = split(/\,/,$s);
109 0           $s =~ s/\,//g; # strip commas and check for non-digits
110 0 0         return ("Phone number string msisdn contains non-digits: $arg{msisdn}",0) if $s =~ m/\D/;
111             # check for duplicate phone numbers
112 0           my %h;
113 0           for (@telno) { $h{$_}++; }
  0            
114 0           my @dupno;
115 0           for ( keys %h )
116             {
117 0 0         push(@dupno,$_) if $h{$_} > 1;
118             }
119 0 0         return("Phone number string msisdn contains duplicate phone numbers : ".join(",",@dupno)." in $arg{msisdn}",0) if scalar @dupno > 0;
120             # check count of phone numbers
121 0 0         return("Phone number string msisdn contains no phone numbers: $arg{msisdn}",0) if scalar @telno <= 0;
122 0   0       my $max_recipients = $this->{max_recipients} || 0;
123 0           my $count = scalar @telno;
124 0 0 0       if ( $max_recipients && $count > $max_recipients )
125             {
126 0           return("Cannot send more than $max_recipients messages at once ($count phone numbers in list)",0);
127             }
128             }
129              
130             # delete undefined parameters
131 0           for ( keys %arg )
132             {
133 0 0         delete $arg{$_} unless defined $arg{$_};
134             }
135              
136 0           my $req = POST $url, [ %arg ];
137              
138 0           my $ua = LWP::UserAgent->new;
139 0           $ua->env_proxy(); # allow for loading of proxy settings from *_proxy env vars; see "perldoc LWP::UserAgent" for details
140 0           my $res = $ua->request($req);
141            
142 0 0         if ( $res->is_success ) # web request worked
    0          
143             {
144 0           return ($res->content, 1);
145             }
146             elsif ( $res->is_error ) # web request failed
147             {
148 0           return ($res->code . ": " . $res->message, 0);
149             }
150             else # redirect or information - should not happen, treat as failure
151             {
152             return (
153 0           ( "Code: " . $res->code . "\n"
154             . "Message: " . $res->message . "\n"
155             . "Content: " . $res->content . "\n" )
156             , 0);
157             }
158             }
159              
160             sub get_credits
161             {
162 0     0 1   $_[0]->transaction ( txn => "get_credits" );
163             }
164              
165             # send_sms - send an SMS text message
166             #
167             # required parameters
168             # message: max 160 chars, 280 for 8bit
169             # msisdn: comma separated list of recipient phone numbers
170             #
171             # optional parameters
172             # sender: sender id (if alphanumeric, max 11 characters). This facility has to be specifically enabled for your account on request. Alphanumeric sender id is a route-specific feature.
173             # msg_class: currently 0 (flash SMS) or 2 (normal SMS), default 2
174             # dca: Data Coding Alphabet: 7bit,8bit or 16bit, default 7bit (normal text message). For 8bit (ringtones,logos) or 16bit (Unicode), a message containing only hexadecimal octets, to a maximum of 280 characters (140 octet pairs), must be submitted. Currently, concatenation is not supported for Unicode messages. 16-bit is a route-specific feature.
175             # want_report: 0 or 1, default 0
176             # cost_route: 1 or 2, default 1 (future functionality - always use 1 for now, if used)
177             # msg_id: a unique id generated by yourself, to reduce risk of duplicate submissions - future functionality, currently unused.
178             #
179             # returns
180             # status_code|status_description|message_id (where message_id is optional, depending on the error)
181             #
182             # status codes
183             # 0: In progress (a normal message submission, with no error encountered so far).
184             # 22: Internal fatal error
185             # 23: Authentication failure
186             # 24: Data validation failed
187             # 25: You do not have sufficient credits
188             # 26: Upstream credits not available
189             # 27: You have exceeded your daily quota
190             # 28: Upstream quota exceeded
191             # 40: Temporarily unavailable
192             #
193              
194             sub base_send_sms
195             {
196 0     0 0   my $this = shift;
197 0           my %arg = (
198             ## mandatory
199             message => undef,
200             msisdn => undef,
201             ## optional
202             sender => $this->{sender}, # sender id, alphanumeric, max 11 chars
203             msg_class => "2", # 2 = normal SMS, 0 = flash SMS
204             dca => "7bit", # data coding alphabet: 7bit (default), 8bit (ringtones,logos), 16bit (Unicode)
205             want_report => 1, # 0 or 1
206             cost_route => 1, # 1 or 2, default 1, 2 not implemented
207             #msg_id => our unique id
208             ## user
209             quote => 0,
210             @_,
211             );
212 0 0 0       confess unless $arg{message} && $arg{msisdn};
213 0 0         my $txn = $arg{quote} ? "quote_sms" : "send_sms";
214 0           $arg{message} .= $this->{signature};
215 0           my $datetime = strftime "%e-%b-%Y %H:%M", localtime(time);
216 0 0         $arg{message} .= "\n$datetime" if $this->{signature_datetime};
217              
218             # call HTTP web request API interface at bulksms
219             # on HTTP status code <> 200 we should try resending a few times
220 0           my ($webmsg,$webcode);
221 0   0       for (1..($this->{num_retries}||3))
222             {
223 0           ($webmsg,$webcode) = $this->transaction (
224             txn => $txn,
225             message => $arg{message},
226             msisdn => $arg{msisdn},
227             msg_class => $arg{msg_class},
228             dca => $arg{dca},
229             want_report => $arg{want_report},
230             cost_route => $arg{cost_route},
231             sender => $arg{sender},
232             );
233 0 0         return ($webmsg,1) if $webcode; # web request success
234 0           select(undef,undef,undef,0.25); # wait 0.25 seconds
235             }
236 0           return ($webmsg,0); # web request failed
237             }
238              
239             sub send_sms
240             {
241 0     0 1   my $this = shift;
242             # try and send, retry on temporary error (code 40)
243             # possibly should retry on 26-28 or warn user about account/quota limit at caller's level
244 0           my ($code,$desc,$msg_id);
245 0   0       for (1..($this->{num_retries}||3))
246             {
247 0           my ($webmsg,$webcode) = $this->base_send_sms ( @_ );
248 0 0         return ($webmsg,0) unless $webcode; # web request failed
249 0           ($code,$desc,$msg_id) = split(/\|/,$webmsg,3);
250 0   0       $desc ||= "";
251 0 0         return ("$msg_id",1) if $code eq "0"; # code 0 == in progress (successful)
252 0 0         return ("$code: $desc",0) if $code ne "40"; # outright failure, except 40 which is temporary
253 0           select(undef,undef,undef,0.25); # wait 0.25 seconds before retry
254             }
255 0           return ("$code: $desc",0); # failure
256             }
257              
258             sub quote_sms
259             {
260 0     0 1   my $this = shift;
261 0           my ($webmsg,$webcode) = $this->base_send_sms ( @_, quote => 1 );
262 0 0         return ($webmsg,0) unless $webcode; # web request failed
263 0           my ($code,$desc,$quote_total) = split(/\|/,$webmsg,3);
264 0   0       $desc ||= "";
265 0 0         return ("$quote_total",1) if $code eq "1000"; # code 1000 == successful quotation
266 0           return ("$code: $desc",0); # failure
267             }
268              
269             # get_report - report status of a sent SMS text message
270             #
271             # required parameters
272             # msg_id: message id return by send_sms
273             # optional parameter
274             # msisdn: supply if querying the status of a single recipient.
275             #
276             # returns on failure
277             # desc: "request_status_code: desc" formatted request status code and description
278             # code: 0
279             # result: undefined
280             #
281             # possible request status codes
282             # 23: Authentication failure
283             # 24: Data validation failed
284             # 1001: Error - message not found (msg_id incorrect)
285             #
286             # returns on success
287             # desc: "request status code: description", code 1000, description
288             # "Results to follow\n" followed by one or more newline-separated items in the format:
289             # "msisdn|status_code|status_description"
290             # code: 1
291             # result: pointer to results hash indexed by msisdn (phone number),
292             # each entry a pointer to hash of {code,desc}
293             #
294             # status codes and descriptions:
295             #
296             # 0: In progress (a normal message submission, with no error encountered so far).
297             # 10: Delivered upstream
298             # 11: Delivered to mobile
299             # 22: Internal fatal error
300             # 23: Authentication failure
301             # 24: Data validation failed
302             # 25: You do not have sufficient credits
303             # 26: Upstream credits not available
304             # 27: You have exceeded your daily quota
305             # 28: Upstream quota exceeded
306             # 29: Message sending cancelled
307             # 30: Test complete (you should never see this)
308             # 40: Temporarily unavailable
309             # 50: Delivery failed - generic failure
310             # 51: Delivery to phone failed
311             # 52: Delivery to network failed
312             # 60: Transient upstream failure (transient)
313             # 61: Upstream status update (transient)
314             # 62: Upstream cancel failed (transient)
315             # 70: Unknown upstream status
316              
317             sub get_report
318             {
319 0     0 1   my $this = shift;
320 0           my %arg = ( msg_id => undef, msisdn => undef, @_ );
321 0 0         confess unless defined $arg{msg_id};
322 0           my ($webmsg,$webcode) = $this->transaction ( txn => "get_report", msg_id => $arg{msg_id}, msisdn => $arg{msisdn} );
323 0 0         return ($webmsg,0,undef) unless $webcode; # web request failed
324             # check bulksms API return
325 0           my ($code,$desc) = split(/\|/,$webmsg,2);
326 0   0       $desc ||= "";
327 0 0         return ("$code: $desc",0,undef) if $code ne "1000"; # failure (1001,23,24) (code 1000 = success)
328             # API returns on success a string containing
329             # 1000|Results to follow\n Followed by one or more newline-separated items in the format:
330             # msisdn|status_code|status_description
331             # build result hash from this
332 0           my @l = split(/\n/,$desc);
333 0           shift @l; # remove "Results to follow" first line
334 0           my %h;
335 0           for (@l)
336             {
337 0           my ($msisdn,$status_code,$status_desc) = split(/\|/,$_);
338 0           $h{$msisdn} = { code => $status_code, desc => $status_desc };
339             }
340 0           return ("$code: $desc",1,\%h); # success
341             }
342              
343             sub phonebook_public_add_member
344             {
345 0     0 0   confess "not implemented";
346             }
347              
348             sub phonebook_public_remove_member
349             {
350 0     0 0   confess "not implemented";
351             }
352              
353              
354             1;
355             __END__