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