/**
* 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 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.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.event.DocumentEvent;
import javax.swing.filechooser.FileNameExtensionFilter;
/**
* "Save Profile" form.
*/
class ProfileSaver
extends JFrame
{
/** should account passwords be saved in profile? */
private static final boolean DEFAULT_SAVE_ACCOUNT_PASSWORDS = false;
/** recommended minimum password length */
private static final int RECOMMENDED_MINIMUM_PASSWORD_LENGTH = 8;
/** parent GmailAssistant object */
private final GmailAssistant parent;
/** file chooser for saving profile file */
private final JFileChooser fileChooser;
/** are the "Options" selection valid? */
private volatile boolean optionsValid = false;
/** is the "Filename" selection valid? */
private volatile boolean filenameValid = false;
/**
* Constructor.
*
* @param parent
* parent GmailAssistant object
*/
ProfileSaver(
final GmailAssistant parent)
{
/*********************
* INITIALIZE FIELDS *
*********************/
this.parent = parent;
/* get current directory */
File currentDirectory = new File(".");
try
{
currentDirectory = currentDirectory.getCanonicalFile();
}
catch (Exception e)
{
/* ignore */
}
/* file chooser */
this.fileChooser = new JFileChooser(currentDirectory);
this.fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
this.fileChooser.setMultiSelectionEnabled(false);
this.fileChooser.addChoosableFileFilter(new FileNameExtensionFilter(
GmailAssistant.NAME + " Profile (*." + GmailAssistant.PROFILE_FILENAME_EXTENSION + ")",
GmailAssistant.PROFILE_FILENAME_EXTENSION));
/******************************
* INITIALIZE FORM COMPONENTS *
******************************/
initComponents();
/***************************
* CONFIGURE FORM SETTINGS *
***************************/
setTitle("Save Profile - " + this.parent.getTitle());
addWindowListener(new WindowAdapter()
{
@Override
public void windowClosing(WindowEvent e)
{
ProfileSaver.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: "Profile Password" */
this.passwordField.getDocument().addDocumentListener(new DocumentListenerAdapter()
{
@Override
public void insertUpdate(DocumentEvent e)
{
checkOptions();
}
@Override
public void removeUpdate(DocumentEvent e)
{
checkOptions();
}
@Override
public void changedUpdate(DocumentEvent e)
{
checkOptions();
}
});
/* checkbox: "Save account passwords" */
this.saveBox.setSelected(ProfileSaver.DEFAULT_SAVE_ACCOUNT_PASSWORDS);
/* field: "Filename" (initialize to an unused filename) */
File f = new File(
currentDirectory,
GmailAssistant.PROFILE_DEFAULT_FILENAME + "." + GmailAssistant.PROFILE_FILENAME_EXTENSION);
try
{
f = f.getCanonicalFile();
}
catch (Exception e)
{
/* ignore */
}
if (f.exists())
{
for (int i = 1; i < Integer.MAX_VALUE; i++)
{
f = new File(
currentDirectory,
GmailAssistant.PROFILE_DEFAULT_FILENAME + "." + i + "." + GmailAssistant.PROFILE_FILENAME_EXTENSION);
if (f.exists())
{
f = null;
}
else
{
break;
}
}
}
this.filenameField.setText((f == null) ? "" : f.getPath());
this.filenameField.getDocument().addDocumentListener(new DocumentListenerAdapter()
{
@Override
public void insertUpdate(DocumentEvent e)
{
checkFilename();
}
@Override
public void removeUpdate(DocumentEvent e)
{
checkFilename();
}
@Override
public void changedUpdate(DocumentEvent e)
{
checkFilename();
}
});
/* button: "Browse..." */
this.browseButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
final int val = ProfileSaver.this.fileChooser.showOpenDialog(ProfileSaver.this);
if (val == JFileChooser.APPROVE_OPTION)
{
File f = ProfileSaver.this.fileChooser.getSelectedFile();
if (!f.getName().contains("."))
{
f = new File(f.getParentFile(), f.getName() + "." + GmailAssistant.PROFILE_FILENAME_EXTENSION);
}
ProfileSaver.this.filenameField.setText(f.getPath());
}
}
});
/* button: "OK" */
this.okButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
saveProfile();
}
});
/* button: "Cancel" */
this.cancelButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
cancelForm();
}
});
/* add standard editing popup menu to text fields */
SwingManipulator.addStandardEditingPopupMenu(new JTextField[]
{
this.passwordField,
this.filenameField
});
/* 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)
{
ProfileSaver.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)
{
ProfileSaver.this.cancelButton.doClick();
}
});
/* check form for errors */
checkOptions();
checkFilename();
/* center form on the parent form */
setLocationRelativeTo(this.parent);
}
/**
* Check "Options" selection specified by the user.
* This method must run on the EDT.
*/
private void checkOptions()
{
String warning = null;
final char[] password = SwingManipulator.getPasswordJPasswordField(this.passwordField);
final int passwordLength = password.length;
Arrays.fill(password, '\0');
if (passwordLength == 0)
{
warning = "A password is strongly recommended";
}
else if (passwordLength < ProfileSaver.RECOMMENDED_MINIMUM_PASSWORD_LENGTH)
{
warning = "A password of at least " + ProfileSaver.RECOMMENDED_MINIMUM_PASSWORD_LENGTH +
" characters is strongly recommended";
}
this.optionsValid = true;
this.optionsError.setText((warning == null) ? " " : "<html><font color='red'>" + warning + "</font></html>");
if (this.optionsValid &&
this.filenameValid)
{
/* all selections are valid */
this.okButton.setEnabled(true);
}
}
/**
* Check "Filename" selection specified by the user.
* This method must run on the EDT.
*/
private void checkFilename()
{
String error = null;
if (SwingManipulator.getTextJTextField(this.filenameField).trim().isEmpty())
{
error = "A filename must be specified";
}
if (error != null)
{
this.filenameError.setText("<html><font color='red'>" + error + "</font></html>");
this.okButton.setEnabled(false);
this.filenameField.selectAll();
this.filenameField.requestFocus();
this.filenameValid = false;
return;
}
this.filenameValid = true;
this.filenameError.setText(" ");
if (this.optionsValid &&
this.filenameValid)
{
/* all selections are valid */
this.okButton.setEnabled(true);
}
}
/**
* Save profile using the user-specified settings on the form.
* This method must run on the EDT.
*/
private void saveProfile()
{
final File f = new File(SwingManipulator.getTextJTextField(this.filenameField));
if (f.isDirectory())
{
SwingManipulator.showErrorDialog(this, "Save Profile - " + GmailAssistant.NAME,
"Failed to save profile data to \"" + f.getPath() + "\" because it is a directory and not a file.");
return;
}
if (f.exists())
{
/* file already exists; prompt on overwriting */
final int choice = JOptionPane.showConfirmDialog(this,
"File \"" + f.getPath() + "\" already exists. Overwrite this file?",
"Save Profile - " + GmailAssistant.NAME,
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE);
if (choice == 1)
{
/* user chose NO */
return;
}
}
/* list of cleartext strings to be converted to a byte array for encryption */
final List<String> cleartext = new ArrayList<String>();
/* save program properties */
for (GmailAssistant.Property p : GmailAssistant.Property.values())
{
if (p.keyString != null)
{
final String valString = GmailAssistant.propertyValueToString(p, this.parent.getProperty(p));
if (valString != null)
{
/* "key:value" */
cleartext.add(p.keyString);
cleartext.add(":");
cleartext.add(valString);
cleartext.add(GmailAssistant.PROPERTY_DELIMITER);
}
}
}
/* save account properties */
final boolean savePasswords = this.saveBox.isSelected();
synchronized (this.parent.accounts)
{
for (Account ac : this.parent.accounts)
{
/* "[username]" */
cleartext.add("[");
cleartext.add(Account.propertyValueToString(Account.Property.USERNAME, ac.getProperty(Account.Property.USERNAME)));
cleartext.add("]");
cleartext.add(GmailAssistant.PROPERTY_DELIMITER);
for (Account.Property p : Account.Property.values())
{
if ((p.keyString != null) &&
((p != Account.Property.PASSWORD) ||
((p == Account.Property.PASSWORD) && savePasswords)))
{
final String valString = Account.propertyValueToString(p, ac.getProperty(p));
if (valString != null)
{
/* "key:value" */
cleartext.add(p.keyString);
cleartext.add(":");
cleartext.add(valString);
cleartext.add(GmailAssistant.PROPERTY_DELIMITER);
}
}
}
}
}
/* get user-specified password */
final char[] password = SwingManipulator.getPasswordJPasswordField(this.passwordField);
/* encrypt profile data */
final byte[] salt = new byte[Encryptor.SALT_LENGTH];
byte[] cleartextBytes = new byte[0];
byte[] ciphertextBytes = new byte[0];
try
{
cleartextBytes = Encryptor.stringListToByteArray(cleartext);
ciphertextBytes = Encryptor.encrypt(salt, String.valueOf(password), cleartextBytes);
Arrays.fill(password, '\0');
Arrays.fill(cleartextBytes, (byte) 0x00);
}
catch (Exception e)
{
Arrays.fill(password, '\0');
Arrays.fill(salt, (byte) 0x00);
Arrays.fill(cleartextBytes, (byte) 0x00);
Arrays.fill(ciphertextBytes, (byte) 0x00);
SwingManipulator.showErrorDialog(this, "Save Profile - " + GmailAssistant.NAME,
"Failed to encrypt profile data because of an unexpected error: " + e +
"\nPlease help to improve " + GmailAssistant.NAME + " by filing a bug report at " +
GmailAssistant.HOMEPAGE + ".\n\n" +
GmailAssistant.stackTraceString(e));
return;
}
/* write encrypted profile data to binary file */
try
{
final FileOutputStream fos = new FileOutputStream(f);
fos.write(salt);
fos.write(ciphertextBytes);
fos.flush();
fos.close();
Arrays.fill(salt, (byte) 0x00);
Arrays.fill(ciphertextBytes, (byte) 0x00);
}
catch (Exception e)
{
Arrays.fill(salt, (byte) 0x00);
Arrays.fill(ciphertextBytes, (byte) 0x00);
SwingManipulator.showErrorDialog(this, "Save Profile - " + GmailAssistant.NAME,
"Failed to write profile data to file \"" + f.getPath() + "\": " + e +
"\nPlease check that the file can be created and written to.");
return;
}
/* done */
this.setVisible(false);
}
/**
* Cancel and close the form.
*/
private void cancelForm()
{
this.setVisible(false);
}
/**
* Present the form for saving a profile.
*/
void showForm()
{
this.setVisible(true);
this.setExtendedState(JFrame.NORMAL);
this.toFront();
}
/***************************
* NETBEANS-GENERATED CODE *
***************************/
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents()
{
scrollPane = new javax.swing.JScrollPane();
panel = new javax.swing.JPanel();
optionsPanel = new javax.swing.JPanel();
lock = new javax.swing.JLabel();
passwordLabel = new javax.swing.JLabel();
saveBox = new javax.swing.JCheckBox();
passwordField = new javax.swing.JPasswordField();
optionsError = new javax.swing.JLabel();
filenamePanel = new javax.swing.JPanel();
browseButton = new javax.swing.JButton();
filenameField = new javax.swing.JTextField();
filenameError = new javax.swing.JLabel();
buttonsPanel = new javax.swing.JPanel();
okButton = new javax.swing.JButton();
cancelButton = new javax.swing.JButton();
setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);
scrollPane.setBorder(null);
scrollPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
panel.setLayout(new javax.swing.BoxLayout(panel, javax.swing.BoxLayout.Y_AXIS));
optionsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Options"));
lock.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
lock.setIcon(new javax.swing.ImageIcon(getClass().getResource("/gmailassistant/resources/lock.png"))); // NOI18N
lock.setToolTipText("GmailAssistant encrypts your profile data using AES-128 encryption");
passwordLabel.setDisplayedMnemonic('p');
passwordLabel.setLabelFor(passwordField);
passwordLabel.setText("Profile password:");
passwordLabel.setToolTipText("Password to be used for encrypting the profile data");
saveBox.setMnemonic('s');
saveBox.setText("Save account passwords in profile");
saveBox.setToolTipText("Save Gmail account passwords in the profile so they do not need to be entered again when the profile is loaded");
passwordField.setToolTipText("Password to be used for encrypting the profile data");
optionsError.setText("<html><font color='red'>options error</font></html>");
optionsError.setVerticalAlignment(javax.swing.SwingConstants.TOP);
javax.swing.GroupLayout optionsPanelLayout = new javax.swing.GroupLayout(optionsPanel);
optionsPanel.setLayout(optionsPanelLayout);
optionsPanelLayout.setHorizontalGroup(
optionsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(optionsPanelLayout.createSequentialGroup()
.addContainerGap()
.addGroup(optionsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(saveBox, javax.swing.GroupLayout.DEFAULT_SIZE, 338, Short.MAX_VALUE)
.addGroup(optionsPanelLayout.createSequentialGroup()
.addComponent(passwordLabel)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(passwordField, javax.swing.GroupLayout.DEFAULT_SIZE, 231, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(lock))
.addComponent(optionsError, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 338, Short.MAX_VALUE))
.addContainerGap())
);
optionsPanelLayout.setVerticalGroup(
optionsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(optionsPanelLayout.createSequentialGroup()
.addGroup(optionsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(optionsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(passwordLabel)
.addComponent(passwordField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
.addComponent(lock))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
.addComponent(saveBox)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
.addComponent(optionsError))
);
panel.add(optionsPanel);
filenamePanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Filename"));
browseButton.setMnemonic('b');
browseButton.setText("Browse...");
browseButton.setToolTipText("Choose a filename for the profile file");
filenameField.setText("C:\\Path\\To\\File");
filenameField.setToolTipText("Filename of the profile file");
filenameError.setText("<html><font color='red'>filename error</font></html>");
filenameError.setVerticalAlignment(javax.swing.SwingConstants.TOP);
javax.swing.GroupLayout filenamePanelLayout = new javax.swing.GroupLayout(filenamePanel);
filenamePanel.setLayout(filenamePanelLayout);
filenamePanelLayout.setHorizontalGroup(
filenamePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, filenamePanelLayout.createSequentialGroup()
.addContainerGap()
.addGroup(filenamePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
.addComponent(filenameField, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 338, Short.MAX_VALUE)
.addGroup(filenamePanelLayout.createSequentialGroup()
.addComponent(filenameError, javax.swing.GroupLayout.DEFAULT_SIZE, 253, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(browseButton)))
.addContainerGap())
);
filenamePanelLayout.setVerticalGroup(
filenamePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(filenamePanelLayout.createSequentialGroup()
.addComponent(filenameField, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(filenamePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(browseButton)
.addComponent(filenameError))
.addContainerGap(25, Short.MAX_VALUE))
);
panel.add(filenamePanel);
scrollPane.setViewportView(panel);
buttonsPanel.setLayout(new java.awt.GridLayout(1, 2));
okButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/gmailassistant/resources/tick.png"))); // NOI18N
okButton.setMnemonic('O');
okButton.setText("OK");
okButton.setToolTipText("Save profile data to the specified file");
buttonsPanel.add(okButton);
cancelButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/gmailassistant/resources/cross.png"))); // NOI18N
cancelButton.setMnemonic('C');
cancelButton.setText("Cancel");
cancelButton.setToolTipText("Cancel saving of profile data");
buttonsPanel.add(cancelButton);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
.addComponent(buttonsPanel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 386, Short.MAX_VALUE)
.addComponent(scrollPane, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 386, Short.MAX_VALUE))
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(scrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 203, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(buttonsPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 37, javax.swing.GroupLayout.PREFERRED_SIZE)
.addContainerGap())
);
pack();
}// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JButton browseButton;
private javax.swing.JPanel buttonsPanel;
private javax.swing.JButton cancelButton;
private javax.swing.JLabel filenameError;
private javax.swing.JTextField filenameField;
private javax.swing.JPanel filenamePanel;
private javax.swing.JLabel lock;
private javax.swing.JButton okButton;
private javax.swing.JLabel optionsError;
private javax.swing.JPanel optionsPanel;
private javax.swing.JPanel panel;
private javax.swing.JPasswordField passwordField;
private javax.swing.JLabel passwordLabel;
private javax.swing.JCheckBox saveBox;
private javax.swing.JScrollPane scrollPane;
// End of variables declaration//GEN-END:variables
}