Skip Menu |

This queue is for tickets about the Mail-IMAPClient CPAN distribution.

Report information
The Basics
Id: 49691
Status: resolved
Priority: 0/
Queue: Mail-IMAPClient

People
Owner: PLOBBES [...] cpan.org
Requestors: ROBN [...] cpan.org
Cc:
AdminCc:

Bug Information
Severity: Important
Broken in: 3.00
Fixed in: 3.21



Subject: [PATCH] rewrite of fetch_hash to resolve several issues
fetch_hash() has a number of bugs that make it difficult to use for anything but the most trivial of uses: - it searches for the UID attribute in every result line. Many MTAs add the string "uid 510" or similar to the Received header to identify the unix user that sent the message. This matches and causes spurious entries in the result hash - it does not understand that BODY attributes will be returned in response to BODY.PEEK requests - the attribute value parser does not handle nested parentheses, making it impossible to request the BODYSTRUCTURE attribute The attached patch rewrites much of the internals, making it do the right thing in these cases. A cursory glance seems to show that it will produce the same results for calls that worked previously. The fact that its been broken for so long suggests to me that it has rarely been used, so hopefully there won't be any particular problem changing it (especially given the API break in the change to 3.0). I plan at some point to break out the response parsing bits into a seperate function so that it can be tested.
Subject: fetch-hash-rewrite.diff
diff --git a/lib/Mail/IMAPClient.pm b/lib/Mail/IMAPClient.pm index bda613d..b543622 100644 --- a/lib/Mail/IMAPClient.pm +++ b/lib/Mail/IMAPClient.pm @@ -2013,57 +2013,90 @@ sub fetch_hash { for (@words) { s/([\( ])FAST([\) ])/${1}FLAGS INTERNALDATE RFC822\.SIZE$2/i; -s/([\( ])FULL([\) ])/${1}FLAGS INTERNALDATE RFC822\.SIZE ENVELOPE BODY$2/i; + s/([\( ])FULL([\) ])/${1}FLAGS INTERNALDATE RFC822\.SIZE ENVELOPE BODY$2/i; } + my %words = map { uc($_) => 1 } @words; my $output = $self->fetch( $msgs, "($what)" ) or return undef; - for ( my $x = 0 ; $x <= $#$output ; $x++ ) { + while (my $l = shift @$output) { + $l =~ m/^\* (\d+) FETCH \(/g; my $mid = $1 || undef; + $mid or next; + my $entry = {}; - my $l = $output->[$x]; - if ( $self->Uid ) { - my $uid = $l =~ /\bUID\s+(\d+)/i ? $1 : undef; - $uid or next; + my ($key, $value); + ATTR: while ($l !~ m/\G\s*\)\s*$/gc) { + if ($l =~ m/\G\s*([\w\d\.]+(?:\[[^\]]+\])?)\s*/gc) { + $key = uc($1); + } + elsif (!defined $key) { + # some kind of malformed response + $self->LastError("Invaliid item name in FETCH response: $l"); + return undef; + } + + if ($l =~ m/\G\s*$/gc) { + $value = shift @$output; + $entry->{$key} = $value; + $l = shift @$output; + next ATTR; + } + + elsif ($l =~ m/\G(?:"([^"]+)"|([^()\s]+))\s*/gc) { + $value = defined $1 ? $1 : $2; + $entry->{$key} = $value; + next ATTR; + } + + elsif ($l =~ m/\G\(/gc) { + my $depth = 1; + $value = ""; + while ($l =~ m/\G(\(|\)|[^()]+)/gc) { + my $stuff = $1; + if ($stuff eq "(") { + $depth++; + $value .= "("; + } + elsif ($stuff eq ")") { + $depth--; + if ($depth == 0) { + $entry->{$key} = $value; + next ATTR; + } + $value .= ")"; + } + else { + $value .= $stuff; + } + } + m/\G\s*/gc; + } + + else { + $self->LastError("Invalid item value in FETCH response: $l"); + return undef; + } + } - if ( $uids->{$uid} ) { $entry = $uids->{$uid} } - else { $uids->{$uid} ||= $entry } + if ($self->Uid) { + $uids->{$entry->{UID}} = $entry; } else { - my $mid = $l =~ /^\* (\d+) FETCH/i ? $1 : undef; - $mid or next; - - if ( $uids->{$mid} ) { $entry = $uids->{$mid} } - else { $uids->{$mid} ||= $entry } + $uids->{$mid} = $entry; } - foreach my $w (@words) { - if ( $l =~ /\Q$w\E\s*$/i ) { - $entry->{$w} = $output->[ $x + 1 ]; - $entry->{$w} =~ s/(?:$CR?$LF)+$//og; - chomp $entry->{$w}; - } - elsif ( - $l =~ /\( # open paren followed by ... - (?:.*\s)? # ...optional stuff and a space - \Q$w\E\s # escaped fetch field<sp> - (?:" # then: a dbl-quote - (\\.| # then bslashed anychar(s) or ... - [^"]+) # ... nonquote char(s) - "| # then closing quote; or ... - \( # ...an open paren - ([^\)]*) # ... non-close-paren char(s) - \)| # then closing paren; or ... - (\S+)) # unquoted string - (?:\s.*)? # possibly followed by space-stuff - \) # close paren - /xi - ) - { - $entry->{$w} = defined $1 ? $1 : defined $2 ? $2 : $3; + for my $word (keys %$entry) { + next if exists $words{$word}; + + if (my ($stuff) = $word =~ m/^BODY(\[.*)$/) { + next if exists $words{"BODY.PEEK".$stuff}; } + + delete $entry->{$word}; } } + return wantarray ? %$uids : $uids; } diff --git a/lib/Mail/IMAPClient.pod b/lib/Mail/IMAPClient.pod index 1e44874..dc31dd3 100644 --- a/lib/Mail/IMAPClient.pod +++ b/lib/Mail/IMAPClient.pod @@ -1168,27 +1168,12 @@ This would result in L<Data::Dumper> output similar to the following: } }; -You can specify I<BODY[HEADER.FIELDS ($fieldlist)> as an argument, but -you should keep the following in mind if you do: - -B<1.> You can only specify one argument of this type per call. If you -need multiple fields, then you'll have to call B<fetch_hashref> -multiple times, each time specifying a different FETCH attribute but -the same. - -B<2.> Fetch operations that return RFC822 message headers return the -whole header line, including the field name and the colon. For -example, if you do a C<$imap-E<gt>fetch_hash("BODY[HEADER.FIELDS -(Subject)]")>, you will get back subject lines that start with -"Subject: ". - -By itself this method may be useful for, say, speeding up programs -that want the size of every message in a folder. It issues one -command and receives one (possibly long!) response from the server. -However, it's true power lies in the as-yet-unwritten methods that -will rely on this method to deliver even more powerful result hashes -(and which may even remove the restrictions mentioned in B<1> and -B<2>, above). Look for more new function in later releases. +By itself this method may be useful for, say, speeding up programs that +want the size of every message in a folder. It issues one command and +receives one (possibly long!) response from the server. However, it's +true power lies in the as-yet-unwritten methods that will rely on this +method to deliver even more powerful result hashes. Look for more new +function in later releases. This method is new with version 2.2.3 and is thus still experimental. If you decide to try this method and run into problems, please see the
Indeed this method has been horribly broken for anything but basic usage for a long long time so your patch is *much appreciated*. I'd definitely like to get in a few test cases before the next release so hopefully I'll get a little time to work on this sooner than later.
Original patch has a single char typo that would cause BODY[] not to be recognised as a valid return attribute. Updated patch attached.
diff --git a/lib/Mail/IMAPClient.pm b/lib/Mail/IMAPClient.pm index bda613d..4f7a815 100644 --- a/lib/Mail/IMAPClient.pm +++ b/lib/Mail/IMAPClient.pm @@ -2013,57 +2013,90 @@ sub fetch_hash { for (@words) { s/([\( ])FAST([\) ])/${1}FLAGS INTERNALDATE RFC822\.SIZE$2/i; -s/([\( ])FULL([\) ])/${1}FLAGS INTERNALDATE RFC822\.SIZE ENVELOPE BODY$2/i; + s/([\( ])FULL([\) ])/${1}FLAGS INTERNALDATE RFC822\.SIZE ENVELOPE BODY$2/i; } + my %words = map { uc($_) => 1 } @words; my $output = $self->fetch( $msgs, "($what)" ) or return undef; - for ( my $x = 0 ; $x <= $#$output ; $x++ ) { + while (my $l = shift @$output) { + $l =~ m/^\* (\d+) FETCH \(/g; my $mid = $1 || undef; + $mid or next; + my $entry = {}; - my $l = $output->[$x]; - if ( $self->Uid ) { - my $uid = $l =~ /\bUID\s+(\d+)/i ? $1 : undef; - $uid or next; + my ($key, $value); + ATTR: while ($l !~ m/\G\s*\)\s*$/gc) { + if ($l =~ m/\G\s*([\w\d\.]+(?:\[[^\]]*\])?)\s*/gc) { + $key = uc($1); + } + elsif (!defined $key) { + # some kind of malformed response + $self->LastError("Invaliid item name in FETCH response: $l"); + return undef; + } + + if ($l =~ m/\G\s*$/gc) { + $value = shift @$output; + $entry->{$key} = $value; + $l = shift @$output; + next ATTR; + } + + elsif ($l =~ m/\G(?:"([^"]+)"|([^()\s]+))\s*/gc) { + $value = defined $1 ? $1 : $2; + $entry->{$key} = $value; + next ATTR; + } + + elsif ($l =~ m/\G\(/gc) { + my $depth = 1; + $value = ""; + while ($l =~ m/\G(\(|\)|[^()]+)/gc) { + my $stuff = $1; + if ($stuff eq "(") { + $depth++; + $value .= "("; + } + elsif ($stuff eq ")") { + $depth--; + if ($depth == 0) { + $entry->{$key} = $value; + next ATTR; + } + $value .= ")"; + } + else { + $value .= $stuff; + } + } + m/\G\s*/gc; + } + + else { + $self->LastError("Invalid item value in FETCH response: $l"); + return undef; + } + } - if ( $uids->{$uid} ) { $entry = $uids->{$uid} } - else { $uids->{$uid} ||= $entry } + if ($self->Uid) { + $uids->{$entry->{UID}} = $entry; } else { - my $mid = $l =~ /^\* (\d+) FETCH/i ? $1 : undef; - $mid or next; - - if ( $uids->{$mid} ) { $entry = $uids->{$mid} } - else { $uids->{$mid} ||= $entry } + $uids->{$mid} = $entry; } - foreach my $w (@words) { - if ( $l =~ /\Q$w\E\s*$/i ) { - $entry->{$w} = $output->[ $x + 1 ]; - $entry->{$w} =~ s/(?:$CR?$LF)+$//og; - chomp $entry->{$w}; - } - elsif ( - $l =~ /\( # open paren followed by ... - (?:.*\s)? # ...optional stuff and a space - \Q$w\E\s # escaped fetch field<sp> - (?:" # then: a dbl-quote - (\\.| # then bslashed anychar(s) or ... - [^"]+) # ... nonquote char(s) - "| # then closing quote; or ... - \( # ...an open paren - ([^\)]*) # ... non-close-paren char(s) - \)| # then closing paren; or ... - (\S+)) # unquoted string - (?:\s.*)? # possibly followed by space-stuff - \) # close paren - /xi - ) - { - $entry->{$w} = defined $1 ? $1 : defined $2 ? $2 : $3; + for my $word (keys %$entry) { + next if exists $words{$word}; + + if (my ($stuff) = $word =~ m/^BODY(\[.*)$/) { + next if exists $words{"BODY.PEEK".$stuff}; } + + delete $entry->{$word}; } } + return wantarray ? %$uids : $uids; } diff --git a/lib/Mail/IMAPClient.pod b/lib/Mail/IMAPClient.pod index 1e44874..dc31dd3 100644 --- a/lib/Mail/IMAPClient.pod +++ b/lib/Mail/IMAPClient.pod @@ -1168,27 +1168,12 @@ This would result in L<Data::Dumper> output similar to the following: } }; -You can specify I<BODY[HEADER.FIELDS ($fieldlist)> as an argument, but -you should keep the following in mind if you do: - -B<1.> You can only specify one argument of this type per call. If you -need multiple fields, then you'll have to call B<fetch_hashref> -multiple times, each time specifying a different FETCH attribute but -the same. - -B<2.> Fetch operations that return RFC822 message headers return the -whole header line, including the field name and the colon. For -example, if you do a C<$imap-E<gt>fetch_hash("BODY[HEADER.FIELDS -(Subject)]")>, you will get back subject lines that start with -"Subject: ". - -By itself this method may be useful for, say, speeding up programs -that want the size of every message in a folder. It issues one -command and receives one (possibly long!) response from the server. -However, it's true power lies in the as-yet-unwritten methods that -will rely on this method to deliver even more powerful result hashes -(and which may even remove the restrictions mentioned in B<1> and -B<2>, above). Look for more new function in later releases. +By itself this method may be useful for, say, speeding up programs that +want the size of every message in a folder. It issues one command and +receives one (possibly long!) response from the server. However, it's +true power lies in the as-yet-unwritten methods that will rely on this +method to deliver even more powerful result hashes. Look for more new +function in later releases. This method is new with version 2.2.3 and is thus still experimental. If you decide to try this method and run into problems, please see the
By the way, I've started work on some tests. I'd hope to have something to you by next week.
Another updated patch. There was a case where the match for "* N FETCH" at the start of the line could fail but $mid could still end up being set. I've simplified the code at the top of the loop to handle this now. Sorry about all the mucking around. I'm pretty sure its right this time.
diff --git a/lib/Mail/IMAPClient.pm b/lib/Mail/IMAPClient.pm index bda613d..60ebf07 100644 --- a/lib/Mail/IMAPClient.pm +++ b/lib/Mail/IMAPClient.pm @@ -2013,57 +2013,90 @@ sub fetch_hash { for (@words) { s/([\( ])FAST([\) ])/${1}FLAGS INTERNALDATE RFC822\.SIZE$2/i; -s/([\( ])FULL([\) ])/${1}FLAGS INTERNALDATE RFC822\.SIZE ENVELOPE BODY$2/i; + s/([\( ])FULL([\) ])/${1}FLAGS INTERNALDATE RFC822\.SIZE ENVELOPE BODY$2/i; } + my %words = map { uc($_) => 1 } @words; my $output = $self->fetch( $msgs, "($what)" ) or return undef; - for ( my $x = 0 ; $x <= $#$output ; $x++ ) { + while (my $l = shift @$output) { + next if $l !~ m/^\* (\d+) FETCH \(/g; + my $mid = $1; + my $entry = {}; - my $l = $output->[$x]; - if ( $self->Uid ) { - my $uid = $l =~ /\bUID\s+(\d+)/i ? $1 : undef; - $uid or next; + my ($key, $value); + ATTR: while ($l !~ m/\G\s*\)\s*$/gc) { + if ($l =~ m/\G\s*([\w\d\.]+(?:\[[^\]]*\])?)\s*/gc) { + $key = uc($1); + } + elsif (!defined $key) { + # some kind of malformed response + $self->LastError("Invaliid item name in FETCH response: $l"); + return undef; + } + + if ($l =~ m/\G\s*$/gc) { + $value = shift @$output; + $entry->{$key} = $value; + $l = shift @$output; + next ATTR; + } + + elsif ($l =~ m/\G(?:"([^"]+)"|([^()\s]+))\s*/gc) { + $value = defined $1 ? $1 : $2; + $entry->{$key} = $value; + next ATTR; + } + + elsif ($l =~ m/\G\(/gc) { + my $depth = 1; + $value = ""; + while ($l =~ m/\G(\(|\)|[^()]+)/gc) { + my $stuff = $1; + if ($stuff eq "(") { + $depth++; + $value .= "("; + } + elsif ($stuff eq ")") { + $depth--; + if ($depth == 0) { + $entry->{$key} = $value; + next ATTR; + } + $value .= ")"; + } + else { + $value .= $stuff; + } + } + m/\G\s*/gc; + } + + else { + $self->LastError("Invalid item value in FETCH response: $l"); + return undef; + } + } - if ( $uids->{$uid} ) { $entry = $uids->{$uid} } - else { $uids->{$uid} ||= $entry } + if ($self->Uid) { + $uids->{$entry->{UID}} = $entry; } else { - my $mid = $l =~ /^\* (\d+) FETCH/i ? $1 : undef; - $mid or next; - - if ( $uids->{$mid} ) { $entry = $uids->{$mid} } - else { $uids->{$mid} ||= $entry } + $uids->{$mid} = $entry; } - foreach my $w (@words) { - if ( $l =~ /\Q$w\E\s*$/i ) { - $entry->{$w} = $output->[ $x + 1 ]; - $entry->{$w} =~ s/(?:$CR?$LF)+$//og; - chomp $entry->{$w}; - } - elsif ( - $l =~ /\( # open paren followed by ... - (?:.*\s)? # ...optional stuff and a space - \Q$w\E\s # escaped fetch field<sp> - (?:" # then: a dbl-quote - (\\.| # then bslashed anychar(s) or ... - [^"]+) # ... nonquote char(s) - "| # then closing quote; or ... - \( # ...an open paren - ([^\)]*) # ... non-close-paren char(s) - \)| # then closing paren; or ... - (\S+)) # unquoted string - (?:\s.*)? # possibly followed by space-stuff - \) # close paren - /xi - ) - { - $entry->{$w} = defined $1 ? $1 : defined $2 ? $2 : $3; + for my $word (keys %$entry) { + next if exists $words{$word}; + + if (my ($stuff) = $word =~ m/^BODY(\[.*)$/) { + next if exists $words{"BODY.PEEK".$stuff}; } + + delete $entry->{$word}; } } + return wantarray ? %$uids : $uids; } diff --git a/lib/Mail/IMAPClient.pod b/lib/Mail/IMAPClient.pod index 1e44874..dc31dd3 100644 --- a/lib/Mail/IMAPClient.pod +++ b/lib/Mail/IMAPClient.pod @@ -1168,27 +1168,12 @@ This would result in L<Data::Dumper> output similar to the following: } }; -You can specify I<BODY[HEADER.FIELDS ($fieldlist)> as an argument, but -you should keep the following in mind if you do: - -B<1.> You can only specify one argument of this type per call. If you -need multiple fields, then you'll have to call B<fetch_hashref> -multiple times, each time specifying a different FETCH attribute but -the same. - -B<2.> Fetch operations that return RFC822 message headers return the -whole header line, including the field name and the colon. For -example, if you do a C<$imap-E<gt>fetch_hash("BODY[HEADER.FIELDS -(Subject)]")>, you will get back subject lines that start with -"Subject: ". - -By itself this method may be useful for, say, speeding up programs -that want the size of every message in a folder. It issues one -command and receives one (possibly long!) response from the server. -However, it's true power lies in the as-yet-unwritten methods that -will rely on this method to deliver even more powerful result hashes -(and which may even remove the restrictions mentioned in B<1> and -B<2>, above). Look for more new function in later releases. +By itself this method may be useful for, say, speeding up programs that +want the size of every message in a folder. It issues one command and +receives one (possibly long!) response from the server. However, it's +true power lies in the as-yet-unwritten methods that will rely on this +method to deliver even more powerful result hashes. Look for more new +function in later releases. This method is new with version 2.2.3 and is thus still experimental. If you decide to try this method and run into problems, please see the
Turns out I had some spare time after all. Here's some tests. They should be enough to test most bits of the fetch_hash() parser, though I haven't done a coverage check or anything like that to confirm. It should be a useful starting point anyway.
diff --git a/MANIFEST b/MANIFEST index 32effad..f71af2b 100644 --- a/MANIFEST +++ b/MANIFEST @@ -32,6 +32,7 @@ prepare_dist sample.perldb t/basic.t t/bodystructure.t +t/fetch_hash.t t/messageset.t t/pod.t t/simple.t diff --git a/t/fetch_hash.t b/t/fetch_hash.t new file mode 100644 index 0000000..3f8d06a --- /dev/null +++ b/t/fetch_hash.t @@ -0,0 +1,325 @@ +#!/usr/bin/perl + +# +# tests for fetch_hash() +# +# fetch_hash() calls fetch() internally. rather than refactor fetch_hash() +# just for testing, we instead subclass M::IC and use the overidden fetch() to +# feed it test data. +# + +use strict; +use warnings; +use Test::More tests => 18; + +BEGIN { use_ok('Mail::IMAPClient') or exit; } + +my @tests = ( + [ + "unquoted value", + [ + q{* 1 FETCH (UNQUOTED foobar)}, + ], + [ [1], qw(UNQUOTED) ], + { + "1" => { + "UNQUOTED" => q{foobar}, + } + }, + ], + [ + "quoted value", + [ + q{* 1 FETCH (QUOTED "foo bar baz")}, + ], + [ [1], qw(QUOTED) ], + { + "1" => { + "QUOTED" => q{foo bar baz}, + }, + }, + ], + [ + "parenthesized value", + [ + q{* 1 FETCH (PARENS (foo bar))}, + ], + [ [1], qw(PARENS) ], + { + "1" => { + "PARENS" => q{foo bar}, + }, + }, + ], + [ + "parenthesized value with quotes", + [ + q{* 1 FETCH (PARENS (foo "bar" baz))}, + ], + [ [1], qw(PARENS) ], + { + "1" => { + "PARENS" => q{foo "bar" baz}, + }, + }, + ], + [ + "parenthesized value with parens at start", + [ + q{* 1 FETCH (PARENS ((foo) bar baz))}, + ], + [ [1], qw(PARENS) ], + { + "1" => { + "PARENS" => q{(foo) bar baz}, + }, + }, + ], + [ + "parenthesized value with parens in middle", + [ + q{* 1 FETCH (PARENS (foo (bar) baz))}, + ], + [ [1], qw(PARENS) ], + { + "1" => { + "PARENS" => q{foo (bar) baz}, + }, + }, + ], + [ + "parenthesized value with parens at end", + [ + q{* 1 FETCH (PARENS (foo bar (baz)))}, + ], + [ [1], qw(PARENS) ], + { + "1" => { + "PARENS" => q{foo bar (baz)}, + }, + }, + ], + [ + "complex parens", + [ + q{* 1 FETCH (PARENS ((((foo) "bar") baz (quux))))}, + ], + [ [1], qw(PARENS) ], + { + "1" => { + "PARENS" => q{(((foo) "bar") baz (quux))}, + }, + }, + ], + [ + "basic literal value", + [ + q{* 1 FETCH (LITERAL}, + q{foo}, + q{)}, + ], + [ [1], qw(LITERAL) ], + { + "1" => { + "LITERAL" => q{foo}, + }, + }, + ], + [ + "multiline literal value", + [ + q{* 1 FETCH (LITERAL}, + q{foo\r\nbar\r\nbaz\r\n}, + q{)}, + ], + [ [1], qw(LITERAL) ], + { + "1" => { + "LITERAL" => q{foo\r\nbar\r\nbaz\r\n}, + }, + }, + ], + [ + "multiple attributes", + [ + q{* 1 FETCH (FOO foo BAR bar BAZ baz)}, + ], + [ [1], qw(FOO BAR BAZ) ], + { + "1" => { + "FOO" => q{foo}, + "BAR" => q{bar}, + "BAZ" => q{baz}, + }, + }, + ], + [ + "dotted attribute", + [ + q{* 1 FETCH (FOO.BAR foobar)}, + ], + [ [1], qw(FOO.BAR) ], + { + "1" => { + "FOO.BAR" => q{foobar}, + }, + }, + ], + [ + "complex attribute", + [ + q{* 1 FETCH (FOO.BAR[BAZ (QUUX)] quuz)}, + ], + [ [1], q{FOO.BAR[BAZ (QUUX)]} ], + { + "1" => { + q{FOO.BAR[BAZ (QUUX)]} => q{quuz}, + }, + }, + ], + [ + "BODY.PEEK[] requests match BODY[] responses", + [ + q{* 1 FETCH (BODY[] foo)} + ], + [ [1], qw(BODY.PEEK[]) ], + { + "1" => { + "BODY[]" => q{foo}, + }, + }, + ], + [ + "BODY.PEEK[] requests match BODY.PEEK[] responses also", + [ + q{* 1 FETCH (BODY.PEEK[] foo)} + ], + [ [1], qw(BODY.PEEK[]) ], + { + "1" => { + "BODY.PEEK[]" => q{foo}, + }, + }, + ], + [ + "real life example", + [ + '* 1 FETCH (UID 541 FLAGS (\\Seen) INTERNALDATE "15-Sep-2009 20:05:45 +1000" RFC822.SIZE 771 BODY[HEADER.FIELDS (TO FROM DATE SUBJECT)]', + 'Date: Tue, 15 Sep 2009 20:05:45 +1000 +To: rob@pyro +From: rob@pyro +Subject: test Tue, 15 Sep 2009 20:05:45 +1000 + +', + ' BODY[]', + 'Return-Path: <rob@pyro> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on pyro.home +X-Spam-Level: +X-Spam-Status: No, score=-0.5 required=5.0 tests=ALL_TRUSTED,BAYES_00, + FH_FROMEML_NOTLD,TO_MALFORMED autolearn=no version=3.2.5 +X-Original-To: rob@pyro +Delivered-To: rob@pyro +Received: from pyro (pyro [127.0.0.1]) + by pyro.home (Postfix) with ESMTP id A5C8115A066 + for <rob@pyro>; Tue, 15 Sep 2009 20:05:45 +1000 (EST) +Date: Tue, 15 Sep 2009 20:05:45 +1000 +To: rob@pyro +From: rob@pyro +Subject: test Tue, 15 Sep 2009 20:05:45 +1000 +X-Mailer: swaks v20061116.0 jetmore.org/john/code/#swaks +Message-Id: <20090915100545.A5C8115A066@pyro.home> +X-Bogosity: Spam, tests=bogofilter, spamicity=0.999693, version=1.2.1 +Lines: 1 + +This is a test mailing +', + ') +', + ], + [ [1], q{BODY.PEEK[HEADER.FIELDS (To From Date Subject)]}, qw(FLAGS INTERNALDATE RFC822.SIZE BODY[]) ], + { + "1" => { + 'BODY[]' => 'Return-Path: <rob@pyro> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on pyro.home +X-Spam-Level: +X-Spam-Status: No, score=-0.5 required=5.0 tests=ALL_TRUSTED,BAYES_00, + FH_FROMEML_NOTLD,TO_MALFORMED autolearn=no version=3.2.5 +X-Original-To: rob@pyro +Delivered-To: rob@pyro +Received: from pyro (pyro [127.0.0.1]) + by pyro.home (Postfix) with ESMTP id A5C8115A066 + for <rob@pyro>; Tue, 15 Sep 2009 20:05:45 +1000 (EST) +Date: Tue, 15 Sep 2009 20:05:45 +1000 +To: rob@pyro +From: rob@pyro +Subject: test Tue, 15 Sep 2009 20:05:45 +1000 +X-Mailer: swaks v20061116.0 jetmore.org/john/code/#swaks +Message-Id: <20090915100545.A5C8115A066@pyro.home> +X-Bogosity: Spam, tests=bogofilter, spamicity=0.999693, version=1.2.1 +Lines: 1 + +This is a test mailing +', + 'INTERNALDATE' => '15-Sep-2009 20:05:45 +1000', + 'FLAGS' => '\\Seen', + 'BODY[HEADER.FIELDS (TO FROM DATE SUBJECT)]' => 'Date: Tue, 15 Sep 2009 20:05:45 +1000 +To: rob@pyro +From: rob@pyro +Subject: test Tue, 15 Sep 2009 20:05:45 +1000 + +', + 'RFC822.SIZE' => '771' + }, + }, + ], +); + +my @uid_tests = ( + [ + "uid enabled", + [ + q{* 1 FETCH (UID 123 UNQUOTED foobar)}, + ], + [ [123], qw(UNQUOTED) ], + { + "123" => { + "UNQUOTED" => q{foobar}, + } + }, + ], +); + +package Test::Mail::IMAPClient; + +use vars qw(@ISA); +@ISA = qw(Mail::IMAPClient); + +sub new { + my ($class, %args) = @_; + my %me = %args; + return bless \%me, $class; +} + +sub fetch { + my ($self, @args) = @_; + return $self->{_next_fetch_response} || []; +} + +package main; + +sub run_tests { + my ($imap, $tests) = @_; + + for my $test (@$tests) { + my ($comment, $fetch, $request, $response) = @$test; + $imap->{_next_fetch_response} = $fetch; + my $r = $imap->fetch_hash(@$request); + is_deeply($r, $response, $comment); + } +} + +my $imap = Test::Mail::IMAPClient->new(Uid => 0); +run_tests($imap, \@tests); + +$imap->Uid(1); +run_tests($imap, \@uid_tests);
Rob, the patches are excellent. I'll be releasing Mail::IMAPClient 3.21 very soon. Thank you!
Just released Mail::IMAPClient 3.21 which should resolve this bug.