001package 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. 002 003import java.util.*; 004import javax.mail.internet.*; 005import votorola.g.*; 006import votorola.g.lang.*; 007import votorola.g.mail.*; 008 009 010/** A user identifier in two forms: email address and mailish username. Both are globally 011 * unique and map 1:1. 012 */ 013 @SuppressWarnings("overrides")/* overrides equals, but not hashCode*/ 014public @ThreadSafe class IDPair implements java.io.Serializable 015{ 016 017 private static final long serialVersionUID = 1L; 018 019 020 021 /** Constructs an IDPair. The username will automatically be translated to normal 022 * form. 023 * 024 * @see #email() 025 * @see #username() 026 * @see #isFromEmail() 027 */ 028 public IDPair( String _email, String _username, boolean _isFromEmail ) 029 { 030 email = _email; 031 username = normalUsername( _username ); 032 isFromEmail = _isFromEmail; 033 } 034 035 036 037 /** Constructs an IDPair by copying from another. 038 */ 039 protected IDPair( final IDPair other ) 040 { 041 email = other.email; 042 username = other.username; 043 isFromEmail = other.isFromEmail; 044 } 045 046 047 048 /** Constructs an IDPair from a canonical email address. 049 * 050 * @see #email() 051 * 052 * @return a newly constructed IDPair, or NOBODY if the email address is null. 053 * 054 * @see votorola.g.mail.InternetAddressX#canonicalAddress(String) 055 */ 056 public static IDPair fromEmail( final String email ) 057 { 058 return email == null? NOBODY: 059 new IDPair( email, toUsername(email), /*isFromEmail*/true ); 060 } 061 062 063 064 /** Constructs an IDPair from a username, which will automatically be translated to 065 * normal form. 066 * 067 * @see #username() 068 * 069 * @return a newly constructed IDPair, or NOBODY if the username is null. 070 */ 071 public static IDPair fromUsername( final String username ) throws AddressException 072 { 073 return username == null? NOBODY: 074 new IDPair( toInternetAddress(username).getAddress(), username, /*isFromEmail*/false ); 075 } 076 077 078 079 // -------------------------------------------------------------------------------- 080 081 082 /** Translates an email address to a mailish username, and appends the result to the 083 * specified string builder.<pre> 084 * 085 * Email address Mailish username 086 * ----------------------- ------------------------------ 087 * Smith@acme.com Smith AcmeCom 088 * smith@acme.com Smith-AcmeCom 089 * ThomasvonderElbe@gmx.de ThomasvonderElbe GmxDe 090 * mike@zelea.com Mike-ZeleaCom</pre> 091 * 092 * <p>Note that MediaWiki ignores the case of the first letter of the username, 093 * forcing all names to begin with an uppercase letter, wheras the local part of an 094 * email address (before the @) is case sensitive. We therefore encode the fact of 095 * lowercase by mapping the '@' symbol to a dash '-' surrogate, and of uppercase by 096 * mapping it to a space ' ' surrogate. We might have tried using $wgCapitalLinks in 097 * MediaWiki, but, despite that, calls to ucfirst() in includes/User.php methods 098 * isValidUserName() and getCanonicalName() (and maybe elsewhere) block the creation 099 * of any username having a lowercase first character.</p> 100 * 101 * <p>This translation should work with all email addresses in common use today. 102 * Here are the strict requirements:</p><pre> 103 * 104 * a) No '/' in address 105 * - invalid in MediaWiki username 106 * b) No ' ' in address 107 * - MediaWiki cannot distinguish '_' and ' ' in page names 108 * c) Only ASCII characters in domain name 109 * - only ASCII domain names are considered case insensitive 110 * and we use lower-to-uppercase boundaries in place of '.' 111 * to separate the component labels of the name 112 * d) No '-' or digit (or anything else non-lowercase) at start of a domain label 113 * - we encode label with leading uppercase 114 * e) No '-' at end of a domain label 115 * - next label is encoded with leading uppercase, so we have internal 116 * sequence '-U', where U is an uppercase letter; but we use '-U' 117 * to encode '@' (though only for email addresses that begin with 118 * lowercase) 119 * f) No '_' in domain name 120 * - we use it (or equivalently ' ') in place of '@' (though only for email 121 * addresses that begin with uppercase)</pre> 122 * 123 * <pre>References: 124 * - http://tools.ietf.org/html/rfc1035 125 * - http://tools.ietf.org/html/rfc2181#section-11 126 * - no chars *abolutely* restricted in components (labels) of a domain name 127 * - http://tools.ietf.org/html/rfc1035#section-2.3.1 128 * - but mail hosts *ought* to follow the preferred name syntax 129 * - http://tools.ietf.org/html/rfc4343#section-3.2 130 * - case-insensitivity applies only to ASCII labels</pre> 131 * 132 * @param email a canonical email address. 133 * 134 * @return the same string builder b. 135 * 136 * @see #toUsername(String) 137 * @see votorola.g.mail.InternetAddressX#canonicalAddress(String) 138 */ 139 public static StringBuilder appendUsername( final String email, final StringBuilder b ) 140 { 141 // cf. emailToUsername($email) http://reluk.ca/project/_/mailish/MailishUsername.php 142 // emailToUsername($) http://reluk.ca/system/host/havoc/usr/local/libexec/relay-to-minder 143 // 144 // Using plain 16 bit Java chars for case conversions. 32 bit code-points are 145 // poorly documented. What's the code-point for '@'? Is it just (int)'@'? No 146 // worry, because email addresses are unlikely to use the extended characters. 147 148 final int cN = email.length(); 149 150 // First character 151 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 152 int c = 0; 153 final char atSurrogateChar; 154 { 155 final char ch = email.charAt( c++ ); 156 final char chUp = Character.toUpperCase( ch ); 157 b.append( chUp ); 158 159 if( ch == chUp ) atSurrogateChar = ' '; 160 else atSurrogateChar = '-'; 161 } 162 163 // Remainder of local part and at-surrogate 164 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 165 for( ;; ) 166 { 167 char ch = email.charAt( c++ ); 168 if( ch == '@' ) break; 169 else if( ch == '_' ) ch = ' '; // normalize 170 171 b.append( ch ); 172 } 173 174 b.append( atSurrogateChar ); 175 176 // Domain name 177 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 178 b.append( Character.toUpperCase( email.charAt( c++ ))); 179 for(; c < cN; ) 180 { 181 char ch = email.charAt( c++ ); 182 if( ch == '.' ) ch = Character.toUpperCase( email.charAt( c++ )); // collapse '.' 183 184 b.append( ch ); 185 } 186 187 return b; 188 } 189 190 191 192 /** Appends a short abbreviation of the username to the string builder. 193 */ 194 public static StringBuilder buildUserMnemonic( final String username, final StringBuilder sB ) 195 { 196 // changing? change also in a/web/gwt/super/ 197 198 int n = username.length(); 199 if( username.startsWith("Test-") && username.endsWith("-ZeleaCom") && n > 14 ) 200 { 201 n -= 14; 202 sB.append( Character.toUpperCase( username.charAt( 5 ))); 203 if( n > 1 ) 204 { 205 final char ch = username.charAt( 6 ); 206 if( Character.isLetterOrDigit( ch )) sB.append( ch ); 207 } 208 else sB.append( 'z' ); // fill out single-letter names, which are common and unnatural 209 } 210 else 211 { 212 sB.append( username.charAt( 0 )); 213 if( n > 1 ) 214 { 215 final char ch = username.charAt( 1 ); 216 if( Character.isLetterOrDigit( ch )) sB.append( ch ); 217 } 218 } 219 sB.append( '.' ); 220 return sB; 221 } 222 223 224 225 /** Identifies the user by canonical email address. This is the primary form of 226 * identifier for purposes of persistent back-end storage. 227 * 228 * @see votorola.g.mail.InternetAddressX#canonicalAddress(String) 229 */ 230 public final String email() { return email; } 231 232 233 private final String email; 234 235 236 237 /** Returns the same email address; or, if it is null or {@linkplain #NOBODY NOBODY}, 238 * the localized string for "nobody". 239 * 240 * @param resA a resource bundle of type application (A) 241 */ 242 public static String emailOrNobody( final String email, final ResourceBundle resA ) 243 { 244 return email == null || email.equals(NOBODY.email())? 245 resA.getString("a.count.nobodyEmailPlaceholder"): email; 246 } 247 248 249 250 /** Returns true iff o is an ID pair with the same email address. This method is 251 * provided for backward compatibility and will soon be deprecated; use plain 252 * equals() instead. 253 * 254 * @see #equals(Object) 255 */ 256 public final boolean equalsEmail( Object o ) { return equals( o ); } 257 // { 258 // if( !( o instanceof IDPair )) return false; 259 // 260 // final IDPair other = (IDPair)o; 261 // return email.equals( other.email ); 262 // } 263 264 265 266 /** Whether this ID pair was created from an email address as opposed to a username. 267 * 268 * @deprecated as an unecessary complexity and source of potential bugs. 269 */ 270 public @Deprecated final boolean isFromEmail() { return isFromEmail; } 271 272 273 private final boolean isFromEmail; 274 275 276 277 /** An ID pair for a non-existent user. 278 */ 279 public static IDPair NOBODY = IDPair.fromEmail( "nobody@zelea.com" ); 280 281 282 283 /** Translates the mailish username to normal form by shifting the first letter to 284 * uppercase and substituting spaces for underscores. The normal name is globally 285 * unique and maps 1:1 with the canonical email address. Examples are 286 * "Smith-AcmeCom" for smith@acme.com, or "Smith AcmeCom" for Smith@acme.com. 287 * 288 * @return the translated name, which may be the same name; or null if the name 289 * is null. 290 * 291 * @see #username() 292 * @see MediaWiki#normalUsername(String) 293 */ 294 public static String normalUsername( String name ) { return MediaWiki.normalUsername( name ); } 295 // changing? change also in a/web/gwt/super/ 296 297 298 299 /** Translates a username to a canonical email address. 300 * 301 * @param username the username encoded with either a space (or equivalently 302 * underscore) or dash at-surrogate. 303 * 304 * @see #username() 305 * @see votorola.g.mail.InternetAddressX#canonicalAddress(String) 306 */ 307 public static InternetAddress toInternetAddress( final String username ) throws AddressException 308 { 309 // cf. usernameToEmail($) http://reluk.ca/system/host/havoc/usr/local/libexec/relay-to-minder 310 final int cN = username.length(); 311 final int cAtSurrogate; 312 { 313 final int cDash; 314 { 315 int c = cN - 1; 316 boolean isLastUppercase = false; 317 for(; c >= 0; c-- ) 318 { 319 final char ch = username.charAt( c ); 320 if( ch == '-' && isLastUppercase ) break; 321 // dash may occur in domain-name part, but never followed by uppercase 322 323 isLastUppercase = Character.isUpperCase( ch ); 324 } 325 cDash = c; 326 } 327 int cSpace = username.lastIndexOf( ' ' ); 328 if( cSpace == -1 ) cSpace = username.lastIndexOf( '_' ); // then at-surrogate must be '_' 329 cAtSurrogate = Math.max( cDash, cSpace ); // whichever is closest to the end 330 if( cAtSurrogate < 0 ) 331 { 332 throw new AddressException( 333 "username has neither space nor underscore, nor a dash followed by an uppercase letter" ); 334 } 335 } 336 337 final StringBuilder b = new StringBuilder( cN + 1/*dot*/ + 3/*extra*/ ); 338 339 // First character 340 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 341 int c = 0; 342 { 343 char ch = username.charAt( c++ ); 344 final char atSurrogate = username.charAt( cAtSurrogate ); 345 if( atSurrogate == '-' ) ch = Character.toLowerCase( ch ); 346 b.append( ch ); 347 } 348 349 // Remainder of local part 350 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 351 for(; c < cAtSurrogate; ++c ) 352 { 353 char ch = username.charAt( c ); 354 if( ch == ' ' ) ch = '_'; // equivalent because we disallow space in address 355 b.append( ch ); 356 } 357 358 // @ 359 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 360 b.append( '@' ); c++; 361 362 // Domain name 363 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 364 b.append( Character.toLowerCase( username.charAt( c++ ))); 365 for(; c < cN; ) 366 { 367 final char ch = username.charAt( c++ ); 368 final char chLow = Character.toLowerCase( ch ); 369 if( ch != chLow ) b.append( '.' ); // uncollapse '.' 370 b.append( chLow ); 371 } 372 final InternetAddress iAddress = new InternetAddress( b.toString(), /*strict*/true ); 373 if( !InternetAddressX.VALID_PATTERN_BARE_DOMNAME.matcher( // stricter checks 374 iAddress.getAddress() ).matches() ) 375 { 376 throw new AddressException( "translation yields malformed email address \"" 377 + iAddress.getAddress() + "\"" ); 378 } 379 380 return iAddress; 381 } 382 383 384 385 /** Translates a canonical email address to a mailish username in normal form. 386 * 387 * @param email the canonical email address of the user. 388 * 389 * @see IDPair#username() 390 * @see #appendUsername(String,StringBuilder) 391 * @see votorola.g.mail.InternetAddressX#canonicalAddress(String) 392 */ 393 public static String toUsername( final String email ) 394 { 395 final StringBuilder b = new StringBuilder( email.length() ); 396 appendUsername( email, b ); 397 return b.toString(); 398 } 399 400 401 402 /** Identifies the user by mailish username. This is a front-end identifier for use 403 * in the pollwiki and other UIs where it offers a degree of protection from the 404 * address harvesting practices of spammers. Another advantage is its lack of 405 * punctuation characters ('@' and '.') which makes the UI a little easier to read. 406 * 407 * @see #normalUsername(String) 408 * @see #toUsername(String) 409 * @see <a href='http://reluk.ca/project/_/mailish/MailishUsername.xht'>MailishUsername extension for MediaWiki</a> 410 */ 411 public final String username() { return username; } 412 413 414 private final String username; 415 416 417 418 // - O b j e c t ------------------------------------------------------------------ 419 420 421 // /** Returns true iff o is an ID pair constructed from the same (equals) identifier. 422 // * If two ID pairs are equal, then they identify the same user; but the reverse is 423 // * not true when the ID pairs were created from different source identifiers (email 424 // * and username). 425 // * 426 // * @see #isFromEmail() 427 // * @see #equalsEmail(Object) 428 // */ 429 // public @Override boolean equals( Object o ) 430 // { 431 // if( !( o instanceof IDPair )) return false; 432 // 433 // final IDPair other = (IDPair)o; 434 // return isFromEmail == other.isFromEmail && email.equals( other.email ); 435 // 436 // } 437 //// will cause bugs, so revert (hopefully without also causing bugs): 438 439 /** True iff o is an ID pair constructed from the same (equals) email address. 440 */ 441 public @Override final boolean equals( Object o ) 442 { 443 if( !( o instanceof IDPair )) return false; 444 445 final IDPair other = (IDPair)o; 446 return email.equals( other.email ); 447 448 } 449 450 451 452 /** Returns the same identifier from which the ID pair was constructed. 453 * 454 * @see #isFromEmail() 455 */ 456 public @Override final String toString() { return isFromEmail? email(): username(); } 457 458 459 460}