package votorola.a.voter; // Copyright 2009, 2011, 2013, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. import java.util.*; import javax.mail.internet.*; import votorola.g.*; import votorola.g.lang.*; import votorola.g.mail.*; /** A user identifier in two forms: email address and mailish username. Both are globally * unique and map 1:1. */ @SuppressWarnings("overrides")/* overrides equals, but not hashCode*/ public @ThreadSafe class IDPair implements java.io.Serializable { private static final long serialVersionUID = 1L; /** Constructs an IDPair. The username will automatically be translated to normal * form. * * @see #email() * @see #username() * @see #isFromEmail() */ public IDPair( String _email, String _username, boolean _isFromEmail ) { email = _email; username = normalUsername( _username ); isFromEmail = _isFromEmail; } /** Constructs an IDPair by copying from another. */ protected IDPair( final IDPair other ) { email = other.email; username = other.username; isFromEmail = other.isFromEmail; } /** Constructs an IDPair from a canonical email address. * * @see #email() * * @return a newly constructed IDPair, or NOBODY if the email address is null. * * @see votorola.g.mail.InternetAddressX#canonicalAddress(String) */ public static IDPair fromEmail( final String email ) { return email == null? NOBODY: new IDPair( email, toUsername(email), /*isFromEmail*/true ); } /** Constructs an IDPair from a username, which will automatically be translated to * normal form. * * @see #username() * * @return a newly constructed IDPair, or NOBODY if the username is null. */ public static IDPair fromUsername( final String username ) throws AddressException { return username == null? NOBODY: new IDPair( toInternetAddress(username).getAddress(), username, /*isFromEmail*/false ); } // -------------------------------------------------------------------------------- /** Translates an email address to a mailish username, and appends the result to the * specified string builder.
      *
      *             Email address  Mailish username
      *   -----------------------  ------------------------------
      *            Smith@acme.com  Smith AcmeCom
      *            smith@acme.com  Smith-AcmeCom
      *   ThomasvonderElbe@gmx.de  ThomasvonderElbe GmxDe
      *            mike@zelea.com  Mike-ZeleaCom
* *

Note that MediaWiki ignores the case of the first letter of the username, * forcing all names to begin with an uppercase letter, wheras the local part of an * email address (before the @) is case sensitive. We therefore encode the fact of * lowercase by mapping the '@' symbol to a dash '-' surrogate, and of uppercase by * mapping it to a space ' ' surrogate. We might have tried using $wgCapitalLinks in * MediaWiki, but, despite that, calls to ucfirst() in includes/User.php methods * isValidUserName() and getCanonicalName() (and maybe elsewhere) block the creation * of any username having a lowercase first character.

* *

This translation should work with all email addresses in common use today. * Here are the strict requirements:

      *
      *  a) No '/' in address
      *      - invalid in MediaWiki username
      *  b) No ' ' in address
      *      - MediaWiki cannot distinguish '_' and ' ' in page names
      *  c) Only ASCII characters in domain name
      *      - only ASCII domain names are considered case insensitive
      *        and we use lower-to-uppercase boundaries in place of '.'
      *        to separate the component labels of the name
      *  d) No '-' or digit (or anything else non-lowercase) at start of a domain label
      *      - we encode label with leading uppercase
      *  e) No '-' at end of a domain label
      *      - next label is encoded with leading uppercase, so we have internal
      *        sequence '-U', where U is an uppercase letter; but we use '-U'
      *        to encode '@' (though only for email addresses that begin with
      *        lowercase)
      *  f) No '_' in domain name
      *      - we use it (or equivalently ' ') in place of '@' (though only for email
      *        addresses that begin with uppercase)
* *
References:
      *      - http://tools.ietf.org/html/rfc1035
      *      - http://tools.ietf.org/html/rfc2181#section-11
      *          - no chars *abolutely* restricted in components (labels) of a domain name
      *      - http://tools.ietf.org/html/rfc1035#section-2.3.1
      *          - but mail hosts *ought* to follow the preferred name syntax
      *      - http://tools.ietf.org/html/rfc4343#section-3.2
      *          - case-insensitivity applies only to ASCII labels
* * @param email a canonical email address. * * @return the same string builder b. * * @see #toUsername(String) * @see votorola.g.mail.InternetAddressX#canonicalAddress(String) */ public static StringBuilder appendUsername( final String email, final StringBuilder b ) { // cf. emailToUsername($email) http://reluk.ca/project/_/mailish/MailishUsername.php // emailToUsername($) http://reluk.ca/system/host/havoc/usr/local/libexec/relay-to-minder // // Using plain 16 bit Java chars for case conversions. 32 bit code-points are // poorly documented. What's the code-point for '@'? Is it just (int)'@'? No // worry, because email addresses are unlikely to use the extended characters. final int cN = email.length(); // First character // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - int c = 0; final char atSurrogateChar; { final char ch = email.charAt( c++ ); final char chUp = Character.toUpperCase( ch ); b.append( chUp ); if( ch == chUp ) atSurrogateChar = ' '; else atSurrogateChar = '-'; } // Remainder of local part and at-surrogate // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - for( ;; ) { char ch = email.charAt( c++ ); if( ch == '@' ) break; else if( ch == '_' ) ch = ' '; // normalize b.append( ch ); } b.append( atSurrogateChar ); // Domain name // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - b.append( Character.toUpperCase( email.charAt( c++ ))); for(; c < cN; ) { char ch = email.charAt( c++ ); if( ch == '.' ) ch = Character.toUpperCase( email.charAt( c++ )); // collapse '.' b.append( ch ); } return b; } /** Appends a short abbreviation of the username to the string builder. */ public static StringBuilder buildUserMnemonic( final String username, final StringBuilder sB ) { // changing? change also in a/web/gwt/super/ int n = username.length(); if( username.startsWith("Test-") && username.endsWith("-ZeleaCom") && n > 14 ) { n -= 14; sB.append( Character.toUpperCase( username.charAt( 5 ))); if( n > 1 ) { final char ch = username.charAt( 6 ); if( Character.isLetterOrDigit( ch )) sB.append( ch ); } else sB.append( 'z' ); // fill out single-letter names, which are common and unnatural } else { sB.append( username.charAt( 0 )); if( n > 1 ) { final char ch = username.charAt( 1 ); if( Character.isLetterOrDigit( ch )) sB.append( ch ); } } sB.append( '.' ); return sB; } /** Identifies the user by canonical email address. This is the primary form of * identifier for purposes of persistent back-end storage. * * @see votorola.g.mail.InternetAddressX#canonicalAddress(String) */ public final String email() { return email; } private final String email; /** Returns the same email address; or, if it is null or {@linkplain #NOBODY NOBODY}, * the localized string for "nobody". * * @param resA a resource bundle of type application (A) */ public static String emailOrNobody( final String email, final ResourceBundle resA ) { return email == null || email.equals(NOBODY.email())? resA.getString("a.count.nobodyEmailPlaceholder"): email; } /** Returns true iff o is an ID pair with the same email address. This method is * provided for backward compatibility and will soon be deprecated; use plain * equals() instead. * * @see #equals(Object) */ public final boolean equalsEmail( Object o ) { return equals( o ); } // { // if( !( o instanceof IDPair )) return false; // // final IDPair other = (IDPair)o; // return email.equals( other.email ); // } /** Whether this ID pair was created from an email address as opposed to a username. * * @deprecated as an unecessary complexity and source of potential bugs. */ public @Deprecated final boolean isFromEmail() { return isFromEmail; } private final boolean isFromEmail; /** An ID pair for a non-existent user. */ public static IDPair NOBODY = IDPair.fromEmail( "nobody@zelea.com" ); /** Translates the mailish username to normal form by shifting the first letter to * uppercase and substituting spaces for underscores. The normal name is globally * unique and maps 1:1 with the canonical email address. Examples are * "Smith-AcmeCom" for smith@acme.com, or "Smith AcmeCom" for Smith@acme.com. * * @return the translated name, which may be the same name; or null if the name * is null. * * @see #username() * @see MediaWiki#normalUsername(String) */ public static String normalUsername( String name ) { return MediaWiki.normalUsername( name ); } // changing? change also in a/web/gwt/super/ /** Translates a username to a canonical email address. * * @param username the username encoded with either a space (or equivalently * underscore) or dash at-surrogate. * * @see #username() * @see votorola.g.mail.InternetAddressX#canonicalAddress(String) */ public static InternetAddress toInternetAddress( final String username ) throws AddressException { // cf. usernameToEmail($) http://reluk.ca/system/host/havoc/usr/local/libexec/relay-to-minder final int cN = username.length(); final int cAtSurrogate; { final int cDash; { int c = cN - 1; boolean isLastUppercase = false; for(; c >= 0; c-- ) { final char ch = username.charAt( c ); if( ch == '-' && isLastUppercase ) break; // dash may occur in domain-name part, but never followed by uppercase isLastUppercase = Character.isUpperCase( ch ); } cDash = c; } int cSpace = username.lastIndexOf( ' ' ); if( cSpace == -1 ) cSpace = username.lastIndexOf( '_' ); // then at-surrogate must be '_' cAtSurrogate = Math.max( cDash, cSpace ); // whichever is closest to the end if( cAtSurrogate < 0 ) { throw new AddressException( "username has neither space nor underscore, nor a dash followed by an uppercase letter" ); } } final StringBuilder b = new StringBuilder( cN + 1/*dot*/ + 3/*extra*/ ); // First character // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - int c = 0; { char ch = username.charAt( c++ ); final char atSurrogate = username.charAt( cAtSurrogate ); if( atSurrogate == '-' ) ch = Character.toLowerCase( ch ); b.append( ch ); } // Remainder of local part // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - for(; c < cAtSurrogate; ++c ) { char ch = username.charAt( c ); if( ch == ' ' ) ch = '_'; // equivalent because we disallow space in address b.append( ch ); } // @ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - b.append( '@' ); c++; // Domain name // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - b.append( Character.toLowerCase( username.charAt( c++ ))); for(; c < cN; ) { final char ch = username.charAt( c++ ); final char chLow = Character.toLowerCase( ch ); if( ch != chLow ) b.append( '.' ); // uncollapse '.' b.append( chLow ); } final InternetAddress iAddress = new InternetAddress( b.toString(), /*strict*/true ); if( !InternetAddressX.VALID_PATTERN_BARE_DOMNAME.matcher( // stricter checks iAddress.getAddress() ).matches() ) { throw new AddressException( "translation yields malformed email address \"" + iAddress.getAddress() + "\"" ); } return iAddress; } /** Translates a canonical email address to a mailish username in normal form. * * @param email the canonical email address of the user. * * @see IDPair#username() * @see #appendUsername(String,StringBuilder) * @see votorola.g.mail.InternetAddressX#canonicalAddress(String) */ public static String toUsername( final String email ) { final StringBuilder b = new StringBuilder( email.length() ); appendUsername( email, b ); return b.toString(); } /** Identifies the user by mailish username. This is a front-end identifier for use * in the pollwiki and other UIs where it offers a degree of protection from the * address harvesting practices of spammers. Another advantage is its lack of * punctuation characters ('@' and '.') which makes the UI a little easier to read. * * @see #normalUsername(String) * @see #toUsername(String) * @see MailishUsername extension for MediaWiki */ public final String username() { return username; } private final String username; // - O b j e c t ------------------------------------------------------------------ // /** Returns true iff o is an ID pair constructed from the same (equals) identifier. // * If two ID pairs are equal, then they identify the same user; but the reverse is // * not true when the ID pairs were created from different source identifiers (email // * and username). // * // * @see #isFromEmail() // * @see #equalsEmail(Object) // */ // public @Override boolean equals( Object o ) // { // if( !( o instanceof IDPair )) return false; // // final IDPair other = (IDPair)o; // return isFromEmail == other.isFromEmail && email.equals( other.email ); // // } //// will cause bugs, so revert (hopefully without also causing bugs): /** True iff o is an ID pair constructed from the same (equals) email address. */ public @Override final boolean equals( Object o ) { if( !( o instanceof IDPair )) return false; final IDPair other = (IDPair)o; return email.equals( other.email ); } /** Returns the same identifier from which the ID pair was constructed. * * @see #isFromEmail() */ public @Override final String toString() { return isFromEmail? email(): username(); } }