File Coverage

blib/lib/Mail/Webmail/Yahoo.pm
Criterion Covered Total %
statement 40 42 95.2
branch n/a
condition n/a
subroutine 14 14 100.0
pod n/a
total 54 56 96.4


';
line stmt bran cond sub pod time code
1             # (C) Simon Drabble 2002,2003
2             # sdrabble@cpan.org 2002/03/22
3              
4             # $Id: Yahoo.pm,v 1.31 2003/10/19 03:55:50 simon Exp $
5             #
6              
7             package Mail::Webmail::Yahoo;
8              
9             require 5.006_000;
10              
11              
12             #BEGIN { open SIMONLOG, ">simon.$$.tmp" }
13             #END { close SIMONLOG }
14              
15              
16 1     1   5645 use strict;
  1         2  
  1         27  
17 1     1   4 use warnings;
  1         2  
  1         79  
18              
19             require Exporter;
20             our @ISA = qw(Exporter);
21              
22              
23             # This is an object-based package. We export nothing except for some flag
24             # values.
25             our @EXPORT_OK = ();
26             our @EXPORT = qw(
27             YAHOO_MSG_FLAGS
28             SAVE_COPY_TO_SENT_FOLDER
29             SUPPRESS_BANNERS
30             DELETE_ON_READ
31             MOVE_ON_READ
32             ATTACH_SIG
33             SEND_AS_HTML
34             );
35              
36              
37 1     1   4 use Carp qw(carp);
  1         4  
  1         38  
38              
39              
40 1     1   1018 use LWP::UserAgent;
  1         47565  
  1         32  
41             # Turn on for mondo debugging oh yeah.
42             #use LWP::Debug qw(+);
43 1     1   765 use URI::URL;
  1         4044  
  1         51  
44 1     1   6 use HTTP::Request;
  1         1  
  1         21  
45 1     1   5 use HTTP::Headers;
  1         1  
  1         21  
46 1     1   754 use HTTP::Cookies;
  1         14543  
  1         38  
47 1     1   727 use HTML::LinkExtor;
  1         8804  
  1         34  
48 1     1   9 use HTML::Entities;
  1         1  
  1         68  
49 1     1   764 use Mail::Internet;
  1         14309  
  1         34  
50 1     1   756 use MIME::Base64;
  1         680  
  1         61  
51 1     1   712 use HTML::FormParser;
  1         493  
  1         24  
52 1     1   1352 use HTML::TableContentParser;
  0            
  0            
53             use Mail::Webmail::MessageParser;
54             use CGI qw(escape unescape);
55              
56              
57              
58             our $VERSION = 0.601;
59              
60             use Class::MethodMaker
61             get_set => [qw(trace cache_messages cache_headers)];
62              
63              
64             # These next bits should ideally go in a config file or something. Or be
65             # passable on the command line, or overrideable in the calling app. They will
66             # (hopefully) never change, but if they do, it would be better for the user to
67             # edit a simple configuration file than modify (possibly system-wide) code.
68              
69             # TODO: future ver: have all relevant items in resource file (localisable?)
70             # thus few (if any) code changes needed if Yahoo change page layout (again)
71              
72             # Config specific to this package.
73             our $USER_AGENT = "Yahoo-Webmail/$VERSION";
74             our $ENV_PROXY = 0;
75              
76              
77             # Would prefer to 'use constant...' but that doesn't work well in regexps.
78             our $SERVER = 'http://mail.yahoo.com';
79             our $LOGIN_SERVER = 'http://mail.yahoo.com';
80              
81             our $FOLDER_APP_NAME = 'Folders';
82             our $SHOW_FOLDER_APP_NAME = 'ShowFolder';
83             our $EMPTY_FOLDER_APP_NAME= 'ShowFolder';
84             our $SHOW_MSG_APP_NAME = 'ShowLetter';
85             our $SHOW_TOC = qr{toc=[^\&]*};
86              
87             our $ATTACH_SECTION = '#attachments';
88              
89             our $FULL_HEADER_FLAG = 'Nhead=f&head=f';
90             our $EMPTY_FULL_HEADER_FLAG = qr{head=[^\&]*&?};
91              
92             our $LOGIN_FIELD = 'login';
93             our $PASSWORD_FIELD = 'passwd';
94             our $SAVE_USER_INFO_FIELD = '.persistent';
95              
96              
97             # Should only get the 'check mail' option if logged in..
98             our $WELCOME_PAGE_CHECK = qr{]*>Check Mail};
99              
100              
101             our $DATE_MOLESTERED_STRING = 'Date header was inserted';
102              
103             our $COMPOSE_APP_NAME = 'Compose';
104             our $COMPOSE_TO_FIELD = 'To';
105             our $COMPOSE_CC_FIELD = 'Cc';
106             our $COMPOSE_BCC_FIELD = 'Bcc';
107             our $COMPOSE_SUBJ_FIELD = 'Subj';
108             our $COMPOSE_BODY_FIELD = 'Body';
109             our $COMPOSE_SAVE_COPY = 'SaveCopy';
110             our $COMPOSE_ATTACH_SIG = 'SigAtt';
111             our $COMPOSE_SEND_HTML = 'Format';
112             our $COMPOSE_MONEY_FIELD = 'Money';
113             our $COMPOSE_SEND_MONEY_CHK = 'SendMoney';
114              
115             ##our $COMPOSE_SENT_OK_PRE = 'Your\s+mail\s*';
116             ##our $COMPOSE_SENT_OK_POST= '\s*has\s+been\s+sent\s+to';
117             # New Version of Yahoo
118             our $COMPOSE_SENT_OK_PRE = 'Message Sent
119             our $COMPOSE_SENT_OK_POST= '';
120              
121              
122              
123             # Fields for performing group operations - delete, move to, etc.
124             our $ACTION_FORM_NAME = 'messageList';
125             our $DELETE_FLAG_NAME = 'DEL';
126             our $MOVE_FLAG_NAME = 'MOV';
127             our $MOVE_TO_FOLDER_NAME = 'destBox';
128              
129              
130             ## Flag names & values. Used when sending, among other things.
131             use constant SAVE_COPY_TO_SENT_FOLDER => 1;
132             use constant SUPPRESS_BANNERS => 2;
133             use constant DELETE_ON_READ => 4; # This and MOVE_ON_READ are
134             use constant MOVE_ON_READ => 8; # mutually exclusive.
135             use constant ATTACH_SIG => 16;
136             use constant SEND_AS_HTML => 32;
137             ##use constant GET_UNREAD_ONLY => 64; # Not Yet Implemented..
138              
139             use constant YAHOO_MSG_FLAGS => qw(
140             SAVE_COPY_TO_SENT_FOLDER
141             SUPPRESS_BANNERS
142             DELETE_ON_READ
143             MOVE_ON_READ
144             ATTACH_SIG
145             SEND_AS_HTML
146             );
147              
148              
149             # ick.
150             our $ATTACH_PRE = q{\s*};
151             our $ATTACH_POST= q{(?)};
152             our $DOWNLOAD_FILE_LINK = qr{${ATTACH_PRE}Download File${ATTACH_POST}};
153             our $DOWNLOAD_FILE_LINK2 = qr{${ATTACH_PRE}Download Without Scan${ATTACH_POST}};
154              
155              
156             # Used for matching (actually, removing anything not) email addresses
157             our $NAME_PART = qr{("?[\w\s]+"?)?};
158             our $EMAIL_PART = qr{(?)};
159             #our $CLEAN_FROM = qr{(^From:)(?!$NAME_PART)?($NAME_PART)(?!$EMAIL_PART)?($EMAIL_PART).*};
160              
161              
162             ## http://us.f406.mail.yahoo.com/ym/ShowFolder?Search=&Npos=1&next=1&YY=88041&inc=200&order=down&sort=date&pos=0&view=a&head=b&box=Inbox
163             our $NEXT_MESSAGES_LINK = qr{[^<"]*ShowFolder\?Search.*?&next=1[^>"']*};
164             our $PREV_MESSAGES_LINK = qr{[^<"]*ShowFolder\?Search.*?&previous=1[^>"']*};
165              
166              
167              
168             sub new
169             {
170             my $class = shift;
171              
172             my %args = @_;
173              
174             my $ua = new LWP::UserAgent(agent => $USER_AGENT, env_proxy => $ENV_PROXY);
175            
176             my $self = bless {
177             _server => $args{server} || $SERVER,
178             _username => $args{username} || carp('No username defined'),
179             _password => $args{password} || carp('No password defined'),
180             _login_server => $args{login_server}|| $args{server} || $LOGIN_SERVER,
181             _cookie_file => $args{cookie_file},
182             _logged_in => 0,
183             _connected => 0,
184             _ua => $ua,
185             _html_parser => new Textractor,
186             }, $class;
187              
188              
189             if ($args{retrieve}) {
190             warn __PACKAGE__, ": new: use of 'retrieve' parameter is deprecated and will be ignored.\n";
191             }
192              
193              
194             if (!$self->{_ua}->is_protocol_supported('https')) {
195             die "https not supported by LWP. This application will not work.\n";
196             }
197              
198             $self->{_ua}->env_proxy;
199              
200             my $cookie_jar = new HTTP::Cookies::Netscape(
201             File => $self->{_cookie_file},
202             AutoSave => 1);
203              
204             $cookie_jar->load;
205              
206             $self->{_cookie_jar} = $cookie_jar;
207              
208             $self->{_ua}->cookie_jar($cookie_jar);
209              
210             $self->cache_messages(1);
211             $self->cache_headers(1);
212              
213             # FIXME: why?
214             $self->trace(0);
215            
216             return $self;
217             }
218              
219              
220             sub connect
221             {
222             my ($self) = @_;
223             return 0 if $self->{_connected};
224              
225             # FIXME: really connect if necessary.
226             $self->debug("connected.") if $self->trace;
227             $self->{_connected} = 1;
228             }
229              
230              
231             sub login
232             {
233             my ($self) = @_;
234             return 0 if $self->{_logged_in};
235              
236             $self->connect unless $self->{_connected};
237              
238             my $uri = $self->{_login_server};
239             $self->debug(" requesting login page '$uri'.") if $self->trace > 3;
240             my $info = $self->_get_a_page($uri, 'GET');
241             my $login_page = $info->content;
242              
243             unless ($login_page) {
244             $@ = "Problem getting login page.";
245             return undef;
246             }
247              
248             my $p = new HTML::FormParser;
249              
250             my @login_params;
251              
252             $self->debug(" parsing login page.") if $self->trace > 4;
253              
254             # Parse the returned 'welcome' page looking for a suspicious link to login
255             # with. This is kindly provided by (as of 2002-04-06 at least) the only form
256             # in the page. So hurrah.
257             # Note we don't store the login info in a cookie since it kinda makes no sense
258             # -- the only reason for doing so is to remove the need to enter a username in
259             # the login page; we provide this username in the object parameters.
260             # It might speed things up a little, but until Yahoo stops retiring sessions
261             # every eight hours or so, I'm not gonna bother re-using cookies.
262             my $pobj = $p->parse($login_page,
263             start_form => sub {
264             my ($attr, $origtext) = @_;
265             $self->{STORED_URIS}->{login} = $attr;
266             },
267             input => sub {
268             my ($attr, $origtext) = @_;
269             if ($attr->{name} eq $LOGIN_FIELD) {
270             $attr->{value} = $self->{_username};
271             } elsif ($attr->{name} eq $PASSWORD_FIELD) {
272             $attr->{value} = $self->{_password};
273             } elsif ($attr->{name} eq $SAVE_USER_INFO_FIELD) {
274             $attr->{value} = '';
275             $attr->{name} = '';
276             }
277             push @login_params, $attr;
278             }
279             );
280              
281              
282             my @params;
283             for (@login_params) {
284             next unless $_->{name};
285             push @params, "$_->{name}=$_->{value}";
286             }
287              
288              
289             # This bit makes the actual request to login, having stuffed the @params array
290             # with the fields gleaned from the login page (plus our username and password
291             # of course). Note that there is some feature in LWP that doesn't like
292             # redirects from https, so we have to give it an insecure URI here.
293             # (might be a POST issue - worked ok in other code)
294             # FIXME: track this down; provide a secure work-around.
295             $uri = $self->{STORED_URIS}->{login}->{action};
296             $uri =~ s/https/http/g;
297             my $meth = $self->{STORED_URIS}->{login}->{method};
298             # for (@params) { warn "$_\n" }
299              
300             $info = $self->_get_a_page($uri, $meth, \@params);
301             my $welcome_page = $info->content;
302              
303             unless ($welcome_page) {
304             $@ = "Unable to log in (No welcome page).";
305             $self->debug($@) if $self->trace;
306             return undef;
307             }
308              
309             ## welcome_page could be the login page returned, in the event of login
310             ## failure.
311             if ($welcome_page !~ /$WELCOME_PAGE_CHECK/) {
312             $@ = "Unable to log in (welcome page did not contain welcome text).";
313             $self->debug($@) if $self->trace;
314             # $self->debug($welcome_page) if $self->trace > 9;
315             return undef;
316             }
317              
318              
319             $self->{STORED_PAGES}->{welcome} = $welcome_page;
320              
321             $self->debug("Welcome page is ($welcome_page)") if $self->trace > 9;
322              
323             my $logged_in_uri = $info->request->url;
324              
325             $self->{STORED_URIS}->{base} = make_host($logged_in_uri);
326             $self->{STORED_URIS}->{welcome} = $logged_in_uri;
327              
328             $self->debug("logged in.") if $self->trace;
329             $self->debug("Base URI is $self->{STORED_URIS}->{base}") if $self->trace > 4;
330             $self->debug("Welcome URI is $self->{STORED_URIS}->{welcome}") if $self->trace > 4;
331             $self->{_logged_in} = 1;
332              
333             return 1;
334             }
335              
336              
337              
338              
339             sub get_mail_messages
340             {
341             my ($self, $mbox, $msg_list, $flags, $newfol) = @_;
342             $flags ||= 0;
343              
344              
345             if (!$self->{_logged_in}) {
346             # Although ideally login() should print some diagnostics, it might be called
347             # from an application with "no" stderr. At this point, however, we are
348             # acting as an application function, so we die with the generated error here.
349             # If the application author wishes to live through this, perhaps to ask the
350             # user for another password, the call to get_mail_messages() can be eval'd.
351             if (!$self->login) {
352             die "get_mail_messages: $@\n";
353             }
354             }
355              
356             $self->get_folder_list;
357             my @msgs = $self->get_folder_index($mbox);
358              
359             if ($flags & DELETE_ON_READ && $flags & MOVE_ON_READ) {
360             warn "DELETE_ON_READ and MOVE_ON_READ are mutually incompatible.\n";
361             warn "MOVE_ON_READ takes precedence.\n";
362             $flags ^= DELETE_ON_READ;
363             }
364              
365             if ($flags & DELETE_ON_READ) {
366             my $l = $self->get_folder_action_link($mbox, $DELETE_FLAG_NAME);
367             if (!$l) {
368             warn "Unable to get 'Delete' URI - messages will NOT be deleted.\n";
369             $flags ^= DELETE_ON_READ;
370             }
371             }
372              
373             if ($flags & MOVE_ON_READ) {
374             die "No folder to move to!\n" unless $newfol;
375             my $l = $self->get_folder_action_link($mbox, $MOVE_FLAG_NAME);
376             if (!$l) {
377             warn "Unable to get 'Move' URI - messages will NOT be moved.\n";
378             $flags ^= MOVE_ON_READ;
379             }
380             }
381              
382             my @messages;
383              
384             my @message_nums;
385              
386             # FIXME: confusing? bleh. could be construed as 'message numbers' or
387             # 'start..end'.
388             if (ref($msg_list) eq 'ARRAY') {
389             @message_nums = @{$msg_list};
390             $self->debug("Fetching messages numbered @message_nums") if $self->trace;
391             } elsif (lc($msg_list) eq 'all' || !$msg_list) {
392             @message_nums = (1..@msgs);
393             }
394              
395             my $mcount = 0;
396              
397             for (@msgs) {
398             ++$mcount;
399             next unless @message_nums && grep { $_ == $mcount } @message_nums;
400              
401             # Change the program name to display the number of the current message, if
402             # supported.
403             (my $prog = $0) =~ s/\s+\d+\s+messages//g;
404             $prog .= ' ' . (0+@messages) . ' messages';
405             $0 = $prog;
406              
407              
408             my $uri = $_->{uri};
409             $uri =~ s/$EMPTY_FULL_HEADER_FLAG//g;
410             $uri .= "&" . $FULL_HEADER_FLAG;
411             $uri =~ s/inc=\d+\&?//g;
412             my ($yahoo_msg_id) = $uri =~ /MsgId=([^&]+)&/;
413              
414             my $info = $self->_get_a_page($uri);
415             my $page = $info->content;
416            
417             if ($page) {
418              
419             $self->debug("Processing page at $uri") if $self->trace;
420             push @messages, $self->_process_message($page, $yahoo_msg_id);
421              
422             if ($flags & DELETE_ON_READ) {
423             my $uri = $self->{STORED_URIS}->{base} .
424             $self->{STORED_URIS}->{DEL_action};
425             $uri .= "&Mid=$yahoo_msg_id";
426             $self->debug("Deleting $yahoo_msg_id") if $self->trace;
427             my $page = $self->_get_a_page($uri, 'GET');
428             }
429              
430              
431             if ($flags & MOVE_ON_READ) {
432             my $uri = $self->{STORED_URIS}->{base} .
433             $self->{STORED_URIS}->{MOV_action};
434             $uri .= "&$MOVE_TO_FOLDER_NAME=$newfol&Mid=$yahoo_msg_id";
435             $self->debug("Moving $yahoo_msg_id to $newfol with $uri")
436             if $self->trace;
437             my $page = $self->_get_a_page($uri, 'GET');
438             }
439              
440             } else {
441              
442             warn "Couldn't retrieve message id $_->{id}\n";
443              
444             }
445             }
446              
447             return @messages;
448             }
449              
450              
451              
452             sub _process_message
453             {
454             my ($self, $page, $yahoo_msg_id) = @_;
455              
456             my $mhdr = $self->_extract_headers($page, $yahoo_msg_id);
457             if ($mhdr) {
458             $self->_extract_body($mhdr, $page);
459             }
460              
461             ### push @messages, $mhdr;
462             ### print 0+@messages, " messages\n" if (!(@messages % 20));
463              
464             return $mhdr;
465             }
466              
467              
468              
469             sub _extract_headers
470             {
471             my ($self, $page, $yahoo_msg_id) = @_;
472             my @hdrs;
473              
474             my $p = new HTML::TableContentParser;
475             my $stored_tables = $p->parse($page);
476              
477             my $from_date = '';
478              
479              
480             # print SIMONLOG "sdd 025.$mcount; ($page)\n";
481              
482             for my $t (@$stored_tables) {
483             next unless $t->{rows};
484              
485             for my $r (@{$t->{rows}}) {
486             next unless $r->{cells};
487              
488             for my $c (0..@{$r->{cells}}-1) {
489              
490             # We're only interested in data that contains a message header, and the field
491             # associated with it -- but there may be a bunch of other crap stuck in by
492             # yahoo that 'looks like' a message header. So we validate against a known
493             # list of message headers. The first check is faster than examining every item
494             # of data through the grep.
495              
496             next unless my $field = $r->{cells}->[$c]->{data};
497             my $data = $r->{cells}->[$c+1]->{data};
498              
499             my $mp = new Mail::Webmail::MessageParser;
500             $mp->{_debug} = $self->trace;
501             my $hdr = $mp->parse_header($field, $data);
502             $mp->delete(); # Free allocated memory
503             next unless $hdr;
504             ++$c;
505              
506             # 'From' header has 'block address' and other crap in it..
507             if ($hdr =~ /^From/) {
508             # Remove everything not looking like an email address..
509             # Make no attempt to validate the address; just remove non-compliant
510             # characters (actually quite hard.. the address itself is all we care about,
511             # really, but we'll try and get the "name" part)
512             # $hdr =~ s/((\ )|(\s*))?(\||\240).*//g;
513             # $hdr =~ s/$CLEAN_FROM/$1 $2 $3/;
514             my ($from, $name) = $hdr =~ /(From:?)\s*($NAME_PART)/i;
515             $name ||= '';
516             my ($email) = $hdr =~ /($EMAIL_PART)/i;
517             $hdr = "$from $name $email";
518              
519             # Also add a 'From' line so pine et al recognise it as a message.
520             $from = "$name $email";
521             # $from =~ s/".*"//g;
522             # $from =~ s/<|>//g;
523              
524             push @hdrs, "From $from";
525              
526             } elsif ($hdr =~ /Date/) {
527             # Sometimes the date field gets molestered..
528             if ($hdr =~ /$DATE_MOLESTERED_STRING/) {
529             $hdr = ' ' . scalar localtime time;
530             }
531             # ($from_date = $data) =~ s/,//g;
532             $from_date = ' ' . scalar localtime time;
533             }
534              
535             push @hdrs, $hdr;
536             }
537             }
538             }
539             # Add the Yahoo message Id - this might come in useful.
540             push @hdrs, "X-Yahoo-MsgId: $yahoo_msg_id";
541              
542             # Add our own header - might be useful
543             push @hdrs, "X-Mail-Webmail-Yahoo-Version: $VERSION";
544              
545             # Sort the headers so 'From' comes first..
546             my $hdr = [sort { $a =~ /^From\s+/ ? -1 : 1 } @hdrs];
547             # ..and add the date to the 'From' header, so it looks like mail.
548             $hdr->[0] .= $from_date;
549              
550              
551             # Finally construct a new Mail object containing our headers and return it.
552             my $mhdr = new Mail::Internet($hdr);
553             return $mhdr;
554             }
555              
556              
557              
558             sub _extract_body
559             {
560             my ($self, $mhdr, $page) = @_;
561             # So much for the header, now for the body.. Yahoo kindly provides
562             #
at the top, but the bottom is just a
. So we have to
563             # hope the HTML is correctly formed, or at least those parts of it - a stray
564             # inside the message body will cause problems. See the documentation
565             # for MessageParser for more.
566             my $mp = new Mail::Webmail::MessageParser;
567             $mp->{_debug} = $self->trace;
568             # Gets the part of the page that contains the message, as defined by Yahoo..
569             $mp->message_start(_tag => 'div', id => 'message');
570             $mp->message_read($page);
571              
572             # Yahoo quite decently provides a way to remove inlined attachments..
573             $mp->remove_matching(_tag => 'a', name => 'attachments');
574              
575             # Remove any extra HTML that might appear around the delivered body..
576             $mp->extract_body([_tag => 'table'], [_tag => 'tr'], [_tag => 'td']);
577             # Yahoo gives text messages 'pre' tags..
578             # Ha! We don't need to do this - the
 tags will get swallowed in the 
579             # conversion to text (as_text). Of course, this relies on the content-type
580             # being set correctly..
581             ######## $mp->extract_body([_tag => 'pre'], [_tag => 'tt']);
582             # And finally get the body text in the required form.
583             my $body = $mp->body_as_appropriate($mhdr);
584             $mp->delete();
585              
586              
587             # Set the body. Grue. I kinda think it would be nice if $mhdr->print_body
588             # could be given a delimiter to print between each pair of elements.
589             my @body = map { "$_\n" } split /\n/, $body;
590             $mhdr->body(@body);
591              
592             # Check for downloadable attachments, mime-encode, and stuff into the message
593             # using some magic to set content types etc.
594             while ($page =~ s{$DOWNLOAD_FILE_LINK}{}si ||
595             $page =~ s{$DOWNLOAD_FILE_LINK2}{}si) {
596             my $download_link = $1;
597             $self->debug("Attachment link: $download_link") if $self->trace > 3;
598             my $url = make_host($_->{uri});
599             $download_link .= $FULL_HEADER_FLAG;
600             my $link = $url . $download_link;
601             $self->download_attachment($link, $mhdr);
602             }
603             return 1; # no errors?
604             }
605              
606              
607              
608              
609             sub download_attachment
610             {
611             my ($self, $download_link, $snagmsg) = @_;
612              
613             my ($filename) = $download_link =~ /filename=([^\&]*)/;
614             my $info = $self->_get_a_page($download_link);
615              
616             if ($snagmsg) {
617             $self->add_attachment_to_message($snagmsg, $info, $filename);
618             }
619              
620             return $info;
621             }
622              
623              
624              
625              
626             sub add_attachment_to_message
627             {
628             my ($self, $msg, $att, $filename) = @_;
629              
630             my $filedata = $att->content;
631              
632             my $ct = $msg->get('Content-Type') || '';
633             $self->debug("Content-Type for $filename: $ct") if $self->trace > 3;
634              
635             # This shouldn't happen, but can if we can't, for some reason, get the full
636             # header page.
637             # TODO: write make_multipart_boundary!
638             if ($ct !~ /multipart\/mixed/i) {
639             $msg->replace('Content-Type', $self->make_multipart_boundary($msg));
640             $ct = $msg->get('Content-Type');
641             }
642              
643             $ct =~ s/boundary="?([^"]+)"?//i;
644             my $bndry = $1;
645             $msg->replace('MIME-Version', '1.0');
646              
647             ## --0-1260933182-1019570195=:33950
648             ## Content-Type: text/plain; charset=us-ascii
649             ## Content-Disposition: inline
650              
651             # TODO: tidy this up a bit
652             my @body = @{$msg->body};
653             unless ($body[0] =~ m{This is a multi-part message in MIME format.}) {
654             unshift @body,
655             "This is a multi-part message in MIME format.\n\n",
656             "--$bndry\n",
657             "Content-Type: $ct; charset=us-ascii\n",
658             "Content-Disposition: inline;\n\n";
659             }
660            
661             my $encoded_data = MIME::Base64::encode_base64($filedata);
662              
663             push @body, "--$bndry\n",
664             "Content-Type: ", join('; ', $att->content_type), "\n",
665             "Content-Transfer-Encoding: base64\n",
666             "Content-Disposition: attachment; filename=$filename\n\n",
667             $encoded_data;
668             $msg->body(@body);
669            
670             }
671              
672              
673              
674             sub make_multipart_boundary
675             {
676             }
677              
678              
679              
680              
681             sub get_folder_action_link
682             {
683             my ($self, $mbox, $linktype, $force) = @_;
684              
685             $self->login unless $self->{_logged_in};
686              
687             if (!$self->{STORED_URIS}->{folder_list}->{$mbox}) {
688             die "No such folder '$mbox' found in list.\n";
689             }
690              
691             my $index;
692             if (!($index = $self->{STORED_PAGES}->{message_index}->{$mbox}->[0])
693             || $force) {
694             my $uri = $self->{STORED_URIS}->{folder_list}->{$mbox};
695             $self->debug("INDEX URI for $mbox: $uri") if $self->trace() > 1;
696             my $info = $self->_get_a_page($uri);
697             $index = $info->content;
698              
699             $self->{STORED_PAGES}->{message_index}->{$mbox}->[0] = $index;
700             }
701              
702              
703             my $form_uri = '';
704             my @params = ();
705             my $start_collecting = 0;
706             my $p = new HTML::FormParser;
707              
708             my $pobj = $p->parse($index,
709             start_form => sub {
710             my ($attr, $origtext) = @_;
711             if ($attr->{name} eq $ACTION_FORM_NAME) {
712             $form_uri = $attr->{action};
713             $start_collecting = 1;
714             }
715             },
716              
717             start_input => sub {
718             my ($attr, $origtext) = @_;
719             return unless $start_collecting;
720             return unless $attr->{name};
721             if ($attr->{name} eq '.crumb' || $attr->{name} eq $linktype) {
722             $attr->{value} = 1 if $attr->{name} eq $linktype;
723             push @params, "$attr->{name}=$attr->{value}";
724             }
725             },
726             );
727              
728             return undef unless $form_uri;
729            
730             # store link as well as return it
731             my $plist = join '&', @params;
732             $form_uri .= $form_uri =~ /\?/ ? "&$plist" : "?$plist";
733             $self->{STORED_URIS}->{"${linktype}_action"} = $form_uri;
734             return $form_uri;
735              
736             }
737              
738              
739              
740              
741             sub get_folder_index
742             {
743             my ($self, $mbox) = @_;
744              
745             $mbox ||= 'Inbox';
746             $self->login unless $self->{_logged_in};
747              
748             if (!$self->{STORED_URIS}->{folder_list}->{$mbox}) {
749             die "No such folder '$mbox' found in list.\n";
750             }
751              
752             my $uri = $self->{STORED_URIS}->{folder_list}->{$mbox};
753             $self->debug("INDEX URI for $mbox: $uri") if $self->trace() > 1;
754             my $info = $self->_get_a_page($uri);
755             my $index = $info->content;
756              
757             $self->{STORED_PAGES}->{message_index}->{$mbox}->[0] = $index;
758              
759             my @msgs;
760              
761             if ($index) { push @msgs, $self->_get_message_links($index) }
762              
763              
764             # Handle 'next' and 'previous' - mail box might be set up to display in
765             # reverse. We'll continue to follow the first of either type found.
766             # If 'next', has_more = 1. If 'prev', has_more = -1, otherwise 0.
767             my $has_more = 0;
768             my $more_page;
769             do {
770             if ($has_more >= 0) {
771             $more_page = ($index =~ /($NEXT_MESSAGES_LINK)/i)[0];
772             $has_more = $more_page ? 1 : 0;
773             }
774             if ($has_more <= 0) {
775             $more_page = ($index =~ /($PREV_MESSAGES_LINK)/i)[0];
776             $has_more = $more_page ? -1 : 0;
777             }
778            
779             if ($has_more) {
780             $self->debug(" following link for more messages") if $self->trace > 4;
781             my $url = new URI::URL($uri);
782             my $link = $url->scheme . '://' . $url->host . $more_page;
783             $index = $self->_get_a_page($link)->content;
784             if ($index) { push @msgs, $self->_get_message_links($index) }
785             }
786              
787             } while ($has_more != 0);
788              
789             return @msgs;
790             }
791              
792              
793              
794             sub _get_message_links
795             {
796             my ($self, $page) = @_;
797             my @msgs;
798             my $p = new HTML::LinkExtor(
799             sub
800             {
801             my ($tag, $type, $uri) = @_;
802             # Attachment links are shown before the message subject-link.
803             if ($type eq 'href' &&
804             $uri =~ /$SHOW_MSG_APP_NAME\?.*MsgId=([^\&]*)/i &&
805             $uri !~ /$SHOW_TOC/i &&
806             $uri !~ /$ATTACH_SECTION/i) {
807             $self->debug(" get_message_list: $uri") if $self->trace > 4;
808             $self->{STORED_URIS}->{messages}->{$1} = $uri;
809             # Use a separate array here rather than simply returning the keys of the
810             # STORED_URIS->message hash since we're only interested in one folder.
811             push @msgs, {
812             id => $1,
813             uri => $uri,
814             };
815             }
816             },
817             $self->{STORED_URIS}->{base});
818              
819             $p->parse($page);
820              
821             return @msgs;
822             }
823              
824              
825             sub get_folder_list
826             {
827             my ($self) = @_;
828             $self->login unless $self->{_logged_in};
829              
830             my $index = $self->{STORED_PAGES}->{welcome};
831             if (!$index) {
832             my $info = $self->_get_a_page($self->{_server});
833             my $server = $info->request->uri;
834             $index = $info->content;
835             $self->{STORED_URIS}->{folder} = $server;
836             }
837              
838              
839             if (!$self->{STORED_URIS}->{front_page}) {
840             my $p = new HTML::LinkExtor(
841             sub
842             {
843             my ($tag, $type, $uri) = @_;
844             if ($type eq 'href' && $uri =~ /$FOLDER_APP_NAME\?/) {
845             $self->debug("FRONT PAGE: $uri") if $self->trace > 4;
846             $self->{STORED_URIS}->{front_page} =
847             $uri;
848             }
849             },
850             $self->{STORED_URIS}->{base});
851              
852             $p->parse($index);
853             }
854              
855             # TODO: inefficient to get this more than once per session - check for folders
856             # already before collecting/ parsing page again.
857              
858             if ($self->{STORED_URIS}->{front_page}) {
859             my $indp = $self->{STORED_PAGES}->{index_page} ||
860             $self->_get_a_page($self->{STORED_URIS}->{front_page})->content;
861              
862             my $p = new HTML::LinkExtor(
863             sub
864             {
865             my ($tag, $type, $uri) = @_;
866             if ($type eq 'href') {
867             if ($uri =~ /$SHOW_FOLDER_APP_NAME\?.*box=([^\&]*)/) {
868             $self->{STORED_URIS}->{folder_list}->{$1} = $uri;
869             $self->debug(" get_folder_list: $uri for $1") if $self->trace > 4;
870             # Yahoo has these two special folders - Bulk & Trash. 'Empty' works magically
871             # on them..
872             } elsif ($uri =~ /$EMPTY_FOLDER_APP_NAME\?.*\b?EB=1/) {
873             # For some reason Yahoo names the bulk folder '%40B%40Bulk' (@B@Bulk)
874             $self->{STORED_URIS}->{empty_folder_list}->{Bulk} = $uri;
875             $self->debug(" get_folder_list: Empty: $uri for Bulk") if $self->trace > 4;
876             } elsif ($uri =~ /$EMPTY_FOLDER_APP_NAME\?.*\b?ET=1/) {
877             $self->{STORED_URIS}->{empty_folder_list}->{Trash} = $uri;
878             $self->debug(" get_folder_list: Empty: $uri for Trash") if $self->trace > 4;
879             }
880             }
881             },
882             $self->{STORED_URIS}->{base});
883              
884             $p->parse($indp);
885             }
886              
887             return keys %{$self->{STORED_URIS}->{folder_list}};
888             }
889              
890              
891              
892              
893              
894              
895              
896             sub send
897             {
898             my ($self, $to, $subject, $body, $cc, $bcc, $flags) = @_;
899              
900             $cc ||= '';
901             $bcc ||= '';
902             $flags ||= 0;
903              
904             my $really_to = ref($to) eq 'ARRAY' ? join ',', @$to : $to;
905             my $really_cc = ref($cc) eq 'ARRAY' ? join ',', @$cc : $cc;
906             my $really_bcc = ref($bcc) eq 'ARRAY' ? join ',', @$bcc : $bcc;
907              
908             unless ($self->{_logged_in}) {
909             if (!$self->login) {
910             # Although ideally login() should print some diagnostics, it might be called
911             # from an application with "no" stderr. At this point, however, send() is
912             # acting as an application function, so we die with the generated error here.
913             # If the application author wishes to live through this, perhaps to ask the
914             # user for another password, the call to send() can be eval'd.
915             die "send: $@\n";
916             }
917             }
918              
919             my $compose_uri = $self->{STORED_URIS}->{compose};
920              
921              
922             if (!$compose_uri) {
923             my $p = new HTML::LinkExtor(
924             sub
925             {
926             my ($tag, $type, $uri) = @_;
927             if ($type eq 'href' && $uri =~ /$COMPOSE_APP_NAME/i) {
928             $self->{STORED_URIS}->{compose} = $uri;
929             $compose_uri = $uri;
930             }
931             },
932             $self->{STORED_URIS}->{base});
933              
934             $p->parse($self->{STORED_PAGES}->{welcome});
935             }
936              
937             if (!$compose_uri) {
938             warn "send: Couldn't get compose URI.\n";
939             return undef;
940             }
941              
942             my $compose_page = $self->{STORED_PAGES}->{compose};
943            
944             unless ($compose_page) {
945             my $compose_resp = $self->_get_a_page($compose_uri);
946             $compose_page = $compose_resp->content;
947             }
948              
949             unless ($compose_page) {
950             warn "send: Unable to retrieve compose page.\n";
951             return undef;
952             }
953              
954              
955             my $p = new HTML::FormParser;
956              
957             my @compose_params;
958              
959             my $pobj = $p->parse($compose_page,
960             start_form => sub {
961             my ($attr, $origtext) = @_;
962             $self->{STORED_URIS}->{send} = $attr;
963             },
964             start_input => sub {
965             my ($attr, $origtext) = @_;
966             if (my $name = $attr->{name}) {
967             if ($name eq $COMPOSE_TO_FIELD) {
968             $attr->{value} = $really_to;
969             } elsif ($name eq $COMPOSE_CC_FIELD) {
970             $attr->{value} = $really_cc;
971             } elsif ($name eq $COMPOSE_BCC_FIELD) {
972             $attr->{value} = $really_bcc;
973             } elsif ($name eq $COMPOSE_SUBJ_FIELD) {
974             $attr->{value} = $subject;
975             } elsif ($name eq $COMPOSE_BODY_FIELD) {
976             $attr->{value} = $body;
977             } elsif ($name eq $COMPOSE_MONEY_FIELD) {
978             $attr->{value} = "";
979             } elsif ($name eq $COMPOSE_SEND_MONEY_CHK) {
980             $attr->{value} = "";
981             } elsif ($name eq $COMPOSE_ATTACH_SIG) {
982             $attr->{value} = $flags & ATTACH_SIG ? 'yes' : 'no';
983             } elsif ($name eq $COMPOSE_SEND_HTML) {
984             $attr->{value} = $flags & SEND_AS_HTML ? 'yes' : 'no';
985             } elsif ($name eq $COMPOSE_SAVE_COPY) {
986             $attr->{value} = $flags & SAVE_COPY_TO_SENT_FOLDER ? 'yes' : 'no';
987             }
988             push @compose_params, $attr;
989             }
990             },
991             start_textarea => sub {
992             my ($attr, $origtext) = @_;
993             if ($attr->{name} eq $COMPOSE_BODY_FIELD) {
994             $attr->{value} = $body;
995             }
996             push @compose_params, $attr;
997             }
998            
999              
1000             );
1001              
1002              
1003             my @params;
1004             for (@compose_params) {
1005             next unless $_->{name};
1006             $_->{value} ||= '';
1007             push @params, "$_->{name}=$_->{value}";
1008             }
1009              
1010             my $uri = make_host($self->{STORED_URIS}->{welcome});
1011             $uri .= $self->{STORED_URIS}->{send}->{action};
1012             $uri =~ s/https/http/g;
1013             my $meth = $self->{STORED_URIS}->{send}->{method};
1014              
1015             $self->debug("Sending '$subject' to ", join(';', $really_to, $really_cc, $really_bcc)) if $self->trace;
1016              
1017             my $info = $self->_get_a_page($uri, $meth, \@params);
1018             my $recvd = $info->content;
1019              
1020             open MSGSENT, ">sent";
1021             print MSGSENT $recvd;
1022             close MSGSENT;
1023              
1024             ## my $check_sent_ok = $COMPOSE_SENT_OK_PRE . "\\($subject\\)"
1025             ## . $COMPOSE_SENT_OK_POST;
1026              
1027             ## New version of Yahoo doesn't use the subject in sent confirmation..
1028             my $check_sent_ok = $COMPOSE_SENT_OK_PRE . $COMPOSE_SENT_OK_POST;
1029              
1030             if ($recvd =~ /$check_sent_ok/) {
1031             $self->debug("Sent '$subject' to ", join(';',$really_to, $really_cc, $really_bcc)) if $self->trace;
1032             return 1;
1033             }
1034             warn "send: Sent message page did not contain expected string. Message may not have been sent successfully.\n";
1035             return 0;
1036              
1037             }
1038              
1039              
1040              
1041             # Empties the specified magic folder - only Bulk | Trash as of 2003/10/08
1042             # Returns 1 on 'successful' empty, 0 otherwise.
1043             sub empty
1044             {
1045             my ($self, $folder) = @_;
1046             unless ($self->{_logged_in}) {
1047             if (!$self->login) {
1048             # See notes for 'send'
1049             die "empty: $@\n";
1050             }
1051             }
1052             $self->get_folder_list;
1053              
1054              
1055             my $uri = $self->{STORED_URIS}->{empty_folder_list}->{$folder};
1056             if (!exists $self->{STORED_URIS}->{empty_folder_list}->{$folder}) {
1057             $@ = "Can't empty folder '$folder'.";
1058             return 0;
1059             }
1060              
1061             $self->_get_a_page($uri);
1062              
1063             return 1;
1064             }
1065              
1066              
1067              
1068              
1069              
1070             # TODO: allow $params to be a hashref perhaps
1071             sub _get_a_page
1072             {
1073             my ($self, $uri, $method, $params) = @_;
1074              
1075             return undef unless $uri;
1076              
1077             $method ||= 'GET';
1078             $method =~ tr/a-z/A-Z/;
1079              
1080              
1081             my $req = new HTTP::Request($method, $uri);
1082              
1083             my $post_content = '';
1084             if (ref($params) eq 'ARRAY') {
1085             my @vars;
1086             for (@$params) {
1087             my ($name, $value) = $_ =~ /([^=]*)=?(.*)/s;
1088             push @vars, "$name=" . CGI::escape($value);
1089             }
1090             my $char = $method eq 'GET' ? '&' : "\r\n";
1091             # POST doesn't like \r\n-separated content :/
1092             $char = '&';
1093             $post_content = join $char, @vars;
1094             $post_content .= $char if $char ne '&';
1095             }
1096              
1097              
1098             if ($post_content) {
1099             if ($method =~ /POST/) {
1100             $req->content($post_content);
1101             $req->content_type('application/x-www-form-urlencoded');
1102             $req->content_length(length $post_content);
1103             } elsif ($method =~ /GET/) {
1104             $uri .= "?$post_content";
1105             $uri =~ s/\?([^\?]*)\?/?$1&/g;
1106             }
1107              
1108             }
1109              
1110             $self->debug(" requesting uri '$uri' via $method.") if $self->trace > 1;
1111             $self->debug(" parameters: $post_content")
1112             if $post_content && $self->trace > 3;
1113              
1114             $self->debug(" Request: === \n", $req->as_string, "===\n")
1115             if $self->trace > 4;
1116              
1117             $req->header(pragma => 'no-cache');
1118              
1119             $req->header(Accept => 'text/html, text/plain, application/x-director, application/x-shockwave-flash, image/x-quicktime, video/quicktime, image/jpeg, image/*, application/x-gunzip, application/x-gzip, application/x-bunzip2, application/x-tar-gz, audio/*, video/*, text/sgml, video/mpeg, image/jpeg, image/tiff, image/x-rgb, image/png, image/x-xbitmap, image/x-xbm, image/gif, application/postscript, */*;q=0.01');
1120              
1121              
1122             # $req->header(Accept_Encoding => 'gzip, compress');
1123             $req->header(Accept_Language => '*');
1124             $req->header(Cache_Control => 'no-cache');
1125             $req->header(Referer => 'file://none.html');
1126            
1127             ## $self->{_cookie_jar}->add_cookie_header($req);
1128             my $resp = $self->{_ua}->request($req);
1129              
1130             $self->{_cookie_jar}->extract_cookies($resp);
1131             $self->{_cookie_jar}->save;
1132            
1133             ## $self->debug(" Response:\n", $resp->as_string, "\n\n") if $self->trace > 9;
1134             $self->debug(" returned code ", $resp->code, ".") if $self->trace > 2;
1135              
1136             $self->debug(" request uri ", $resp->request->url) if $self->trace > 4;
1137             $self->debug(" request contents ", $resp->content) if $self->trace > 9;
1138              
1139             # FIXME: Not sure about this guy. Seems like redirects are always gonna be
1140             # GETs even if the original request was a POST. Little bit of hokum from Yahoo
1141             # with their multiple-302 chain.
1142             if ($resp->code == 302) {
1143             $uri = $resp->header('Location');
1144             $self->debug(" 302 (Moved Temporarily) to $uri encountered.")
1145             if $self->trace > 2;
1146             return $self->_get_a_page($uri, 'GET', $params);
1147             }
1148            
1149             return $resp;
1150             }
1151              
1152              
1153              
1154              
1155             sub debug
1156             {
1157             my $self = shift;
1158             warn __PACKAGE__, ": @_\n";
1159             }
1160              
1161              
1162              
1163             sub make_host
1164             {
1165             my ($self, $uri) = @_;
1166            
1167             if (ref($self) ne __PACKAGE__) {
1168             $uri = $self;
1169             }
1170             my $url = new URI::URL($uri);
1171             return $url->scheme . '://' . $url->host . ':' . $url->port;
1172             }
1173              
1174              
1175             1;
1176              
1177              
1178             # Minimal package for extracting & storing message text
1179              
1180             package Textractor;
1181              
1182             use base 'HTML::Parser';
1183              
1184             sub parse_text
1185             {
1186             my ($self, $html) = @_;
1187             $self->{stored_text} = '';
1188             $self->parse($html);
1189             return $self->{stored_text};
1190             }
1191              
1192              
1193             sub text
1194             {
1195             my ($self, $text) = @_;
1196             $self->{stored_text} .= $text;
1197             }
1198              
1199              
1200             1;
1201              
1202              
1203              
1204              
1205             __END__