/android/import-contacts

To get this branch, use:
bzr branch http://bzr.ed.am/android/import-contacts
6 by edam
- added GPL header comments to all files
1
/*
2
 * Importer.java
3
 *
4
 * Copyright (C) 2009 Tim Marston <edam@waxworlds.org>
5
 *
6
 * This file is part of the Import Contacts program (hereafter referred
7
 * to as "this program"). For more information, see
8
 * http://www.waxworlds.org/edam/software/android/import-contacts
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 */
23
1 by edam
Initial import
24
package org.waxworlds.importcontacts;
25
26
import java.util.HashMap;
27
import java.util.HashSet;
28
import java.util.Iterator;
29
import java.util.Set;
30
import java.util.regex.Matcher;
31
import java.util.regex.Pattern;
32
33
import android.content.ContentUris;
34
import android.content.ContentValues;
35
import android.content.SharedPreferences;
36
import android.database.Cursor;
37
import android.net.Uri;
38
import android.os.Message;
39
import android.provider.Contacts;
40
41
public class Importer extends Thread
42
{
3 by edam
- added "all done" message
43
	public final static int ACTION_GOBACK = 0;
44
	public final static int ACTION_ABORT = 1;
45
	public final static int ACTION_ALLDONE = 2;
1 by edam
Initial import
46
47
	public final static int RESPONSE_NEGATIVE = 0;
48
	public final static int RESPONSE_POSITIVE = 1;
49
50
	public final static int RESPONSEEXTRA_NONE = 0;
51
	public final static int RESPONSEEXTRA_ALWAYS = 1;
52
53
	private Doit _doit;
54
	private int _response;
55
	private int _responseExtra;
56
	private HashMap< String, Long > _contacts;
57
	private HashMap< Long, HashSet< String > > _contactNumbers;
58
	private HashMap< Long, HashSet< String > > _contactEmails;
59
	private int _mergeSetting;
60
	private int _lastMergeDecision;
61
	private boolean _abort = false;
3 by edam
- added "all done" message
62
	private boolean _isFinished = false;
1 by edam
Initial import
63
64
	public class ContactData
65
	{
66
		class PhoneData
67
		{
68
			public String _number;
69
			public int _type;
70
			public boolean _isPreferred;
71
72
			public PhoneData( String number, int type, boolean isPreferred ) {
73
				_number = number;
74
				_type = type;
75
				_isPreferred = isPreferred;
76
			}
77
78
			public String getNumber() {
79
				return _number;
80
			}
81
82
			public int getType() {
83
				return _type;
84
			}
85
86
			public boolean isPreferred() {
87
				return _isPreferred;
88
			}
89
		}
90
91
		class EmailData
92
		{
93
			private String _email;
94
			public int _type;
95
			private boolean _isPreferred;
96
97
			public EmailData( String email, int type, boolean isPreferred ) {
98
				_email = email;
99
				_type = type;
100
				_isPreferred = isPreferred;
101
			}
102
103
			public String getAddress() {
104
				return _email;
105
			}
106
107
			public int getType() {
108
				return _type;
109
			}
110
111
			public boolean isPreferred() {
112
				return _isPreferred;
113
			}
114
		}
115
116
		public String _name = null;
117
		public HashMap< String, PhoneData > _phones = null;
118
		public HashMap< String, EmailData > _emails = null;
119
120
		protected void setName( String name )
121
		{
122
			_name = name;
123
		}
124
125
		public String getName()
126
		{
127
			return _name;
128
		}
129
130
		protected void addPhone( String number, int type, boolean isPreferred )
131
		{
132
			if( _phones == null ) _phones = new HashMap< String, PhoneData >();
133
			if( !_phones.containsKey( number ) )
134
				_phones.put( number,
135
						new PhoneData( number, type, isPreferred ) );
136
		}
137
138
		protected void addEmail( String email, int type, boolean isPreferred )
139
		{
140
			if( _emails == null ) _emails = new HashMap< String, EmailData >();
141
			if( !_emails.containsKey( email ) )
142
				_emails.put( email, new EmailData( email, type, isPreferred ) );
143
		}
144
	}
145
146
	protected class AbortImportException extends Exception { };
147
148
	public Importer( Doit doit )
149
	{
150
		_doit = doit;
151
152
		SharedPreferences prefs = getSharedPreferences();
153
		_mergeSetting = prefs.getInt( "merge_setting", 0 );
154
	}
155
156
	@Override
157
	public void run()
158
	{
159
		try
160
		{
161
			// cache current contact names
162
			buildContactsCache();
163
164
			// do the import
165
			onImport();
166
167
			// done!
3 by edam
- added "all done" message
168
			finish( ACTION_ALLDONE );
1 by edam
Initial import
169
		}
170
		catch( AbortImportException e )
171
		{}
3 by edam
- added "all done" message
172
173
		// flag as finished to prevent interrupts
174
		setIsFinished();
175
	}
176
177
	synchronized private void setIsFinished()
178
	{
179
		_isFinished = true;
1 by edam
Initial import
180
	}
181
182
	protected void onImport() throws AbortImportException
183
	{
184
	}
185
186
	public void wake()
187
	{
188
		wake( 0, RESPONSEEXTRA_NONE );
189
	}
190
191
	public void wake( int response )
192
	{
193
		wake( response, RESPONSEEXTRA_NONE );
194
	}
195
196
	synchronized public void wake( int response, int responseExtra )
197
	{
198
		_response = response;
199
		_responseExtra = responseExtra;
200
		notify();
201
	}
202
3 by edam
- added "all done" message
203
	synchronized public boolean setAbort()
1 by edam
Initial import
204
	{
3 by edam
- added "all done" message
205
		if( !_isFinished && !_abort ) {
206
			_abort = true;
207
			notify();
208
			return true;
209
		}
210
		return false;
1 by edam
Initial import
211
	}
212
213
	protected SharedPreferences getSharedPreferences()
214
	{
215
		return _doit.getSharedPreferences();
216
	}
217
218
	protected void showError( int res ) throws AbortImportException
219
	{
220
		showError( _doit.getText( res ).toString() );
221
	}
222
223
	synchronized protected void showError( String message )
224
			throws AbortImportException
225
	{
226
		checkAbort();
227
		_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
228
				_doit._handler, Doit.MESSAGE_ERROR, message ) );
1 by edam
Initial import
229
		try {
230
			wait();
231
		}
232
		catch( InterruptedException e ) { }
3 by edam
- added "all done" message
233
		// no need to check if an abortion happened during the wait, we are
234
		// about to finish anyway!
235
		finish( ACTION_ABORT );
1 by edam
Initial import
236
	}
237
238
	protected void showFatalError( int res ) throws AbortImportException
239
	{
240
		showFatalError( _doit.getText( res ).toString() );
241
	}
242
243
	synchronized protected void showFatalError( String message )
244
			throws AbortImportException
245
	{
246
		checkAbort();
247
		_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
248
				_doit._handler, Doit.MESSAGE_ERROR, message ) );
1 by edam
Initial import
249
		try {
250
			wait();
251
		}
252
		catch( InterruptedException e ) { }
3 by edam
- added "all done" message
253
		// no need to check if an abortion happened during the wait, we are
254
		// about to finish anyway!
255
		finish( ACTION_ABORT );
1 by edam
Initial import
256
	}
257
258
	protected boolean showContinue( int res ) throws AbortImportException
259
	{
260
		return showContinue( _doit.getText( res ).toString() );
261
	}
262
263
	synchronized protected boolean showContinue( String message )
264
			throws AbortImportException
265
	{
266
		checkAbort();
267
		_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
268
				_doit._handler, Doit.MESSAGE_CONTINUEORABORT, message ) );
1 by edam
Initial import
269
		try {
270
			wait();
271
		}
272
		catch( InterruptedException e ) { }
3 by edam
- added "all done" message
273
274
		// check if an abortion happened during the wait
275
		checkAbort();
276
1 by edam
Initial import
277
		return _response == RESPONSE_POSITIVE;
278
	}
279
280
	protected void setProgressMessage( int res ) throws AbortImportException
281
	{
282
		checkAbort();
283
		_doit._handler.sendMessage( Message.obtain( _doit._handler,
3 by edam
- added "all done" message
284
				Doit.MESSAGE_SETPROGRESSMESSAGE, getText( res ) ) );
1 by edam
Initial import
285
	}
286
287
	protected void setProgressMax( int maxProgress )
288
			throws AbortImportException
289
	{
290
		checkAbort();
291
		_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
292
				_doit._handler, Doit.MESSAGE_SETMAXPROGRESS,
1 by edam
Initial import
293
				new Integer( maxProgress ) ) );
294
	}
295
296
	protected void setTmpProgress( int tmpProgress ) throws AbortImportException
297
	{
298
		checkAbort();
299
		_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
300
				_doit._handler, Doit.MESSAGE_SETTMPPROGRESS,
1 by edam
Initial import
301
				new Integer( tmpProgress ) ) );
302
	}
303
304
	protected void setProgress( int progress ) throws AbortImportException
305
	{
306
		checkAbort();
307
		_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
308
				_doit._handler, Doit.MESSAGE_SETPROGRESS,
1 by edam
Initial import
309
				new Integer( progress ) ) );
310
	}
311
3 by edam
- added "all done" message
312
	protected void finish( int action ) throws AbortImportException
1 by edam
Initial import
313
	{
3 by edam
- added "all done" message
314
		// update UI to reflect action
315
		int message;
316
		switch( action )
317
		{
318
		case ACTION_GOBACK:		message = Doit.MESSAGE_FINISHED_GOBACK; break;
319
		case ACTION_ALLDONE:	message = Doit.MESSAGE_FINISHED_ALLDONE; break;
320
		default:	// fall through
321
		case ACTION_ABORT:		message = Doit.MESSAGE_FINISHED; break;
322
		}
323
		_doit._handler.sendEmptyMessage( message );
1 by edam
Initial import
324
3 by edam
- added "all done" message
325
		// stop
326
		throw new AbortImportException();
1 by edam
Initial import
327
	}
328
329
	protected CharSequence getText( int res )
330
	{
331
		return _doit.getText( res );
332
	}
333
334
	protected boolean isImportRequired( String name )
335
			throws AbortImportException
336
	{
337
		checkAbort();
338
		return isImportRequired( name, _mergeSetting );
339
	}
340
3 by edam
- added "all done" message
341
	synchronized private boolean isImportRequired( String name,
342
			int mergeSetting ) throws AbortImportException
1 by edam
Initial import
343
	{
344
		_lastMergeDecision = mergeSetting;
345
346
		// handle special cases
347
		switch( mergeSetting )
348
		{
349
		case R.id.merge_keep:
350
			// if we keep contacts on duplicate, we better check for one
351
			return !_contacts.containsKey( name );
352
353
		case R.id.merge_prompt:
354
			// if we are prompting on duplicate, we better check for one
355
			if( !_contacts.containsKey( name ) )
356
				return true;
357
358
			// ok, it exists, so do prompt
359
			_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
360
					_doit._handler, Doit.MESSAGE_MERGEPROMPT, name ) );
1 by edam
Initial import
361
			try {
362
				wait();
363
			}
364
			catch( InterruptedException e ) { }
365
3 by edam
- added "all done" message
366
			// check if an abortion happened during the wait
367
			checkAbort();
368
1 by edam
Initial import
369
			// if "always" was selected, make choice permenant
370
			if( _responseExtra == RESPONSEEXTRA_ALWAYS )
371
				_mergeSetting = _response;
372
373
			// recurse, with out new merge setting
374
			return isImportRequired( name, _response );
375
		}
376
377
		// for all other cases (either overwriting or merging) we will need the
378
		// imported data
379
		return true;
380
	}
381
382
	protected void skipContact() throws AbortImportException
383
	{
384
		checkAbort();
3 by edam
- added "all done" message
385
		_doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTSKIPPED );
1 by edam
Initial import
386
	}
387
388
	protected void importContact( ContactData contact )
389
			throws AbortImportException
390
	{
391
		checkAbort();
392
3 by edam
- added "all done" message
393
//		if( !showContinue( "====[ IMPORTING ]====\n: " + contact._name ) )
394
//			finish( ACTION_ABORT );
1 by edam
Initial import
395
396
		ContentValues values = new ContentValues();
397
		boolean uiInformed = false;
398
399
		// does contact exist already?
400
		Uri contactUri = null;
401
		Long id;
402
		if( ( id = (Long)_contacts.get( contact._name ) ) != null )
403
		{
404
			// should we skip this import altogether?
405
			if( _lastMergeDecision == R.id.merge_keep ) return;
406
407
			// get contact's URI
408
			contactUri = ContentUris.withAppendedId(
409
					Contacts.People.CONTENT_URI, id );
410
411
			// should we destroy the existing contact before importing?
412
			if( _lastMergeDecision == R.id.merge_overwrite ) {
413
				_doit.getContentResolver().delete( contactUri, null, null );
414
				contactUri = null;
415
416
				// upate the UI
3 by edam
- added "all done" message
417
				_doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTOVERWRITTEN );
1 by edam
Initial import
418
				uiInformed = true;
419
420
				// update cache
421
				_contacts.remove( contact._name );
422
			}
423
		}
424
425
		// if we don't have a contact URI it is because the contact never
426
		// existed or because we deleted it
427
		if( contactUri == null )
428
		{
429
			// create a new contact
430
			values.put( Contacts.People.NAME, contact._name );
431
			contactUri = _doit.getContentResolver().insert(
432
					Contacts.People.CONTENT_URI, values );
433
			id = ContentUris.parseId( contactUri );
434
			if( id <= 0 ) return;	// shouldn't happen!
435
436
			// add them to the "My Contacts" group
437
			Contacts.People.addToGroup(
438
					_doit.getContentResolver(), id,
439
					Contacts.Groups.GROUP_MY_CONTACTS );
440
441
			// update cache
442
			_contacts.put( contact._name, id );
443
444
			// update UI
445
			if( !uiInformed ) {
3 by edam
- added "all done" message
446
				_doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTCREATED );
1 by edam
Initial import
447
				uiInformed = true;
448
			}
449
		}
450
451
		// update UI
452
		if( !uiInformed )
3 by edam
- added "all done" message
453
			_doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTMERGED );
1 by edam
Initial import
454
455
		// import contact parts
456
		if( contact._phones != null )
457
			importContactPhones( contactUri, contact._phones );
458
		if( contact._emails != null )
459
			importContactEmails( contactUri, contact._emails );
460
	}
461
462
	private void importContactPhones( Uri contactUri,
463
			HashMap< String, ContactData.PhoneData > phones )
464
	{
465
		Long contactId = ContentUris.parseId( contactUri );
466
		Uri contactPhonesUri = Uri.withAppendedPath( contactUri,
467
				Contacts.People.Phones.CONTENT_DIRECTORY );
468
		Set phonesKeys = phones.keySet();
7 by edam
- new contact's phone numebrs and email addresses are added to the caches after those contacts are updated to account for the situation where the same contact is imported again from another file (or the contact exists twice in the same file!?)
469
470
		// add phone numbers
1 by edam
Initial import
471
		Iterator i = phonesKeys.iterator();
472
		while( i.hasNext() ) {
473
			ContactData.PhoneData phone = phones.get( i.next() );
474
475
			// we don't want to add this number if it's crap, or it already
476
			// exists (which would cause a duplicate to be created). We don't
477
			// take in to account the type when checking for duplicates. This is
478
			// intentional: types aren't really very reliable. We assume that
479
			// if the number exists at all, it doesn't need importing. Because
480
			// of this, we also can't update the cache (which we don't need to
481
			// anyway, so it's not a problem).
482
			String number = sanitisePhoneNumber( phone._number );
483
			if( number == null ) continue;
484
			HashSet< String > numbers = _contactNumbers.get( contactId );
485
			if( numbers != null && numbers.contains( number ) ) continue;
486
487
			// add phone number
488
			ContentValues values = new ContentValues();
489
			values.put( Contacts.Phones.TYPE, phone._type );
490
			values.put( Contacts.Phones.NUMBER, phone._number );
491
			if( phone._isPreferred ) values.put( Contacts.Phones.ISPRIMARY, 1 );
492
			_doit.getContentResolver().insert( contactPhonesUri, values );
493
		}
7 by edam
- new contact's phone numebrs and email addresses are added to the caches after those contacts are updated to account for the situation where the same contact is imported again from another file (or the contact exists twice in the same file!?)
494
495
		// now add those phone numbers to the cache to prevent the addition of
496
		// duplicate data from another file
497
		i = phonesKeys.iterator();
498
		while( i.hasNext() ) {
499
			ContactData.PhoneData phone = phones.get( i.next() );
500
501
			String number = sanitisePhoneNumber( phone._number );
502
			if( number != null ) {
503
				HashSet< String > numbers = _contactNumbers.get( contactId );
504
				if( numbers == null ) {
505
					_contactNumbers.put( contactId, new HashSet< String >() );
506
					numbers = _contactNumbers.get( contactId );
507
				}
508
				numbers.add( number );
509
			}
510
		}
1 by edam
Initial import
511
	}
512
513
	private void importContactEmails( Uri contactUri,
514
			HashMap< String, ContactData.EmailData > emails )
515
	{
516
		Long contactId = ContentUris.parseId( contactUri );
517
		Uri contactContactMethodsUri = Uri.withAppendedPath( contactUri,
518
				Contacts.People.ContactMethods.CONTENT_DIRECTORY );
519
		Set emailsKeys = emails.keySet();
7 by edam
- new contact's phone numebrs and email addresses are added to the caches after those contacts are updated to account for the situation where the same contact is imported again from another file (or the contact exists twice in the same file!?)
520
521
		// add email addresses
1 by edam
Initial import
522
		Iterator i = emailsKeys.iterator();
523
		while( i.hasNext() ) {
524
			ContactData.EmailData email = emails.get( i.next() );
525
526
			// like with phone numbers, we don't want to add this email address
527
			// if it exists already or we would introduce duplicates.
528
			String address = sanitiseEmailAddress( email.getAddress() );
529
			if( address == null ) continue;
530
			HashSet< String > addresses = _contactEmails.get( contactId );
531
			if( addresses != null && addresses.contains( address ) ) continue;
532
533
			// add phone number
534
			ContentValues values = new ContentValues();
535
			values.put( Contacts.ContactMethods.KIND, Contacts.KIND_EMAIL );
536
			values.put( Contacts.ContactMethods.DATA, email.getAddress() );
537
			values.put( Contacts.ContactMethods.TYPE, email.getType() );
538
			if( email.isPreferred() )
539
				values.put( Contacts.ContactMethods.ISPRIMARY, 1 );
540
			_doit.getContentResolver().insert( contactContactMethodsUri,
541
					values );
542
		}
7 by edam
- new contact's phone numebrs and email addresses are added to the caches after those contacts are updated to account for the situation where the same contact is imported again from another file (or the contact exists twice in the same file!?)
543
544
		// now add those email addresses to the cache to prevent the addition of
545
		// duplicate data from another file
546
		i = emailsKeys.iterator();
547
		while( i.hasNext() ) {
548
			ContactData.EmailData email = emails.get( i.next() );
549
550
			String address = sanitiseEmailAddress( email.getAddress() );
551
			if( address != null ) {
552
				HashSet< String > addresses = _contactEmails.get( contactId );
553
				if( addresses == null ) {
554
					_contactEmails.put( contactId, new HashSet< String >() );
555
					addresses = _contactEmails.get( contactId );
556
				}
557
				addresses.add( address );
558
			}
559
		}
1 by edam
Initial import
560
	}
561
3 by edam
- added "all done" message
562
	synchronized protected void checkAbort() throws AbortImportException
1 by edam
Initial import
563
	{
564
		if( _abort ) {
565
			// stop
566
			throw new AbortImportException();
567
		}
568
	}
569
570
	private void buildContactsCache() throws AbortImportException
571
	{
572
		// update UI
573
		setProgressMessage( R.string.doit_caching );
574
575
		String[] cols;
576
		Cursor cur;
577
578
		// init contacts caches
579
		_contacts = new HashMap< String, Long >();
580
		_contactNumbers = new HashMap< Long, HashSet< String > >();
581
		_contactEmails = new HashMap< Long, HashSet< String > >();
582
583
		// query and store map of contact names to ids
584
		cols = new String[] { Contacts.People._ID, Contacts.People.NAME };
585
		cur = _doit.managedQuery( Contacts.People.CONTENT_URI,
586
				cols, null, null, null);
587
		if( cur.moveToFirst() ) {
588
			int idCol = cur.getColumnIndex( Contacts.People._ID );
589
			int nameCol = cur.getColumnIndex( Contacts.People.NAME );
590
			do {
591
				_contacts.put( cur.getString( nameCol ), cur.getLong( idCol ) );
592
			} while( cur.moveToNext() );
593
		}
594
595
		// query and store map of contact ids to sets of phone numbers
596
		cols = new String[] { Contacts.Phones.PERSON_ID,
597
				Contacts.Phones.NUMBER };
598
		cur = _doit.managedQuery( Contacts.Phones.CONTENT_URI,
599
				cols, null, null, null);
600
		if( cur.moveToFirst() ) {
601
			int personIdCol = cur.getColumnIndex( Contacts.Phones.PERSON_ID );
602
			int numberCol = cur.getColumnIndex( Contacts.Phones.NUMBER );
603
			do {
604
				Long id = cur.getLong( personIdCol );
605
				String number = sanitisePhoneNumber(
606
						cur.getString( numberCol ) );
607
				if( number != null ) {
608
					HashSet< String > numbers = _contactNumbers.get( id );
609
					if( numbers == null ) {
610
						_contactNumbers.put( id, new HashSet< String >() );
611
						numbers = _contactNumbers.get( id );
612
					}
613
					numbers.add( number );
614
				}
615
			} while( cur.moveToNext() );
616
		}
617
618
		// query and store map of contact ids to sets of email addresses
619
		cols = new String[] { Contacts.ContactMethods.PERSON_ID,
620
				Contacts.ContactMethods.DATA };
621
		cur = _doit.managedQuery( Contacts.ContactMethods.CONTENT_URI,
622
				cols, Contacts.ContactMethods.KIND + " = ?",
623
				new String[] { "" + Contacts.KIND_EMAIL }, null );
624
		if( cur.moveToFirst() ) {
625
			int personIdCol = cur.getColumnIndex(
626
					Contacts.ContactMethods.PERSON_ID );
627
			int addressCol = cur.getColumnIndex(
628
					Contacts.ContactMethods.DATA );
629
			do {
630
				Long id = cur.getLong( personIdCol );
631
				String address = sanitiseEmailAddress(
632
						cur.getString( addressCol ) );
633
				if( address != null ) {
634
					HashSet< String > addresses = _contactEmails.get( id );
635
					if( addresses == null ) {
636
						_contactEmails.put( id, new HashSet< String >() );
637
						addresses = _contactEmails.get( id );
638
					}
639
					addresses.add( address );
640
				}
641
			} while( cur.moveToNext() );
642
		}
643
	}
644
645
	private String sanitisePhoneNumber( String number )
646
	{
647
		number = number.replaceAll( "[-\\(\\) ]", "" );
648
		Pattern p = Pattern.compile( "^\\+?[0-9]+" );
649
		Matcher m = p.matcher( number );
650
		if( m.lookingAt() ) return m.group( 0 );
651
		return null;
652
	}
653
654
	private String sanitiseEmailAddress( String address )
655
	{
656
		address = address.trim();
657
		Pattern p = Pattern.compile(
658
				"^[^ @]+@[a-zA-Z]([-a-zA-Z0-9]*[a-zA-z0-9])?(\\.[a-zA-Z]([-a-zA-Z0-9]*[a-zA-z0-9])?)+$" );
659
		Matcher m = p.matcher( address );
660
		if( m.matches() ) {
661
			String[] bits = address.split( "@" );
662
			return bits[ 0 ] + "@" + bits[ 1 ].toLowerCase();
663
		}
664
		return null;
665
	}
666
}