Import Upstream version 0.0.8
[cascardo/sendxmpp.git] / sendxmpp
1 #!/usr/bin/perl -w
2
3 eval 'exec /usr/bin/perl -w -S $0 ${1+"$@"}'
4     if 0; # not running under some shell
5 #-*-mode:perl-*-
6 #Time-stamp: <2005-05-07 19:24:09 (djcb)>
7
8 # script to send message using xmpp (aka jabber), 
9 #   somewhat resembling mail(1)
10
11 # Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
12 # Copyright (c) 2004,2005 Dirk-Jan C. Binnema
13
14 # Released under the terms of the GNU General Public License v2
15
16 use Net::XMPP;
17 use Getopt::Long;
18 use strict;
19
20 # subroutines decls
21 sub xmpp_login($$$$$$$);
22 sub xmpp_send ($$$);
23 sub xmpp_send_message($$$$);
24 sub xmpp_send_chatroom_message($$$$$);
25 sub xmpp_logout($);
26 sub xmpp_check_result;
27 sub parse_cmdline();
28 sub error_exit;
29 sub debug_print;
30 sub read_config_file($);
31 sub push_hash($$);
32 sub terminate();
33 sub main();
34
35 my # MakeMaker
36 $VERSION     = '0.0.8';
37 my $RESOURCE = 'sendxmpp';
38 my $VERBOSE  = 0;
39 my $DEBUG    = 0;
40              
41 # start!
42 &main;
43
44 #
45 # main: main routine
46 #
47 sub main () {
48
49     my $cmdline = parse_cmdline();
50     
51     $| = 1; # no output buffering
52
53     $DEBUG   = 1 if ($$cmdline{'debug'});
54     $VERBOSE = 1 if ($$cmdline{'verbose'});
55     
56     my $config = read_config_file ($$cmdline{'file'})
57         unless ($$cmdline{'jserver'} && $$cmdline{'username'} && $$cmdline{'password'});        
58     
59     # login to xmpp
60     my $cnx =  xmpp_login ($$cmdline{'jserver'}  || $$config{'jserver'},
61                            $$cmdline{'port'}     || $$config{'port'},
62                            $$cmdline{'username'} || $$config{'username'},
63                            $$cmdline{'password'} || $$config{'password'},
64                            $$cmdline{'resource'},
65                            $$cmdline{'tls'},
66                            $$cmdline{'debug'})
67       or error_exit("cannot login: $!");
68     
69    
70     # read message from STDIN or or from -m/--message parameter
71     if (!$$cmdline{interactive}) {
72         
73         # the non-interactive case
74         my $txt;
75         my $message = $$cmdline{'message'}; 
76         if ($message) {
77             open (MSG, "<$message")
78               or error_exit ("cannot open message file '$message': $!");
79             while (<MSG>) {$txt.=$_};
80             close(MSG);
81         }  else  {
82             $txt.=$_ while (<STDIN>);
83         }
84         
85         xmpp_send ($cnx,$cmdline,$txt);
86     
87     } else {
88         # the interactive case, read stdin line by line
89
90         # deal with TERM
91         $main::CNX = $cnx;  
92         $SIG{INT}=\&terminate;
93
94         # line by line...
95         while (<STDIN>) {
96             chomp;
97             xmpp_send ($cnx,$cmdline,$_);
98         }
99     }
100
101     xmpp_logout($cnx);
102     exit 0;
103 }
104
105
106
107 #
108 # read_config_file: read the configuration file
109 # input: filename
110 # output: hash with 'user', 'jserver' and 'password' keys
111 #
112 sub read_config_file ($) {
113    
114     # check permissions
115     my $cfg_file = shift;
116     error_exit ("cannot read $cfg_file: $!") 
117         unless (-r $cfg_file);    
118     my $owner  = (stat($cfg_file))[4];
119     error_exit ("you must own $cfg_file")
120       unless ($owner == $>); 
121     my $mode = (stat($cfg_file))[2] & 07777;
122     error_exit ("$cfg_file must have mode 0600")
123       unless ($mode == 0600);
124     
125     open (CFG,"<$cfg_file")
126       or error_exit("cannot open $cfg_file for reading: $!");
127
128     my %config;
129     my $line = 0;
130     while (<CFG>) {
131         
132         ++$line;
133         
134         next if (/^\s*$/);     # ignore empty lines
135         next if (/^\s*\#.*/);  # ignore comment lines
136         
137         s/\#.*$//; # ignore comments in lines
138         
139         if (/([-\.\w]+)@([-\.\w:]+)\s+(\S+)\s*$/) {
140             %config = ('username' => $1,
141                        'jserver'  => $2, 
142                        'port'     => 0,
143                        'password' => $3);
144
145             if ($config{'jserver'} =~ /(.*):(\d+)/) {
146                 $config{'jserver'} = $1;
147                 $config{'port'}    = $2;
148             }
149         } else {
150             close CFG;
151             error_exit ("syntax error in line $line of $cfg_file");
152         }
153     }
154     
155     close CFG;
156     
157     error_exit ("no correct config found in $cfg_file") 
158       unless (scalar(%config));       
159
160     if ($DEBUG || $VERBOSE) {
161         while (my ($key,$val) = each %config) {
162             debug_print ("config: '$key' => '$val'");
163         }
164     }       
165     
166     return \%config;               
167 }
168
169
170
171 #
172 # parse_cmdline: parse commandline options
173 # output: hash with commandline options
174 #
175 sub parse_cmdline () {
176     
177     usage() unless (scalar(@ARGV));
178     
179     my ($subject,$file,$resource,$jserver,$port,$username,$password,
180         $message,$chatroom,$debug,$tls,$interactive,$help,$verbose);
181     my $res = GetOptions ('subject|s=s'    => \$subject,
182                           'file|f=s'       => \$file,
183                           'resource|r=s'   => \$resource,
184                           'jserver|j=s'    => \$jserver,
185                           'username|u=s'   => \$username,
186                           'password|p=s'   => \$password,
187                           'message|m=s'    => \$message,
188                           'chatroom|c'     => \$chatroom,
189                           'tls|t'          => \$tls,
190                           'interactive|i'  => \$interactive,
191                           'help|usage|h'   => \$help,
192                           'debug|d'        => \$debug,
193                           'verbose|v'      => \$verbose);
194     usage () 
195       if ($help);   
196     
197     my $rcpt = $ARGV[0]
198       or error_exit "no recipient specified";
199  
200     if ($message && $interactive) {
201         error_exit "cannot have both -m (--message) and -i (--interactive)\n";
202     } 
203     
204     if ($jserver && $jserver =~ /(.*):(\d+)/) {
205         $jserver = $1;
206         $port    = $2;
207     }
208         
209     my %dict = ('subject'     => ($subject  or ''),
210                 'resource'    => ($resource or $RESOURCE),
211                 'jserver'     => ($jserver or ''),
212                 'port'        => ($port or 0),
213                 'username'    => ($username or ''),
214                 'password'    => ($password or ''),
215                 'chatroom'    => ($chatroom or 0),
216                 'interactive' => ($interactive or 0),
217                 'tls'         => ($tls or 0),
218                 'debug'       => ($debug or 0),
219                 'verbose'     => ($verbose or 0),
220                 'file'        => ($file or ($ENV{'HOME'}.'/.sendxmpprc')),
221                 'recipient'   => $rcpt);
222
223    if ($DEBUG || $VERBOSE) {
224        while (my ($key,$val) = each %dict) {
225            debug_print ("cmdline: '$key' => '$val'");
226        }
227    }        
228     
229    return \%dict;    
230 }
231
232
233 #
234 # xmpp_login: login to the xmpp (jabber) server
235 # input: hostname,port,username,password,resource,tls,debug
236 # output: an XMPP connection object
237 #
238 sub xmpp_login ($$$$$$$) {
239
240     my ($host,$port,$user,$pw,$res,$tls,$debug) = @_;
241     my $cnx = new Net::XMPP::Client(debuglevel=>($debug?2:0));
242     error_exit "could not create XMPP client object: $!"
243         unless ($cnx);    
244
245     my @res;
246     if (!$port) {
247         @res = $cnx->Connect(hostname=>$host,tls=>$tls);
248     } else {
249         @res = $cnx->Connect(hostname=>$host,port=>$port,tls=>$tls);
250     }
251
252     xmpp_check_result("Connect",\@res,$cnx);
253
254     @res = $cnx->AuthSend('hostname' => $host,
255                           'username' => $user,
256                           'password' => $pw,
257                           'resource' => $res);
258     xmpp_check_result('AuthSend',\@res,$cnx);
259     
260     return $cnx;    
261 }
262
263
264
265
266 #
267 # xmmp_send: send the message, determine from cmdline
268 # whether it's to individual or chatroom
269 #
270 sub xmpp_send ($$$) {
271     
272     my ($cnx, $cmdline, $txt) = @_;
273     
274     unless ($$cmdline{'chatroom'}) {
275         xmpp_send_message ($cnx,
276                            $$cmdline{'recipient'},
277                            $$cmdline{'subject'},
278                            $txt);
279     } else {
280         xmpp_send_chatroom_message ($cnx,
281                                     $$cmdline{'resource'},
282                                     $$cmdline{'subject'},
283                                     $$cmdline{'recipient'},
284                                     $txt);
285     }
286 }
287
288
289
290 #
291 # xmpp_send_message: send a message to some xmpp user
292 # input: connection,recipient,subject,msg
293 #
294 sub xmpp_send_message ($$$$) {
295     
296     my ($cnx,$rcpt,$subject,$msg) = @_;
297  
298     # for some reason, MessageSend does not return anything
299     $cnx->MessageSend('to'      => $rcpt,
300                       'subject' => $subject,
301                       'body'    => $msg);
302     
303     xmpp_check_result('MessageSend',0,$cnx);
304 }
305     
306     
307 #
308 # xmpp_send_chatroom_message: send a message to a chatroom
309 # input: connection,resource,subject,recipient,message
310 #
311 sub xmpp_send_chatroom_message ($$$$$) {
312
313     my ($cnx,$resource,$subject,$rcpt,$msg) =  @_;
314     
315     # set the presence
316     my $pres = new Net::XMPP::Presence;
317     my $res = $pres->SetTo("$rcpt/$resource");
318
319     $cnx->Send($pres); 
320
321     # create/send the message
322     my $groupmsg = new Net::XMPP::Message;
323     $groupmsg->SetMessage(to      => $rcpt, 
324                           body    => $msg,
325                           subject => $subject,
326                           type    => 'groupchat');
327
328     $res = $cnx->Send($groupmsg);
329     xmpp_check_result ('Send',$res,$cnx); 
330     
331     # leave the group
332     $pres->SetPresence (Type=>'unavailable',To=>$rcpt);
333 }
334
335
336 #
337 # xmpp_logout: log out from the xmpp server
338 # input: connection
339 #
340 sub xmpp_logout($) {
341     
342     # HACK
343     # messages may not be received if we log out too quickly...
344     sleep 1; 
345     
346     my $cnx = shift;
347     $cnx->Disconnect();
348     xmpp_check_result ('Disconnect',0); # well, nothing to check, really
349 }
350
351
352
353 #
354 # xmpp_check_result: check the return value from some xmpp function execution
355 # input: text, result, [connection]                   
356 #
357 sub xmpp_check_result {
358
359     my ($txt,$res,$cnx)=@_;
360     
361     error_exit ("Error '$txt': result undefined")
362         unless (defined $res);
363   
364     # res may be 0
365     if ($res == 0) {
366         debug_print "$txt";
367     # result can be true or 'ok' 
368     } elsif ((@$res == 1 && $$res[0]) || $$res[0] eq 'ok') {    
369         debug_print "$txt: " .  $$res[0];
370     # otherwise, there is some error
371     } else {    
372         my $errmsg = $cnx->GetErrorCode() || '?';
373         error_exit ("Error '$txt': " . join (': ',@$res) . "[$errmsg]", $cnx);
374     }
375 }
376
377
378 #
379 # terminate; exit the program upon TERM sig reception
380 #
381 sub terminate () {
382     debug_print "caught TERM";
383     xmpp_logout($main::CNX);
384     exit 0;
385 }
386
387
388 #
389 # debug_print: print the data if defined and DEBUG || VERBOSE is TRUE
390 # input: [array of strings]
391 #
392 sub debug_print {
393     print STDERR "sendxmpp: " . (join ' ', @_) . "\n"
394         if (@_ && ($DEBUG ||$VERBOSE));
395 }
396
397
398 #
399 # error_exit: print error message and exit the program
400 #             logs out if there is a connection 
401 # input: error, [connection]
402 #
403 sub error_exit {
404     
405     my ($err,$cnx) = @_;
406     print STDERR "$err\n";   
407     xmpp_logout ($cnx) 
408         if ($cnx);
409  
410     exit 1;
411 }
412
413
414 #
415 # usage: print short usage message and exit
416 #
417 sub usage () {
418    
419     print STDERR 
420         "sendxmpp version $VERSION, Copyright (c) 2004,2005 Dirk-Jan C. Binnema\n" .
421         "usage: sendxmpp [options] <recipient>\n" .
422         "or refer to the the sendxmpp manpage\n";
423     
424     exit 0;
425 }
426
427
428 #
429 # the fine manual
430 #
431 =pod
432 =head1 NAME
433
434 sendxmpp - send xmpp messages from the commandline.
435
436 =head1 SYNOPSIS
437
438 sendxmpp [options] <recipient>
439
440 =head1 DESCRIPTION
441
442 sendxmpp is a program to send XMPP (Jabber) messages from the commandline, not
443 unlike L<mail(1)>. Messages can be sent both to individual recipients and chatrooms.
444
445 =head1 OPTIONS
446
447 B<-f>,B<--file> <file>
448 use <file> configuration file instead of ~/.sendxmpprc
449
450 B<-u>,B<--username> <user>
451 use <user> instead of the one in the configuration file
452
453 B<-p>,B<--password> <password>
454 use <password> instead of the one in the configuration file
455
456 B<-j>,B<--jserver> <server>
457 use jabber server <server> instead of the one in the configuration file. Note that you can add :<port> to use a non-default port, ie. B<-j myjabber.org:1234>
458
459 B<-r>,B<--resource> <res>
460 use resource <res> for the sender [default: 'sendxmpp']; when sending to a chatroom, this determines the 'alias'
461
462 B<-t>,B<--tls>
463 connect securely, using TLS
464
465 B<-c>,B<--chatroom>
466 send the message to a chatroom 
467
468 B<-s>,B<--subject> <subject> 
469 set the subject for the message to <subject> [default: '']; when sending to a chatroom,
470 this will set the subject for the chatroom
471
472 B<-m>,B<--message> <message>
473 read the message from <message> (a file) instead of stdin
474
475 B<-i>,B<--interactive>
476 work in interactive mode, reading lines from stdin and sending the one-at-time
477
478 B<-v>,B<--verbose>
479 give verbose output about what is happening
480
481 B<-h>,B<--help>,B<--usage>
482 show a 'Usage' message
483
484 B<-d>,B<--debug>
485 show debugging info while running. B<WARNING>: This will include passwords etc. so be careful with the output!
486
487 =head1 CONFIGURATION FILE
488
489 You may define a '~/.sendxmpprc' file with the necessary data for your 
490 xmpp-account, with a line of the format:
491
492    <user>@<host> <password>
493
494 e.g.:
495
496     # my account
497     alice@jabber.org  secret
498
499 ('#' and newlines are allowed like in shellscripts). You can add :<port> to
500 the <host> if you need an alternative port, ie.
501
502     # account with weird port number
503     alice@myjabberhost.com:1234 secret
504     
505 B<NOTE>: for your security, sendxmpp demands that the configuration
506 file is owned by you and has file permissions 600.
507
508 =head1 EXAMPLE
509
510    $ echo "hello bob!" | sendxmpp -s hello someone@jabber.org
511
512      or to send to a chatroom:
513
514    $ echo "Dinner Time" | sendxmpp -r TheCook --chatroom test2@conference.jabber.org    
515
516      or to send your system logs somewhere, as new lines appear:
517    
518    $ tail -f /var/log/syslog | sendxmpp -i sysadmin@myjabberserver.com
519      
520      NOTE: be careful not the overload public jabber services
521      
522 =head1 SEE ALSO
523
524 Documentation for the L<Net::XMPP> module
525
526 The jabber homepage: http://www.jabber.org/
527
528 The sendxmpp homepage: http://www.djcbsoftware.nl/code/sendxmpp
529
530 =head1 AUTHOR
531
532 sendxmpp has been written by Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>, and uses
533 the L<Net::XMPP> modules written by Ryan Eatmon.
534
535 =cut