Skip Menu |

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

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

People
Owner: PLOBBES [...] cpan.org
Requestors: gilles [...] lamiral.info
Cc:
AdminCc:

Bug Information
Severity: (no value)
Broken in: (no value)
Fixed in: 3.41



Subject: Better has_capability()
Date: Tue, 2 Oct 2018 22:42:15 +0200
To: bug-Mail-IMAPClient [...] rt.cpan.org
From: Gilles LAMIRAL <gilles [...] lamiral.info>
Hi Phil, I'm currently playing with has_capability() in the context of getting the value 35651584 from a "APPENDLIMIT=35651584" in a response like this one: * CAPABILITY IMAP4rev1 ... APPENDLIMIT=35651584 Well, I fail. Reading the code, I should know the value in order to ... not get it but to know this value is a good guess. It's not very usable, isn't it? $imap->has_capability('APPENDLIMIT=35651584') # Gives 1 $imap->has_capability('APPENDLIMIT') # Gives "" (meaning "has not this capability") The way to get the value is with $imap->{APPENDLIMIT} Well, ok. Some remarks here: 1) It breaks encapsulation! Why use OO then? 2) It could break more things. A server presenting a CAPABILITY Anything=any_value Anything2=any_value2 ... etc. will put that directly into the object structure at the top level like: $imap->{ ANYTHING } = any_value ; Ok it's uppercased but not very safe in the long run. Safer in term of namespace is to store like: $imap->{ CAPABILITY }->{ ANYTHING } = any_value ; 3) Other issue. There can be multiple values like in AUTH=PLAIN AUTH=LOGIN AUTH=CRAM-MD5 ... Well, again, it finishes with just one $imap->{ AUTH } = 'PLAIN' ; 4) Let's think about a better behaviour. Why not: $imap->capability() # no args => same as already coded scalar context $imap->capability( 'FOO' ) # first value val_1 of FOO=val_1 FOO=val_2 ... list context $imap->capability( 'FOO' ) # list (val_1, val_2, ...) of FOO=val_1 FOO=val_2 ... I suggest to name a new method instead of this complicated behaviour of capability() maybe "capability_of()". what do you think of it? -- Au revoir, Gilles Lamiral. France, Baulon (35580) mob 06 19 22 03 54 tel 09 51 84 42 42
The issues you've noted are certainly far from desirable. I'm thinking we can still fix capability and/or has_capability to do more along the lines of what one means most likely, but I may need to spend a little time thinking about this before committing to any particular change. In particular calling has_capability("$capability=$value") should probably be deprecated and instead be more like: my @vals = $imap->has_capability($capability) inspect @vals via grep or whatever If we saw a "=" in the $capability, we could still support the older behavior though. Another option would be to support an argument to capability() to get the value(s) that the server returned. I haven't thought about this enough to have a strong opinion about what would be most desirable. If you wanted to submit some test cases showing current usage that you have that you would expect to continue to work as-is, that would be nice to add to our testing corpus. Especially if we're going to change this code.
How about the following change for starters? Does this look reasonable? diff --git a/lib/Mail/IMAPClient.pm b/lib/Mail/IMAPClient.pm index bf78f08..b58fe81 100644 --- a/lib/Mail/IMAPClient.pm +++ b/lib/Mail/IMAPClient.pm @@ -2832,12 +2832,20 @@ sub capability { $self->_imap_command('CAPABILITY') or return undef; - my @caps = map { split } grep s/^\*\s+CAPABILITY\s+//, $self->History; - foreach (@caps) { - $self->{CAPABILITY}{ uc $_ }++; - $self->{ uc $1 } = uc $2 if /(.*?)\=(.*)/; - } - + my @caps = map { split / /, uc $_ } grep s/^\*\s+CAPABILITY\s+//, $self->History; + my @addc; + $self->{CAPABILITY} ||= {}; + my $cref = $self->{CAPABILITY}; + foreach my $cap (@caps) { + $cref->{$cap} ||= []; + my @kv = split( /=/, $cap, 2 ); + if ( $#kv == 1 ) { + push( @addc, $kv[0] ) unless exists $cref->{ $kv[0] }; + $cref->{ $kv[0] } ||= []; + push ( @{ $cref->{ $kv[0] } }, $kv[1] ); + } + } + push ( @caps, @addc ); return wantarray ? @caps : \@caps; } With this change has_capability would return an array ref which could be empty or have X values depending up on the server response (still matching "true" if the server has the capability documentation - or "" otherwise. We could change that "true" value be any number of different behaviours if desired. Examples: - return a list (instead of array ref) if wantarray - return 0 but true or 0E0 in scalar context if the capability is supported but there isn't a value associated with that capability in the server response - ... Thoughts?
How about this one? I'm planning on going with this unless you speak up with obvious issues/concerns (I also have a set of tests to exercise this - need to update docs first before I'm done though)... diff --git a/lib/Mail/IMAPClient.pm b/lib/Mail/IMAPClient.pm index bf78f08..1cdfd64 100644 --- a/lib/Mail/IMAPClient.pm +++ b/lib/Mail/IMAPClient.pm @@ -2832,11 +2832,20 @@ sub capability { $self->_imap_command('CAPABILITY') or return undef; - my @caps = map { split } grep s/^\*\s+CAPABILITY\s+//, $self->History; - foreach (@caps) { - $self->{CAPABILITY}{ uc $_ }++; - $self->{ uc $1 } = uc $2 if /(.*?)\=(.*)/; + my @caps = map { split / /, uc $_ } grep s/^\*\s+CAPABILITY\s+//, $self->History; + my @addc; + + $self->{CAPABILITY} ||= {}; + foreach my $cap (@caps) { + $self->{CAPABILITY}->{$cap} ||= []; + my @kv = split( /=/, $cap, 2 ); + if ( $#kv == 1 ) { + push( @addc, $kv[0] ) unless exists $self->{CAPABILITY}->{ $kv[0] }; + $self->{CAPABILITY}->{ $kv[0] } ||= []; + push ( @{ $self->{CAPABILITY}->{ $kv[0] } }, $kv[1] ); + } } + push ( @caps, @addc ); return wantarray ? @caps : \@caps; } @@ -2846,7 +2855,17 @@ sub capability { sub has_capability { my ( $self, $which ) = @_; $self->capability or return undef; - $which ? $self->{CAPABILITY}{ uc $which } : ""; + + my $aref = $self->{CAPABILITY}{ uc $which }; + if (wantarray) { + return @{ $aref || [] }; + } + elsif ($aref) { + return @$aref ? $aref->[0] : $which; + } + else { + return ""; + } } sub imap4rev1 {
Subject: Re: [rt.cpan.org #127271] Better has_capability()
Date: Thu, 11 Oct 2018 08:20:45 +0200
To: "Phil Pearl (Lobbes) via RT" <bug-Mail-IMAPClient [...] rt.cpan.org>
From: Mark Overmeer <solutions [...] overmeer.net>
* Phil Pearl (Lobbes) via RT (bug-Mail-IMAPClient@rt.cpan.org) [181011 03:57]: Show quoted text
> Queue: Mail-IMAPClient > Ticket <URL: https://rt.cpan.org/Ticket/Display.html?id=127271 > > > How about this one? I'm planning on going with this unless you speak > up with obvious issues/concerns (I also have a set of tests to exercise > this - need to update docs first before I'm done though)...
Hum, my message of 5 oct did not reach RT or you. I'll bounce it to you now Show quoted text
> - $self->{CAPABILITY}{ uc $_ }++; > - $self->{ uc $1 } = uc $2 if /(.*?)\=(.*)/;
Show quoted text
> + if ( $#kv == 1 ) {
if(@kv==2) # showing '2' is closer to two ()-pairs Show quoted text
> + $self->{CAPABILITY}->{ $kv[0] } ||= []; > + push ( @{ $self->{CAPABILITY}->{ $kv[0] } }, $kv[1] );
Now you loose backwards compatibility with the ugly $imap->{PARAM}. That construct is documented, hence must be kept. You do not need the first of the pair by autovivification. - $self->{CAPABILITY}->{$cap} ||= []; - my @kv = split( /=/, $cap, 2 ); - if ( $#kv == 1 ) { - push( @addc, $kv[0] ) unless exists $self->{CAPABILITY}->{ $kv[0] }; - $self->{CAPABILITY}->{ $kv[0] } ||= []; - push ( @{ $self->{CAPABILITY}->{ $kv[0] } }, $kv[1] ); - } - } - push ( @caps, @addc ); + my $field = $self->{CAPABILITY}->{$cap} ||= []; + my ($name, $value) = split /\=/, $cap, 2; + push @{$field->{$name}}, $value + if defined $value; But the latter still breaks compatibility. Maybe add: $self->{CAPABILITY}->{$name} = $value; # backwards compat - push( @addc, $kv[0] ) unless exists $self->{CAPABILITY}->{ $kv[0] - push ( @caps, @addc ); - return wantarray ? @caps : \@caps; + my @names = keys %{$self->{CAPABILITY}}; + wantarray ? @names : \@names; Show quoted text
> @@ -2846,7 +2855,17 @@ sub capability {
- my $aref = $self->{CAPABILITY}{ uc $which }; - if (wantarray) { - return @{ $aref || [] }; - } - elsif ($aref) { - return @$aref ? $aref->[0] : $which; ^^^^^^^ interface change - } - else { - return ""; - } + my $aref = $self->{CAPABILITY}{ uc $which } || []; + wantarray ? @$aref : ($aref->[0] || ''); But I strongly advice a new method to return lists, for situations like this: call(count => $imap->has_capability{PARAM}); In LIST content, this may now produce an "Odd number of elements in hash assignment" -- Regards, MarkOv ------------------------------------------------------------------------ Mark Overmeer MSc MARKOV Solutions drs Mark A.C.J. Overmeer MARKOV Solutions Mark@Overmeer.net solutions@overmeer.net http://Mark.Overmeer.net http://solutions.overmeer.net
Here are the relevant docs: =head2 capability ... The B<capability> method returns an array of capabilities as returned by the CAPABILITY IMAP Client command, or a reference to an array of capabilities if called in scalar context. If the CAPABILITY IMAP Client command fails for any reason then the B<capability> method will return C<undef>. Supported capabilities are cached by the client, ... =head2 has_capability ... Returns true if the IMAP server to which the IMAPClient object is connected has the capability specified as an argument to B<has_capability>. If the server does not have the capability then the empty string "" is returned, if the underlying L</capability> calls fails then undef is returned. On Thu Oct 11 02:21:31 2018, solutions@overmeer.net wrote: Show quoted text
> * Phil Pearl (Lobbes) via RT (bug-Mail-IMAPClient@rt.cpan.org) [181011 > 03:57]:
> > Queue: Mail-IMAPClient > > Ticket <URL: https://rt.cpan.org/Ticket/Display.html?id=127271 >
[snip] Show quoted text
> Hum, my message of 5 oct did not reach RT or you. I'll bounce it > to you now
[snip] Show quoted text
> > + if ( $#kv == 1 ) {
> > if(@kv==2) # showing '2' is closer to two ()-pairs
agreed Show quoted text
> > + $self->{CAPABILITY}->{ $kv[0] } ||= []; > > + push ( @{ $self->{CAPABILITY}->{ $kv[0] } }, $kv[1] );
> > Now you loose backwards compatibility with the ugly $imap->{PARAM}. > That construct is documented, hence must be kept.
I don't believe that is documented or supported, but maybe I missed something? Show quoted text
> You do not need the first of the pair by autovivification.
fixed Show quoted text
> + my $field = $self->{CAPABILITY}->{$cap} ||= []; > + my ($name, $value) = split /\=/, $cap, 2; > + push @{$field->{$name}}, $value > + if defined $value; > > But the latter still breaks compatibility. Maybe add: > > $self->{CAPABILITY}->{$name} = $value; # backwards compat
I defer to my first comment that I believe this is not documented, and not supported. This is a place that no one should be poking around in. Show quoted text
> > - push( @addc, $kv[0] ) unless exists $self->{CAPABILITY}->{ $kv[0] > - push ( @caps, @addc ); > - return wantarray ? @caps : \@caps; > > + my @names = keys %{$self->{CAPABILITY}}; > + wantarray ? @names : \@names; >
> > @@ -2846,7 +2855,17 @@ sub capability {
> - my $aref = $self->{CAPABILITY}{ uc $which }; > - if (wantarray) { > - return @{ $aref || [] }; > - } > - elsif ($aref) { > - return @$aref ? $aref->[0] : $which; > ^^^^^^^ interface change
The interface is documented to return true, and any non-zero/empty value is true in perl so I feel this keeps with the documentation and the spirit of original intention. Show quoted text
> But I strongly advice a new method to return lists, for situations > like this: > > call(count => $imap->has_capability{PARAM}); > > In LIST content, this may now produce an > "Odd number of elements in hash assignment"
Agreed, that could be problematic, even if likely rare for today's usage. If we really are concerned about this, then I suppose we might do this: - capability stays as is - return undef/array/arrayref depending upon context - has_capability stays as is - return undef/true(capability name)/false("") - get_capability - similar to has_capability but returns undef or an array ref always? - gathers: 1. an array of values (val1, val2, ...) 2. single element array (capability) when no values are present - returns: a. undef - underlying error b. array ref regardless of list/scalar context Is this preferred? This could be a simple wrapper around has_capabililty as proposed above. Or what if we changed this return in has_capability to always return an array reference? + elsif ($aref) { + return @$aref ? $aref->[0] : $which; + } becomes: + return @$aref ? $aref : [$which]; Then document/suggest the caller use 'scalar' before any arguments in a list to ensure proper context is used to get the desired results. The result evaluates to true still, but now the caller needs to deal with an array ref instead of a string. The docs would need to be updated to note that "true" is now perhaps more useful than before, but is a reference and not just a string which may not have provided the full information desired in the past. Given the docs only stated returning "true", we may have leeway, but do we want true to be more useful/correct or should true really be just a 1 (one) and give other actual info in a separate/new call? I'm not sold on a specific approach yet.
Subject: Re: [rt.cpan.org #127271] Better has_capability()
Date: Thu, 18 Oct 2018 01:19:06 +0200
To: bug-Mail-IMAPClient [...] rt.cpan.org
From: Gilles LAMIRAL <gilles [...] lamiral.info>
Hi Phil and Mark, First of all, I prefer keeping back compatibility over everything else, especially the documented API, which must remains the same for the same basic calls. I suggest to write the new documentation API before coding it. I have to deduce the API reading the code and it's very error-prone for me, I'm not good at Perl. Here is a documentation suggestion. Keep the no param call exactly the same: my $features = $imap->capability or die "Could not determine capability: ", $imap->LastError; Questions: 1) Do we provide $imap->capability( 'KEYWORD' ) 2) Do we honour this MUST: "Client implementations MUST ignore any unknown capability names." https://www.iana.org/assignments/imap-capabilities/imap-capabilities.xhtml I prefer not, like it is now. The client is the user of Mail::IMAPCLient, not Mail::IMAPCLient itself, let him free to break the IANA directive. 3) Let's imagine this CAPABILITY CAPABILITY IMAP4rev1 APPENDLIMIT=1234 AUTH=PLAIN AUTH=LOGIN AUTH=CRAM-MD5 UIDPLUS What returns each of those calls: $imap->has_capability( 'IMAP4rev1' ) $imap->has_capability( 'APPENDLIMIT' ) $imap->has_capability( 'APPENDLIMIT=' ) $imap->has_capability( 'APPENDLIMIT=1234' ) $imap->has_capability( 'APPENDLIMIT=9999' ) $imap->has_capability( 'AUTH' ) $imap->has_capability( 'AUTH=' ) $imap->has_capability( 'AUTH=LOGIN' ) $imap->has_capability( 'AUTH=NOEXISTS' ) -- Au revoir, Gilles Lamiral. France, Baulon (35580) mob 06 19 22 03 54 tel 09 51 84 42 42
Please look at changes here: https://github.com/plobbes/mail-imapclient/commit/742ca2edc9ec0991ebf6c4cbce817fac8ade1c7e And test the latest code which is version 3.41_03. I haven't done a beta release to CPAN as I'm hoping pulling code from GitHub is easy enough. I tried to incorporate ideas from both of you (Gilles and Mark). It may not be "perfect" but I believe it retains compatibility with the older versions, and builds ont that by now providing visibility into the capability data returned from the server cleaner handling of multivalued capabilities. Your feedback is welcome. I'd like to make this an official release soon-ish if there aren't major objections or issues found.
Subject: Re: [rt.cpan.org #127271] Better has_capability()
Date: Mon, 7 Jan 2019 12:17:19 +0100
To: bug-Mail-IMAPClient [...] rt.cpan.org
From: Gilles LAMIRAL <gilles [...] lamiral.info>
Hi Phil, It sounds great! Do the following +lines are also true: * CAPABILITY IMAP4rev1 SORT SORT=DISPLAY I18NLEVEL=1 AUTH=PLAIN AUTH=XTEST Results are returned as shown by comments: $cap = $imap->has_capability("IMAP4rev1") # ret: [ "IMAP4rev1" ] @cap = $imap->has_capability("IMAP4rev1") # ret: ( "IMAP4rev1" ) $cap = $imap->has_capability("SORT") # ret: [ "DISPLAY" ] @cap = $imap->has_capability("SORT=DISPLAY") # ret: ( "SORT=DISPLAY" ) $cap = $imap->has_capability("AUTH") # ret: [ "PLAIN", "XTEST" ] + @cap = $imap->has_capability("AUTH") # ret: ( "PLAIN", "XTEST" ) + $cap = $imap->has_capability("AUTH=XTEST") # ret: [ "AUTH=XTEST" ] @cap = $imap->has_capability("AUTH=XTEST") # ret: ( "AUTH=XTEST" ) $cap = $imap->has_capability("AUTH=NADA") # ret: '' @cap = $imap->has_capability("AUTH=NADA") # ret: () + $cap = $imap->has_capability("NADA") # ret: '' + @cap = $imap->has_capability("NADA") # ret: () Thanks! -- Au revoir, Gilles Lamiral. France, Baulon (35580) mob 06 19 22 03 54 tel 09 51 84 42 42
Made updates similar to what was requested in this commit: commit 6ef6ea4f1793bce7f93d4d64bce8ad0a6f508c1b Author: Phil Pearl (Lobbes) <plobbes@gmail.com> Date: Sun Jan 13 00:46:07 2019 -0500 - minor has_capability POD cleanup On Mon Jan 07 06:17:31 2019, gilles@lamiral.info wrote: Show quoted text
> Hi Phil, > > It sounds great! > > Do the following +lines are also true: > > * CAPABILITY IMAP4rev1 SORT SORT=DISPLAY I18NLEVEL=1 AUTH=PLAIN AUTH=XTEST > Results are returned as shown by comments: > $cap = $imap->has_capability("IMAP4rev1") # ret: [ "IMAP4rev1" ] > @cap = $imap->has_capability("IMAP4rev1") # ret: ( "IMAP4rev1" ) > $cap = $imap->has_capability("SORT") # ret: [ "DISPLAY" ] > @cap = $imap->has_capability("SORT=DISPLAY") # ret: ( "SORT=DISPLAY" ) > $cap = $imap->has_capability("AUTH") # ret: [ "PLAIN", "XTEST" ] > + @cap = $imap->has_capability("AUTH") # ret: ( "PLAIN", "XTEST" ) > + $cap = $imap->has_capability("AUTH=XTEST") # ret: [ "AUTH=XTEST" ] > @cap = $imap->has_capability("AUTH=XTEST") # ret: ( "AUTH=XTEST" ) > $cap = $imap->has_capability("AUTH=NADA") # ret: '' > @cap = $imap->has_capability("AUTH=NADA") # ret: () > + $cap = $imap->has_capability("NADA") # ret: '' > + @cap = $imap->has_capability("NADA") # ret: () > > > Thanks! > >
The fix for this issue is in version 3.41 which was released several hours ago.