/**
* GmailAssistant 1.1 (2008-03-16)
* Copyright 2008 Zach Scrivena
* zachscrivena@gmail.com
* http://gmailassistant.sourceforge.net/
*
* Notifier for multiple Gmail accounts.
*
* TERMS AND CONDITIONS:
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package gmailassistant;
import com.sun.mail.imap.IMAPFolder;
import gmailassistant.Account;
import java.awt.Color;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Pattern;
import javax.mail.AuthenticationFailedException;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.search.FlagTerm;
import javax.mail.search.SearchTerm;
import javax.swing.AbstractAction;
import javax.swing.JColorChooser;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
/**
* Represent a Gmail account.
*/
class Account
extends JFrame
implements Comparable<Account>
{
/** map: key string ---> property */
static final Map<String,Property> KEY_STRING_TO_PROPERTY = new HashMap<String,Property>();
/** suffix for Gmail username */
private static final String GMAIL_USERNAME_SUFFIX = "@gmail.com";
/** regex pattern for Gmail username, excludig suffix "@gmail.com" */
private static final Pattern GMAIL_USERNAME_PATTERN = Pattern.compile(
"[" + Pattern.quote("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.") + "]+");
/** regex pattern for Google Apps email username, including domain name */
private static final Pattern GOOGLE_APPS_EMAIL_USERNAME_PATTERN = Pattern.compile(
"[" + Pattern.quote("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-.") + "]+" +
"@[^@\\s]+\\.[^@\\.\\s]+");
/** incoming mail protocol (secure IMAP) */
private static final String INCOMING_PROTOCOL = "imaps";
/** incoming mail server */
private static final String INCOMING_SERVER = "imap.gmail.com";
/** incoming mail server port */
private static final int INCOMING_PORT = 993;
/** timer refresh interval */
private static final long TIMER_REFRESH_INTERVAL_MILLISECONDS = 200L;
/** search term for messages with the SEEN keep turned OFF */
private final SearchTerm unseenFlag = new FlagTerm(new Flags(Flags.Flag.SEEN), false);
/** parent GmailAssistant object */
private final GmailAssistant parent;
/** account ID */
final int id;
/** text fields for the notify labels */
private final JTextField[] notifyFields;
/** property keys corresponding to the notify labels */
private final Property[] notifyLabelsProperty;
/** is this account being edited? */
private volatile boolean editing = false;
/** have the login credentials been verified? */
private volatile boolean loginCredentialsVerified = false;
/** is the login username valid? */
private volatile boolean usernameValid = false;
/** is the login password valid? */
private volatile boolean passwordValid = false;
/** is the notify selection valid? */
private volatile boolean notifyValid = false;
/** mail store for the account */
private final Store store;
/** mutex lock for this.store */
private final Object storeLock = new Object();
/** unread mails for this account */
private final Map<MailId,Mail> mails = new HashMap<MailId,Mail>();
/** last received mail ID */
private int lastSequenceNumber = 0;
/** timer for the mail checker */
private final Timer timer;
/** queued actions to be performed by the timer */
private final Queue<ActionType> actions = new ArrayDeque<ActionType>();
/** account properties */
private final Map<Property,Object> properties;
/**
* Static initialization block.
*/
static
{
/* create map: key string ---> property */
for (Property p : Property.values())
{
if (p.keyString != null)
{
Account.KEY_STRING_TO_PROPERTY.put(p.keyString, p);
}
}
}
/**
* Return a new default account properties map.
*
* @return
* new default account properties
*/
static Map<Property,Object> getNewDefaultProperties()
{
final Map<Property,Object> properties = new HashMap<Property,Object>();
for (Property p : Property.values())
{
properties.put(p, p.initialVal);
}
return properties;
}
/**
* Constructor.
*
* @param parent
* parent GmailAssistant object
* @param id
* account ID assigned by the parent GmailAssistant object
* @param properties
* account properties for the new account
*/
Account(
final GmailAssistant parent,
final int id,
final Map<Property,Object> properties)
{
/*********************
* INITIALIZE FIELDS *
*********************/
this.parent = parent;
this.id = id;
this.properties = properties;
synchronized (this.storeLock)
{
final Session session = Session.getInstance(System.getProperties(), null);
session.setDebug(false);
try
{
this.store = session.getStore(Account.INCOMING_PROTOCOL);
}
catch (Exception e)
{
SwingManipulator.showErrorDialog(this.parent, GmailAssistant.NAME,
"(INTERNAL) Invalid incoming mail protocol \"" + Account.INCOMING_PROTOCOL + "\".");
throw new TerminatingException(e.getMessage());
}
}
/******************************
* INITIALIZE FORM COMPONENTS *
******************************/
initComponents();
/*****************************
* CONFIGURE FORM COMPONENTS *
*****************************/
addWindowListener(new WindowAdapter()
{
@Override
public void windowClosing(WindowEvent e)
{
/* equivalent to clicking on the "Cancel" button */
Account.this.cancelButton.doClick();
}
@Override
public void windowIconified(WindowEvent e)
{
/* equivalent to clicking on the "Cancel" button */
Account.this.cancelButton.doClick();
}
});
/* inherit "always on top" behavior of parent */
try
{
setAlwaysOnTop(this.parent.isAlwaysOnTop());
}
catch (Exception e)
{
/* ignore */
}
/* inherit program icon of parent */
final List<Image> icons = this.parent.getIconImages();
if (!icons.isEmpty())
{
setIconImage(icons.get(0));
}
/* field: "username" */
this.usernameField.setText((String) getProperty(Property.USERNAME));
this.usernameField.getDocument().addDocumentListener(new DocumentListenerAdapter()
{
@Override
public void insertUpdate(DocumentEvent e)
{
Account.this.loginCredentialsVerified = false;
setProperty(Property.USERNAME, SwingManipulator.getTextJTextField(Account.this.usernameField));
checkUsername();
}
@Override
public void removeUpdate(DocumentEvent e)
{
Account.this.loginCredentialsVerified = false;
setProperty(Property.USERNAME, SwingManipulator.getTextJTextField(Account.this.usernameField));
checkUsername();
}
@Override
public void changedUpdate(DocumentEvent e)
{
Account.this.loginCredentialsVerified = false;
setProperty(Property.USERNAME, SwingManipulator.getTextJTextField(Account.this.usernameField));
checkUsername();
}
});
/* field: "password" */
this.passwordField.setText(String.valueOf((char[]) getProperty(Property.PASSWORD)));
this.passwordField.getDocument().addDocumentListener(new DocumentListenerAdapter()
{
@Override
public void insertUpdate(DocumentEvent e)
{
Account.this.loginCredentialsVerified = false;
setProperty(Property.PASSWORD, SwingManipulator.getPasswordJPasswordField(Account.this.passwordField));
checkPassword();
}
@Override
public void removeUpdate(DocumentEvent e)
{
Account.this.loginCredentialsVerified = false;
setProperty(Property.PASSWORD, SwingManipulator.getPasswordJPasswordField(Account.this.passwordField));
checkPassword();
}
@Override
public void changedUpdate(DocumentEvent e)
{
Account.this.loginCredentialsVerified = false;
setProperty(Property.PASSWORD, SwingManipulator.getPasswordJPasswordField(Account.this.passwordField));
checkPassword();
}
});
/* button: "color" */
final Color c = (Color) getProperty(Property.COLOR);
final String s = "rgb(" +
c.getRed() + "," +
c.getGreen() + "," +
c.getBlue() + ")";
this.colorButton.setText("<html><span style='color:" + s +
";background-color:" + s +
"'> M </span></html>");
this.colorButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
final Color c = JColorChooser.showDialog(
Account.this,
"Account Color - " + Account.this.getTitle(),
(Color) getProperty(Property.COLOR));
if (c != null)
{
final String s = "rgb(" +
c.getRed() + "," +
c.getGreen() + "," +
c.getBlue() + ")";
Account.this.colorButton.setText("<html><span style='color:" + s +
";background-color:" + s +
"'> M </span></html>");
setProperty(Property.COLOR, c);
}
}
});
/* label: "lock" */
this.lock.setToolTipText(GmailAssistant.NAME + " accesses your Gmail account securely using IMAP over SSL");
/* radio button: "Notify Inbox" */
this.notifyInboxRadio.setSelected((Boolean) getProperty(Property.NOTIFY_INBOX));
this.notifyInboxRadio.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setProperty(Property.NOTIFY_INBOX, Account.this.notifyInboxRadio.isSelected());
checkNotify();
}
});
/* radio button: "Notify Any" */
this.notifyAnyRadio.setSelected((Boolean) getProperty(Property.NOTIFY_ANY));
this.notifyAnyRadio.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setProperty(Property.NOTIFY_ANY, Account.this.notifyAnyRadio.isSelected());
checkNotify();
}
});
/* radio button: "Notify Labels" */
this.notifyLabelsRadio.setSelected((Boolean) getProperty(Property.NOTIFY_LABELS));
this.notifyLabelsRadio.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setProperty(Property.NOTIFY_LABELS, Account.this.notifyLabelsRadio.isSelected());
checkNotify();
}
});
/* list of notify labels */
this.notifyFields = new JTextField[]
{
this.notifyField1,
this.notifyField2,
this.notifyField3,
this.notifyField4,
this.notifyField5,
this.notifyField6
};
this.notifyLabelsProperty = new Property[]
{
Property.NOTIFY_LABEL1,
Property.NOTIFY_LABEL2,
Property.NOTIFY_LABEL3,
Property.NOTIFY_LABEL4,
Property.NOTIFY_LABEL5,
Property.NOTIFY_LABEL6
};
for (int i = 0; i < this.notifyFields.length; i++)
{
final JTextField f = this.notifyFields[i];
final Property p = notifyLabelsProperty[i];
f.setText((String) getProperty(p));
f.getDocument().addDocumentListener(new DocumentListenerAdapter()
{
@Override
public void insertUpdate(DocumentEvent e)
{
setProperty(p, f.getText().trim());
checkNotify();
}
@Override
public void removeUpdate(DocumentEvent e)
{
setProperty(p, f.getText().trim());
checkNotify();
}
@Override
public void changedUpdate(DocumentEvent e)
{
setProperty(p, f.getText().trim());
checkNotify();
}
});
}
/* check box: "popup" */
this.popupBox.setSelected((Boolean) getProperty(Property.ALERT_POPUP));
this.popupBox.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
setProperty(Property.ALERT_POPUP, Account.this.popupBox.isSelected());
}
});
/* check box: "chime" */
this.chimeBox.setSelected((Boolean) getProperty(Property.ALERT_CHIME));
this.chimeBox.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
setProperty(Property.ALERT_CHIME, Account.this.chimeBox.isSelected());
}
});
/* check box: "bell" */
this.bellBox.setSelected((Boolean) getProperty(Property.ALERT_PERIODIC_BELL));
this.bellBox.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
setProperty(Property.ALERT_PERIODIC_BELL, Account.this.bellBox.isSelected());
}
});
/* check boxe: "blink" */
this.blinkBox.setSelected((Boolean) getProperty(Property.ALERT_LED));
this.blinkBox.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
setProperty(Property.ALERT_LED, Account.this.blinkBox.isSelected());
}
});
/* button: "Test Alerts" */
this.testButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
testAlerts();
}
});
/* button: "OK" */
this.okButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
tryEnableAccount();
}
});
/* button: "Cancel" */
this.cancelButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
tryDisableAccount();
}
});
/* add standard editing popup menu to text fields */
SwingManipulator.addStandardEditingPopupMenu(new JTextField[]
{
this.usernameField,
this.passwordField,
this.notifyField1,
this.notifyField2,
this.notifyField3,
this.notifyField4,
this.notifyField5,
this.notifyField6
});
/* key binding: ENTER key */
this.scrollPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "ENTER_OK_BUTTON");
this.scrollPane.getActionMap().put("ENTER_OK_BUTTON", new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
Account.this.okButton.doClick();
}
});
/* key binding: ESCAPE key */
this.scrollPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "ESCAPE_CANCEL_BUTTON");
this.scrollPane.getActionMap().put("ESCAPE_CANCEL_BUTTON", new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
Account.this.cancelButton.doClick();
}
});
/* check user selections on the form */
checkUsername();
checkPassword();
checkNotify();
/* center form on the parent form */
setLocationRelativeTo(this.parent);
/***************************
* INITIALIZE TIMER THREAD *
***************************/
this.timer = new Timer("Mail-Checker-Timer", true);
this.timer.schedule(new TimerTask()
{
/**
* Check this account for unread mails.
*/
@Override
public void run()
{
ActionType action;
synchronized (Account.this.actions)
{
action = Account.this.actions.poll();
}
if (action == ActionType.ENABLE)
{
/* enable the mail checker */
setProperty(Property.ENABLED, true);
setProperty(Property.STATUS, "<html>Waiting for next mail check</html>");
}
else if (action == ActionType.DISABLE)
{
/* disable the mail checker */
setProperty(Property.ENABLED, false);
setProperty(Property.STATUS, "<html><font color='red'>Disabled</font></html>");
}
/* proceed to check mail now? */
boolean proceedToCheckNow = false;
if ((Boolean) getProperty(Property.ENABLED))
{
if (action == ActionType.CHECKNOW)
{
proceedToCheckNow = true;
}
else
{
final long mailCheckIntervalMilliseconds =
(Long) Account.this.parent.getProperty(GmailAssistant.Property.MAIL_CHECK_INTERVAL_MILLISECONDS);
if ((Boolean) getProperty(Property.ERROR))
{
if ((System.currentTimeMillis() - (Long) getProperty(Property.LAST_CHECKED)) >=
(mailCheckIntervalMilliseconds / 10))
{
proceedToCheckNow = true;
}
}
else
{
if ((System.currentTimeMillis() - (Long) getProperty(Property.LAST_CHECKED)) >=
mailCheckIntervalMilliseconds)
{
proceedToCheckNow = true;
}
}
}
}
if (proceedToCheckNow)
{
/* proceed to check for unread mails now */
setProperty(Property.STATUS, "<html>Checking mail</html>");
if (sleepTillInterrupted(0L) == ActionType.CANCEL)
{
return;
}
/* get IMAP folders corresponding to the Gmail labels */
final List<MailLabel> labels = new ArrayList<MailLabel>();
final boolean notifyInbox = (Boolean) getProperty(Property.NOTIFY_INBOX);
final boolean notifyAny = (Boolean) getProperty(Property.NOTIFY_ANY);
final boolean notifyLabels = (Boolean) getProperty(Property.NOTIFY_LABELS);
if (notifyInbox)
{
/* notify on any unread mail in Inbox */
labels.add(new MailLabel("INBOX"));
}
else if (notifyAny)
{
/* notify on any unread mail */
labels.add(new MailLabel("All Mail"));
}
else if (notifyLabels)
{
/* notify on unread mail with the specified labels */
for (Property p : Account.this.notifyLabelsProperty)
{
final String s = (String) getProperty(p);
if (!s.isEmpty())
{
labels.add(new MailLabel(s));
}
}
}
synchronized (Account.this.storeLock)
{
setProperty(Property.STATUS, "<html>Checking mail: Logging in</html>");
if (sleepTillInterrupted(0L) == ActionType.CANCEL)
{
return;
}
/* connect to mail server */
String username = (String) getProperty(Property.USERNAME);
if (!username.contains("@"))
{
/* treat as Gmail account */
username += Account.GMAIL_USERNAME_SUFFIX;
}
try
{
Account.this.store.connect(
Account.INCOMING_SERVER,
Account.INCOMING_PORT,
username,
(String) getProperty(Property.PASSWORD_STRING));
}
catch (IllegalStateException e)
{
/* ignore */
}
catch (Exception e)
{
setProperty(Property.STATUS, "<html><font color='red'>Failed to connect to the Gmail server; waiting to retry</font></html>");
setProperty(Property.ERROR, true);
setProperty(Property.LAST_CHECKED, System.currentTimeMillis());
return;
}
if (sleepTillInterrupted(0L) == ActionType.CANCEL)
{
return;
}
boolean recent = false;
/* check for unread mails for each label */
synchronized (Account.this.mails)
{
/* set "keep" flag of all existing unread mails to false */
for (Mail m : Account.this.mails.values())
{
m.keep = false;
}
/* check mails for existing labels */
for (MailLabel l : labels)
{
if (sleepTillInterrupted(0L) == ActionType.CANCEL)
{
return;
}
/* fetch unread mails for this label from the server */
if (notifyInbox)
{
setProperty(Property.STATUS, "<html>Checking mail: Fetching unread mails in Inbox</html>");
}
else if (notifyAny)
{
setProperty(Property.STATUS, "<html>Checking mail: Fetching unread mails</html>");
}
else if (notifyLabels)
{
setProperty(Property.STATUS, "<html>Checking mail: Fetching unread \"" + l.label + "\" mails</html>");
}
try
{
final IMAPFolder f = (IMAPFolder) Account.this.store.getFolder(l.folder);
if (sleepTillInterrupted(0L) == ActionType.CANCEL)
{
return;
}
if (f.exists())
{
if (!f.isOpen())
{
f.open(Folder.READ_ONLY);
}
if (sleepTillInterrupted(0L) == ActionType.CANCEL)