001    package edu.harvard.deas.hyperenc.gui;
002    
003    import java.awt.Color;
004    import java.awt.Component;
005    import java.awt.Container;
006    import java.awt.Font;
007    import java.awt.GridBagConstraints;
008    import java.awt.GridBagLayout;
009    import java.awt.GridLayout;
010    import java.awt.event.ActionEvent;
011    import java.awt.event.ActionListener;
012    import java.awt.event.KeyEvent;
013    import java.awt.event.MouseAdapter;
014    import java.awt.event.MouseEvent;
015    import java.awt.event.MouseListener;
016    import java.awt.event.WindowAdapter;
017    import java.awt.event.WindowEvent;
018    import java.text.DateFormat;
019    import java.util.ArrayList;
020    import java.util.Collections;
021    import java.util.Date;
022    import java.util.List;
023    import java.util.concurrent.ExecutionException;
024    
025    import javax.mail.Address;
026    import javax.swing.JFrame;
027    import javax.swing.JLabel;
028    import javax.swing.JList;
029    import javax.swing.JMenu;
030    import javax.swing.JMenuBar;
031    import javax.swing.JMenuItem;
032    import javax.swing.JOptionPane;
033    import javax.swing.JPanel;
034    import javax.swing.JScrollPane;
035    import javax.swing.JTable;
036    import javax.swing.KeyStroke;
037    import javax.swing.ListSelectionModel;
038    import javax.swing.SwingWorker;
039    import javax.swing.event.ListSelectionEvent;
040    import javax.swing.event.ListSelectionListener;
041    import javax.swing.table.DefaultTableCellRenderer;
042    
043    import org.apache.log4j.Logger;
044    
045    import edu.harvard.deas.hyperenc.BlockMissingException;
046    import edu.harvard.deas.hyperenc.Contact;
047    import edu.harvard.deas.hyperenc.ContactList;
048    import edu.harvard.deas.hyperenc.Direction;
049    import edu.harvard.deas.hyperenc.EncryptionException;
050    import edu.harvard.deas.hyperenc.HyperCollector;
051    import edu.harvard.deas.hyperenc.HyperCommunicator;
052    import edu.harvard.deas.hyperenc.HyperEncryptionEvent;
053    import edu.harvard.deas.hyperenc.HyperEncryptionEventType;
054    import edu.harvard.deas.hyperenc.HyperEncryptionListener;
055    import edu.harvard.deas.hyperenc.HyperMessage;
056    import edu.harvard.deas.hyperenc.HyperMessageType;
057    import edu.harvard.deas.hyperenc.MessageParseException;
058    import edu.harvard.deas.hyperenc.PageException;
059    import edu.harvard.deas.hyperenc.PlainTextMessageParser;
060    import edu.harvard.deas.hyperenc.util.UUCoder;
061    
062    /**
063     * A graphical front-end for hyper-encryption.
064     */
065    public class HyperGui
066        extends JFrame
067        implements ActionListener, HyperEncryptionListener, ComposerListener {
068            private static final long serialVersionUID = 1L;
069            
070            private static final Logger logger = Logger.getLogger(HyperGui.class);
071            
072            private JList contactsListBox;
073            private JTable messagesListBox;
074            private MessageTableModel messageTableModel;
075            private JPanel statusPanel;
076            private Contact currentContact;
077            private ContactList currentContacts;
078            private HyperCommunicator communicator;
079            private HyperCollector collector;
080            private Contact myOwner;
081            
082            private StatusPanel masterPanel;
083            private StatusPanel slavePanel;
084    
085      /**
086       * Constructs a GUI.
087       * 
088       * @param communicator
089       *        a HyperCommunicator. Used for sending and receiving messages.
090       * @param collector
091       *        a HyperCollector that holds a master and slave storage for each
092       *        contact.
093       * @param storage
094       *        a MessageStorage. Messages stored in this MessageStorage will be
095       *        viewable by the user, and newly-received messages will be stored in
096       *        this MessageStorage.
097       * @param contacts
098       *        the people we're talking with; each contact should have a master and
099       *        slave storage in <code>collector</code>
100       * @param myOwner
101       *        Contact representing the user, with the user's e-mail address/name
102       * @throws NullPointerException
103       *         if any argument is <code>null</code>
104       */
105            public HyperGui(HyperCommunicator communicator, HyperCollector collector,
106                            MessageStorage storage, ContactList contacts, Contact myOwner) {
107                    if (communicator == null)
108                      throw new NullPointerException("communicator may not be null");
109                    if (collector == null)
110          throw new NullPointerException("collector may not be null");
111                    if (storage == null)
112          throw new NullPointerException("storage may not be null");
113                    if (contacts == null)
114          throw new NullPointerException("contacts may not be null");
115                    if (myOwner == null)
116                      throw new NullPointerException("myOwner may not be null");
117        
118                    this.communicator = communicator;
119        this.collector = collector;
120        this.messageTableModel = new MessageTableModel(storage);
121        this.myOwner = myOwner;
122        
123                    setTitle("Hyper-encryption -- " + myOwner.toString());
124                    
125                    setSize(850, 400);
126                    setResizable(true);
127                    setLocation(100, 100);
128            
129                    // Handle closing of the window
130                    addWindowListener(new WindowAdapter() {
131                            public void windowClosing(WindowEvent e) {
132                                       System.exit(0);
133                            }
134                    });
135                    
136                    // Creates and initializes the main menu bar
137                    JMenuBar mb = createMenu();
138                    setJMenuBar(mb);
139    
140                    // Creates and initializes the tool bar
141                    JPanel toolbar = new Toolbar(this);
142    
143                    // A simple header to let the user know what's going on 
144                    JLabel contactHeader = new JLabel("Contacts");
145                    contactHeader.setVerticalAlignment(JLabel.CENTER);
146                    contactHeader.setHorizontalAlignment(JLabel.CENTER);
147        
148                    
149                    
150                    // ****** CONTACT LIST BOX ******
151                    
152                    // Grabs the current list of contactsListBox, loads it into a display list
153                    // and wraps it in a scroll pane for ease of use
154                    currentContacts = contacts;
155                    String [] contactList = getContactNames();
156    
157                    contactsListBox = new JList(contactList);
158                    contactsListBox.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
159                    // Fixes the size of the listing
160                    contactsListBox.setPrototypeCellValue("Michael O. Rabin");
161    
162                    assert(contactList != null);
163                    if (contactList.length > 0) {
164                            currentContact = currentContacts.get(0);
165                            contactsListBox.setSelectedIndex(0);  
166                    }
167    
168                    logger.debug("contactsListBox created, 1st is " + currentContact);
169    
170                    // When a different contact is selected, update the currentContact and
171        // change the message listing
172                    contactsListBox.addListSelectionListener(new ListSelectionListener() {
173                            public void valueChanged(ListSelectionEvent e) {
174                            
175                                    int index = contactsListBox.getSelectedIndex();
176                                    if (index != -1) {
177                                            currentContact = currentContacts.get(index);
178                                            messageTableModel.selectSender(currentContact);
179                                    }
180                                    
181                                    updateStatusPanels();
182                            }
183                    });
184    
185                    JScrollPane contactListScrollPane = new JScrollPane();
186                    contactListScrollPane.getViewport().setView(contactsListBox);
187    
188                                    
189                    
190        // ****** MESSAGE LIST BOX ******
191                    
192                    // Create the list to display messagesListBox.  Initialize it to display those
193                    // for the first contact, if there is one
194                    String [] messageList = null;
195                    messagesListBox = new JTable(messageTableModel);
196                    //messagesListBox.setCellRenderer(new MessageCellRenderer());
197                    messagesListBox.setSelectionBackground(Color.CYAN);
198                    messagesListBox.setAutoCreateRowSorter(true);
199                    
200                    messagesListBox.setDefaultRenderer(Date.class,
201                      // from http://java.sun.com/docs/books/tutorial/uiswing/components/table.html
202                new DefaultTableCellRenderer() {
203                        private static final long serialVersionUID = 1L;  // hurrr
204            DateFormat formatter;
205            public void setValue(Object value) {
206              if (formatter == null) {
207                formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
208                                                           DateFormat.MEDIUM);
209              }
210              setText((value == null) ? "" : formatter.format(value));
211            }
212          }
213        );
214    
215                    if (currentContact != null) {
216                            messageTableModel.selectSender(currentContact);
217                    }
218                    logger.debug("messagesListBox populated,");
219    
220                    // Register for double clicks on messages -- open message when double-clicked
221                    MouseListener mouseListener = new MouseAdapter() {
222                            public void mouseClicked(MouseEvent e) {
223                                    if (e.getClickCount() == 2) {   // Register double clicks
224                                            int index = messagesListBox.rowAtPoint(e.getPoint());
225                                            if (index < 0)
226                                              return;
227                                            int modelIndex = messagesListBox.convertRowIndexToModel(index);
228                                            logger.debug("Double clicked on Item " + index + " (model " + modelIndex + ")");
229                                            openMessage(modelIndex);
230                                    }
231                            }
232                    };
233                    messagesListBox.addMouseListener(mouseListener);
234    
235                    
236                    JScrollPane msgBoxScrollPane = new JScrollPane();
237                    messagesListBox.setFillsViewportHeight(true);
238                    msgBoxScrollPane.getViewport().setView(messagesListBox);
239                    
240                    
241                    
242                    
243        // ****** STATUS PANEL ******
244                
245                    statusPanel = new JPanel();
246                    GridLayout gl = new GridLayout(4, 1);
247                    statusPanel.setLayout(gl);
248                    
249                    Font labelFont = new Font("Serif", Font.BOLD, 18);
250        
251                    JLabel master = new JLabel("Sending Status");
252        master.setFont(labelFont);
253        master.setVerticalAlignment(JLabel.CENTER);
254        master.setHorizontalAlignment(JLabel.CENTER);
255        
256        JLabel slave = new JLabel("Receiving Status");
257        slave.setFont(labelFont);
258        slave.setVerticalAlignment(JLabel.CENTER);
259        slave.setHorizontalAlignment(JLabel.CENTER);
260        
261        masterPanel = new StatusPanel();
262        slavePanel = new StatusPanel();
263        
264        statusPanel.add(master);
265        statusPanel.add(masterPanel);
266        statusPanel.add(slave);
267        statusPanel.add(slavePanel);
268        
269                    updateStatusPanels();
270    
271                    
272                    
273                    // Finally, add all the components to the frame
274                    
275                    Container pane = this.getContentPane();
276        pane.setLayout(new GridBagLayout());
277        GridBagConstraints gridConstraints = new GridBagConstraints();
278        
279        addItem(toolbar, gridConstraints, 0, 0, 5, 1, GridBagConstraints.HORIZONTAL,
280                GridBagConstraints.CENTER, 0, 0);
281        addItem(contactHeader, gridConstraints, 0, 1, 1, 1, GridBagConstraints.NONE,
282                GridBagConstraints.CENTER, 10, 0);
283        addItem(contactListScrollPane, gridConstraints, 0, 2, 1, 28/*GridBagConstraints.REMAINDER*/,
284                GridBagConstraints.BOTH, GridBagConstraints.CENTER, 10, 10);
285        addItem(msgBoxScrollPane, gridConstraints, 1, 1, 4,  29,
286                GridBagConstraints.BOTH, GridBagConstraints.CENTER, 50, 50);
287        addItem(statusPanel, gridConstraints, 0, 30, 7, 4, 
288                GridBagConstraints.BOTH, GridBagConstraints.CENTER, 5, 5);
289            }
290    
291    
292            /**
293             * Create the menu bar.
294             * @return a JMenuBar for the menu bar that appears at the top of the window
295             */
296            private JMenuBar createMenu() {
297    
298                    // Create the top-level menus
299                    JMenu menuFile = new JMenu("File");
300                    JMenu menuEdit = new JMenu("Edit");
301                    JMenu menuAction = new JMenu("Action");
302                    JMenu menuHelp = new JMenu("Help");
303    
304                    // Create the individual menu items.  Use the Hyper Encrypter as the action listener
305                    JMenuItem item = new JMenuItem("New Message", KeyEvent.VK_N);
306                    item.addActionListener(this);
307                    menuFile.add(item);
308                    
309                    item = new JMenuItem("Initiate Reconciliation", KeyEvent.VK_R);
310        item.addActionListener(this);
311        menuFile.add(item);
312        
313        item = new JMenuItem("Retrieve a page", KeyEvent.VK_P);
314        item.addActionListener(this);
315        item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK));
316        menuFile.add(item);
317        
318        item = new JMenuItem("Make blocks from pages", KeyEvent.VK_P);
319        item.addActionListener(this);
320        menuFile.add(item);
321    
322                    item = new JMenuItem("Add Contact", KeyEvent.VK_C);
323                    item.addActionListener(this);
324                    menuFile.add(item);
325    
326                    item = new JMenuItem("Remove Contact", KeyEvent.VK_X);
327                    item.addActionListener(this);
328                    menuFile.add(item);
329    
330                    item = new JMenuItem("Exit");
331                    item.addActionListener(this);
332                    menuFile.add(item);
333    
334                    item = new JMenuItem("Delete", KeyEvent.VK_DELETE);
335                    item.addActionListener(this);
336                    menuEdit.add(item);
337    
338                    item = new JMenuItem("Mark Read", KeyEvent.VK_M);
339                    item.addActionListener(this);
340                    menuEdit.add(item);
341    
342                    item = new JMenuItem("Mark Unread", KeyEvent.VK_U);
343                    item.addActionListener(this);
344                    menuEdit.add(item);
345    
346                    item = new JMenuItem("Get Mail", KeyEvent.VK_F5);
347                    item.addActionListener(this);
348                    menuAction.add(item);
349    
350                    item = new JMenuItem("Reply", KeyEvent.VK_R);
351                    item.addActionListener(this);
352                    menuAction.add(item);
353    
354                    item = new JMenuItem("Forward", KeyEvent.VK_F);
355                    item.addActionListener(this);
356                    menuAction.add(item);
357    
358                    item = new JMenuItem("Help");
359                    item.addActionListener(this);
360                    menuHelp.add(item);
361    
362                    item = new JMenuItem("About");
363                    item.addActionListener(this);
364                    menuHelp.add(item);             
365    
366                    // Finally, add all menus to a menu bar and return it
367                    JMenuBar mb = new JMenuBar();
368                    JMenu[] menus = { menuFile, menuEdit, menuAction, menuHelp };
369                    for (int i = 0; i < menus.length; i++) {
370                            mb.add(menus[i]);
371                    }
372    
373                    return mb;
374            }
375    
376            private String[] getContactNames() {
377                    List<String> clst = new ArrayList<String>();
378                    int i = 0;
379                    for (Contact c : currentContacts) {
380                      clst.add(c.getDisplayName());
381                    }
382                    Collections.sort(clst);
383    
384                    String[] ret = new String[clst.size()];
385                    clst.toArray(ret);
386                    return ret;
387            }
388     
389    
390            /* Helper function for adding items to the grid bag layout */
391            // XXX order of arguments is impossible to remember, making this method prone
392      // to error; how can we improve it?
393            private void addItem(Component item, GridBagConstraints gc, 
394                                                     int xPosition, int yPosition, int width, int height,
395                                                     int fill, int anchor, int wx, int wy) {
396                    gc.gridx = xPosition;
397                    gc.gridy = yPosition;
398                    gc.gridwidth = width;
399                    gc.gridheight = height;
400                    gc.fill = fill;
401                    gc.anchor = anchor;
402                    gc.weightx = wx; 
403                    gc.weighty = wy;
404    
405                    getContentPane().add(item, gc);
406            }
407    
408            /** Dispatches and/or handles menu and toolbar commands */
409            @Override
410            public void actionPerformed(ActionEvent e) {
411                    // TODO hard-coding these strings is fragile and unwise -- fix it, probably
412        // by creating private listener classes
413                    if (e.getActionCommand().equals("Exit")) {
414                            System.exit(0);
415                    } else if (e.getActionCommand().equals("New Message") || e.getActionCommand().equals("New")) {
416                            newMessage();
417                    } else if (e.getActionCommand().equals("Initiate Reconciliation")) {
418                      startReconcile();
419                    } else if (e.getActionCommand().equals("Retrieve a page")) {
420                      retrievePage();
421                    } else if (e.getActionCommand().equals("Make blocks from pages")) {
422                      makeBlocksFromPages();
423                    } else if (e.getActionCommand().equals("Add Contact")) {
424                            addContact(null);
425                    } else if (e.getActionCommand().equals("Remove Contact")) {
426                            removeContact();
427                    } else if (e.getActionCommand().equals("Delete")) {
428                            delete();
429                    } else if (e.getActionCommand().equals("Mark Read")) {
430                      for (int rowNum : messagesListBox.getSelectedRows())
431                        markRowAsRead(rowNum, true);
432                    } else if (e.getActionCommand().equals("Mark Unread")) {
433                      for (int rowNum : messagesListBox.getSelectedRows())
434            markRowAsRead(rowNum, false);
435                    } else if (e.getActionCommand().equals("Get Mail")) {
436                            getMail();
437                    } else if (e.getActionCommand().equals("Decrypt")) {
438                      decrypt();
439                    } else if (e.getActionCommand().equals("Reply")) {
440                            reply();
441                    } else if (e.getActionCommand().equals("Forward")) {
442                            forward();
443                    } else if (e.getActionCommand().equals("Help")) {
444                            JOptionPane.showMessageDialog(
445                                null,
446                                "For help, please consult the manual included with this program.",
447                                "Help",
448                                JOptionPane.INFORMATION_MESSAGE);
449                    } else if (e.getActionCommand().equals("About")) {
450                            JOptionPane.showMessageDialog(
451                                null,
452                                "For more information about hyper-encryption, please consult the " +
453                                    "documentation included with this program.",
454                                "About Hyper Encryption",
455                                JOptionPane.INFORMATION_MESSAGE);
456                    }
457                    
458            }
459    
460            /** Creates a new message window  */
461            private void newMessage() {
462                    Composer comp = new Composer(myOwner, currentContact, "", "");
463                    comp.addComposerListener(this);
464            }
465            
466            /** Sends a master reconciliation message */
467            private void startReconcile() {
468              // TODO need to NOT send this message if there's already a pending rec msg
469              SwingWorker<Boolean,Void> worker = new SwingWorker<Boolean,Void>() {
470                @Override protected Boolean doInBackground() {
471                  HyperMessage mrec;
472                  try {
473                    mrec = collector.sendRec(currentContact, new PlainTextMessageParser());
474                  } catch (EncryptionException e) {
475                    return false;
476                  }
477                  communicator.send(mrec);
478                  return true;
479                }
480                
481                @Override protected void done() {
482                  boolean result;
483                  try {
484                    result = get();
485                  } catch (InterruptedException e) {
486                    throw new RuntimeException(
487                  "Unexpected InterruptedException in reconciliation thread", e);
488                  } catch (ExecutionException e) {
489                    throw new RuntimeException(
490                  "Unexpected exception thrown from reconciliation thread", e);
491                  }
492                  if (!result) {
493                    JOptionPane.showMessageDialog(null,
494                  "Could not reconcile pages: there were not enough encryption "
495                      + "blocks available to send the message.",
496                  "Could not start reconciliation",
497                  JOptionPane.ERROR_MESSAGE);
498                  }
499                }
500              };
501              
502              worker.execute();
503            }
504            
505            /** Tries to generate a page for the selected contact in each direction. */
506            private void retrievePage() {
507              SwingWorker<List<String>,Void> worker = new SwingWorker<List<String>,Void>() {            
508                @Override
509                protected List<String> doInBackground() {
510                  List<String> errors = new ArrayList<String>();
511                  
512                  try {
513                    collector.collectNext(currentContact, Direction.MASTER);
514                  } catch (EncryptionException e) {
515                    errors.add("Could not retrieve an outgoing page: there are no " +
516                               "system blocks available.");
517                  } catch (PageException e) {
518                    errors.add("Could not retrieve an outgoing page: all PSNs " +
519                               "contacted were unavailable. Please try again later.");
520                  }
521                  
522                  try {
523                    collector.collectNext(currentContact, Direction.SLAVE);
524                  } catch (EncryptionException e) {
525              errors.add("Could not retrieve an incoming page: there are no " +
526                         "system blocks available.");
527            } catch (PageException e) {
528              errors.add("Could not retrieve an incoming page: all PSNs " +
529                         "contacted were unavailable. Please try again later.");
530            }
531                  
532                  return errors;
533                }
534                
535                @Override
536                protected void done() {
537                  List<String> errList;
538                  try {
539                    errList = get();
540                  } catch (InterruptedException e) {
541                    throw new RuntimeException(
542                  "Unexpected InterruptedException in page retrieval thread", e);
543                  } catch (ExecutionException e) {
544                    // really shouldn't happen
545                    throw new RuntimeException("Unexpected exception", e);
546                  }
547                  
548                  for (String err : errList) {
549              JOptionPane.showMessageDialog(null,
550                  err,
551                  "Could not download pages",
552                  JOptionPane.ERROR_MESSAGE);
553            }
554                }
555              };
556              
557              worker.execute();
558            }
559            
560            /**
561       * Tries to create system/encryption blocks from reconciled pages, in each
562       * direction.
563       */
564      private void makeBlocksFromPages() {
565        try {
566          collector.makeBlocks(currentContact, Direction.MASTER);
567        } catch (EncryptionException e) {
568          JOptionPane.showMessageDialog(null,
569              "There are not enough reconciled pages with which to create " +
570                  "outgoing blocks. (" + HyperCollector.PAGES_PER_PAD + " are " +
571                  "required, but only " +
572                  collector.numRPages(currentContact, Direction.MASTER) +
573                  " are available.)",
574              "Could not download outgoing pages",
575              JOptionPane.ERROR_MESSAGE);
576        }
577        
578        try {
579          collector.makeBlocks(currentContact, Direction.SLAVE);
580        } catch (EncryptionException e) {
581          JOptionPane.showMessageDialog(null,
582              "There are not enough reconciled pages with which to create " +
583                  "incoming blocks. (" + HyperCollector.PAGES_PER_PAD + " are" +
584                  "required, but only " +
585                  collector.numRPages(currentContact, Direction.MASTER) +
586                  " are available.)",
587              "Could not download incoming pages",
588              JOptionPane.ERROR_MESSAGE);
589        }
590      }
591    
592            /** Handles new contact creation. */
593            private void addContact(Contact contact) {
594                    // Open the add contact window
595                    AddContact ac;
596        if (contact == null)
597          ac = new AddContact(this);
598        else
599          ac = new AddContact(this, contact.getEmail().toString(),
600                              contact.getFullName(), contact.getDisplayName());
601                    
602                    // Grab the contact that was created
603                    Contact c = ac.getNewContact();
604    
605                    if (c != null) {
606                            currentContacts.addContact(c);  // Add him to our collection
607                            
608                            //communicator.getContacter().sendContact(c.email); // send keyrequest
609                            //c.status = Contact.STATUS_WE_WAIT;
610                            
611                            collector.newContact(c,
612                                                 ac.getOutgoingSecret(),
613                                                 ac.getIncomingSecret());
614                            
615                            contactsListBox.setListData(getContactNames()); // Update the display
616                            int index = contactsListBox.getSelectedIndex();
617                            if (index != -1) {
618            currentContact = currentContacts.get(index);
619          } else {
620            contactsListBox.setSelectedIndex(0);
621            currentContact = currentContacts.get(0);
622          }
623                                    
624                            updateStatusPanels();
625                    }
626            }
627    
628            /** Remove a contact */
629            private void removeContact() {
630                    Contact c;
631                    int index = contactsListBox.getSelectedIndex();
632    
633                    if (index != -1) {
634                            c = currentContacts.get(index);
635                            currentContacts.removeContact(c);
636                            contactsListBox.setListData(getContactNames());
637                            if (index > 0) {
638                                    contactsListBox.setSelectedIndex(index-1);
639                            } else if (currentContacts.size() > 0) {
640                                    contactsListBox.setSelectedIndex(0);
641                            }                               
642                    }
643                    
644                    // TODO remove contact's HyperStorages from the collector
645            }
646            
647            
648            /**
649             * Decrypt the selected messages.
650             */
651            private void decrypt() {
652              // Build list of model row numbers (converting from table row numbers)
653        List<Integer> modelIndices = new ArrayList<Integer>();
654        for (int tableRow : messagesListBox.getSelectedRows())
655          modelIndices.add(messagesListBox.convertRowIndexToModel(tableRow));
656        
657        for (int modelRow : modelIndices) {
658          GuiMessage gmsg = messageTableModel.getMessageAtRow(modelRow);
659          
660          if (gmsg.getHyperMessage().getType() != HyperMessageType.ENCRYPTED) {
661            JOptionPane.showMessageDialog(null,
662                "Message is not encrypted!",
663                "Error",
664                JOptionPane.ERROR_MESSAGE);
665            return; 
666          }
667          
668          boolean validMac;
669          try {
670            validMac = collector.verifyHEMAC(
671                UUCoder.decode(gmsg.getHyperMessage().getContent()),
672                gmsg.getHyperMessage().getPadsUsed(),
673                gmsg.getHyperMessage().getMac(),
674                gmsg.getSender()
675              );
676          } catch (BlockMissingException e) {
677            String message = "";
678            message += "MAC blocks missing: ";
679            for (int id : e.getMissingIDList())
680              message += id + " ";
681            logger.warn(message);
682            validMac = false;
683          }
684          
685          if (!validMac) {
686            JOptionPane.showMessageDialog(null,
687                "The message authentication code (MAC) could not be verified for "
688                + "the message with subject \"" + gmsg.getSubject() + "\". "
689                + "The message may not be authentic.",
690                "Error",
691                JOptionPane.ERROR_MESSAGE);
692            return; 
693          }
694          
695          HyperMessage decrypted;
696          try {
697            decrypted = collector.decrypt(gmsg.getHyperMessage());
698          } catch (EncryptionException ex) {
699            JOptionPane.showMessageDialog(null,
700                "An encryption block needed to decrypt this message is missing.",
701                "Error",
702                JOptionPane.ERROR_MESSAGE);
703            return;
704          }
705          
706          // Replace the encrypted message with the decrypted one.
707          messageTableModel.replaceMessageAtRow(modelRow,
708              new GuiMessage(decrypted, gmsg.isRead()));
709        }
710            }
711    
712    
713            /**
714             * Creates a new message window, prepped for a reply. If multiple messages
715       * are selected, all but the first are ignored.
716       */
717            private void reply() {
718              int index = messagesListBox.getSelectedRow();
719        if (index == -1) {
720          JOptionPane.showMessageDialog(null,
721              "Please select a message to reply to.",
722              "Select a message",
723              JOptionPane.INFORMATION_MESSAGE);
724          return;
725        }
726        
727        index = messagesListBox.convertRowIndexToModel(index);
728        GuiMessage cur = messageTableModel.getMessageAtRow(index);
729        markRowAsRead(index, true);
730        
731        // Create a new composition window based on the currently selected message
732        Composer comp = new Composer(myOwner, currentContact,
733                                     "Re: " + cur.getSubject(), cur.getContent());
734        comp.addComposerListener(this);
735            }
736    
737            /**
738       * Creates a new message window, prepped for forwarding. If multiple messages
739       * are selected, all but the first are ignored.
740       */
741            private void forward() {
742                    int index = messagesListBox.getSelectedRow();
743                    if (index == -1) {
744          JOptionPane.showMessageDialog(null,
745              "Please select a message to forward.",
746              "Select a message",
747              JOptionPane.INFORMATION_MESSAGE);
748          return;
749                    }
750    
751                    index = messagesListBox.convertColumnIndexToModel(index);
752        GuiMessage cur = messageTableModel.getMessageAtRow(index);
753        markRowAsRead(index, true);
754    
755        // Create a new composition window based on the currently selected message
756        Composer comp = new Composer(myOwner, currentContact,
757                                     "Fwd: " + cur.getSubject(), cur.getContent());
758        comp.addComposerListener(this);
759            }
760    
761            /** Deletes all currently selected messages. */
762            private void delete() {
763                    int[] indices = messagesListBox.getSelectedRows();
764                    for (int index : indices) {
765                      int modelIndex = messagesListBox.convertColumnIndexToModel(index);
766                            GuiMessage cur = messageTableModel.getMessageAtRow(modelIndex);
767                            messageTableModel.removeMessage(cur.getID());
768                    }
769            }
770    
771      /**
772       * Update the read flag of the message in the given row of the message list.
773       * If <code>val</code> is <code>true</code>, mark the message read, otherwise
774       * unread.
775       * <p>
776       * The row number provided should be in <i>table coordinates</i>, such as that
777       * returned by {@link<code>JTable#getSelectedRow()</code>}.
778       */
779            private void markRowAsRead(int tableRowNum, boolean val) {
780              int modelRowNum = messagesListBox.convertRowIndexToModel(tableRowNum);
781              if (val) {
782                messageTableModel.markRead(modelRowNum);
783              } else {
784                messageTableModel.markUnread(modelRowNum);
785              }
786            }
787    
788      /**
789       * Retrieve new mail and process each incoming message. If the message is a
790       * master or slave reconciliation message, the appropriate reconciliation
791       * method of the collector is called to perform the reconciliation. Other
792       * messages are placed in the MessageStorage to be processed at the user's
793       * discretion.
794       */
795            private void getMail() {
796              SwingWorker<List<GuiMessage>,Void> worker =
797                new SwingWorker<List<GuiMessage>,Void>() {
798                @Override protected List<GuiMessage> doInBackground() {
799                  // All incoming messages
800                  List<HyperMessage> newMessages = communicator.receive();
801                  // Incoming messages that aren't processed automatically, and will be
802                  // saved and seen by the user
803                  List<GuiMessage> userMessages = new ArrayList<GuiMessage>();
804                  
805                  for (HyperMessage hm : newMessages) {
806                    if (hm.getType() == HyperMessageType.MASTER_REC) {
807                      // massage all whitespace
808                  String body = hm.getContent().trim().replaceAll("\\s+", ".");
809                      boolean validMac;
810                      try {
811                      validMac = collector.verifyHEMAC(
812                          body.getBytes(), hm.getPadsUsed(),
813                          hm.getMac(), hm.getSender());
814                      } catch (BlockMissingException e) {
815                        String message = "";
816                        message += "MAC blocks missing: ";
817                        for (int id : e.getMissingIDList())
818                          message += id + " ";
819                        logger.warn(message);
820                        validMac = false;
821                      }
822                      
823                      if (!validMac) {
824                        JOptionPane.showMessageDialog(null,
825                            "The message authentication code (MAC) could not be "
826                            + "verified for a reconciliation initiation message (\""
827                            + hm.getSubject() + "\"). The message may not be authentic.",
828                            "Error",
829                            JOptionPane.ERROR_MESSAGE);
830                        continue;
831                      }
832                      
833                      HyperMessage response;
834                      try {
835                        response = collector.reconcile(hm.getSender(), hm, new PlainTextMessageParser());
836                      } catch (MessageParseException e) {
837                        logger.warn("Malformed master rec message: " + hm.getContent());
838                        continue;
839                      }
840                      
841                      communicator.send(response);
842                      logger.info("Responded to master reconciliation message");
843                    } else if (hm.getType() == HyperMessageType.SLAVE_REC) {
844                      // massage all whitespace
845                      String body = hm.getContent().trim().replaceAll("\\s+", ".");
846                      boolean validMac;
847                      try {
848                      validMac = collector.verifyHEMAC(
849                        body.getBytes(), hm.getPadsUsed(),
850                        hm.getMac(), hm.getSender());
851                      } catch (BlockMissingException e) {
852                        String message = "";
853                        message += "MAC blocks missing: ";
854                        for (int id : e.getMissingIDList())
855                          message += id + " ";
856                        logger.warn(message);
857                    validMac = false;
858                  }
859                  
860                  if (!validMac) {
861                    JOptionPane.showMessageDialog(null,
862                        "The message authentication code (MAC) could not be "
863                        + "verified for a reconciliation response message (\""
864                        + hm.getSubject() + "\"). The message may not be authentic.",
865                        "Error",
866                        JOptionPane.ERROR_MESSAGE);
867                    continue;
868                  }
869                      
870                      try {
871                        collector.receiveRec(hm.getSender(), hm, new PlainTextMessageParser());
872                      } catch (MessageParseException e) {
873                        logger.warn("Malformed slave rec message: " + hm.getContent());
874                        continue;
875                      }
876                      
877                      logger.info("Processed slave reconciliation message");
878                    } else {
879                      userMessages.add(new GuiMessage(hm, ReadStatus.UNREAD));
880                      logger.debug("Stored message (type " + hm.getType() + ")");
881                    }
882                  }
883                  
884                  return userMessages;
885                }
886              
887                @Override protected void done() {
888                  List<GuiMessage> userMessages;
889                  try {
890                    userMessages = get();
891                  } catch (InterruptedException e) {
892                throw new RuntimeException(
893                  "Unexpected InterruptedException in mail retrieval thread", e);
894              } catch (ExecutionException e) {
895                // really shouldn't happen
896                throw new RuntimeException(
897                  "Unexpected exception thrown from mail retrieval thread", e);
898              }
899                  
900                  // Put all user-visible messages in the message storage
901                  for (GuiMessage gm : userMessages)
902                    messageTableModel.addMessage(gm);
903                  
904                  // Reload the message list for the selected contact
905                  int index = contactsListBox.getSelectedIndex();
906                  if (index != -1) {
907                    currentContact = currentContacts.get(index);
908                    messageTableModel.selectSender(currentContact);
909                  }
910                }
911              };
912                
913              worker.execute();
914            }
915    
916            private void openMessage(int rowNum) {
917              GuiMessage msg = messageTableModel.getMessageAtRow(rowNum);
918              markRowAsRead(rowNum, true);
919                    Composer comp = new Composer(myOwner, currentContact, 
920                                                                                           msg.getSubject(), msg.getContent());
921                    comp.addComposerListener(this);
922            }
923    
924            /*
925            private void showMaster() {
926            Contact c;
927            int index = contactsListBox.getSelectedIndex();
928        
929            if (index != -1) {
930                c = currentContacts.get(index);
931                            //showDisplay(c.masterDisplay, "Sending Status for " + c.fullName);
932                    }
933            }
934            
935            private void showSlave() {
936            Contact c;
937            int index = contactsListBox.getSelectedIndex();
938        
939            if (index != -1) {
940                c = currentContacts.get(index);
941                            //showDisplay(c.slaveDisplay , "Receiving Status for " + c.fullName);
942                    }
943            }
944                    
945    
946            private void showDisplay(final Display d, String str) {
947                    JFrame f = new JFrame(str);
948                    f.setSize(Display.WIDTH, Display.HEIGHT);
949                    f.setLocation(100, 100);
950    
951                    d.animate();
952                    f.setContentPane(d);
953    
954                    f.addWindowListener(new WindowAdapter() {
955                                    public void windowClosing(WindowEvent e) {
956                                            d.stopAnimation();
957                                    }
958                                    });
959    
960                    f.setVisible(true);
961            }
962            */
963            
964            
965            private void updateStatusPanels() {
966              if (currentContact == null)
967                return;
968              
969              if (!collector.hasContact(currentContact)) {
970                // oops!
971                throw new IllegalStateException("currentContact does not exist in the collector");
972              }
973              
974              masterPanel.setNumSysBlocks(collector.numSysBlocks(currentContact, Direction.MASTER));
975              masterPanel.setNumEncBlocks(collector.numEncBlocks(currentContact, Direction.MASTER));
976        masterPanel.setNumUnrecPages(collector.numUPages(currentContact, Direction.MASTER));
977        masterPanel.setNumRecPages(collector.numRPages(currentContact, Direction.MASTER));
978        
979              slavePanel.setNumSysBlocks(collector.numSysBlocks(currentContact, Direction.SLAVE));
980        slavePanel.setNumEncBlocks(collector.numEncBlocks(currentContact, Direction.SLAVE));
981        slavePanel.setNumUnrecPages(collector.numUPages(currentContact, Direction.SLAVE));
982        slavePanel.setNumRecPages(collector.numRPages(currentContact, Direction.SLAVE));
983            }
984            
985            
986            /**
987             * When a new outgoing message is ready for processing, encrypts it and sends it.
988             */
989            @Override
990            public void messageComposed(ComposerEvent e) {
991              HyperMessage hm = e.getMessage();
992              
993              if (hm == null)   // user closed the Composer window without sending the message
994                return;
995              
996              HyperMessage encryptedMsg;
997        try {
998          encryptedMsg = collector.encrypt(hm);
999        } catch (EncryptionException ex) {
1000          JOptionPane.showMessageDialog(null,
1001              ex.getMessage(),
1002              "Not enough encryption blocks",
1003              JOptionPane.ERROR_MESSAGE);
1004          return;
1005        }
1006        communicator.send(encryptedMsg);
1007            }
1008    
1009            
1010            /** Something happened in the HyperEncryption world - update our info */
1011            @Override
1012            public void eventFired(HyperEncryptionEvent e) {
1013              Address a = e.getContact().getEmail();
1014                    Contact c = currentContacts.getContact(a);
1015              
1016                    if (c == null) {
1017                      logger.warn("Message received and ignored for non-existent contact "
1018                        + a.toString());
1019                      
1020                      // XXX at the moment, this is not used...
1021                      // this isn't useful for now, anyway -- if you don't have
1022                // sys/enc blocks for the contact, adding the contact to your list doesn't
1023                // accomplish anything
1024                            if (e.getType() == HyperEncryptionEventType.CONTACT_REQUEST) {
1025                                    int ans=JOptionPane.showConfirmDialog(this,
1026                                                    a.toString()+
1027                                                    " has requested to concact you."+
1028                                                    "Do you want to add him/her as a "+
1029                                                    "contact now?",
1030                                                    "Contact Initiation Request",
1031                                                    JOptionPane.YES_NO_OPTION,
1032                                                    JOptionPane.QUESTION_MESSAGE);
1033                                    if (ans == JOptionPane.YES_OPTION) {
1034                                      c = new Contact(a.toString(), a, a.toString());
1035                                            addContact(c);
1036                                    }
1037                            }
1038                            return;
1039                    }
1040    
1041                    // Grab the appropriate status panel
1042                    StatusPanel sp = (e.getDirection() == Direction.MASTER) ? masterPanel : slavePanel;
1043    
1044                    // Update numbers appropriately
1045        switch (e.getType()) {
1046                            case BLOCK_STORAGE_UPDATED:
1047                              updateStatusPanels();
1048                              logger.debug("Updated status panels");
1049            break;
1050    
1051          // Deal with contact statuses
1052                            case CONTACT_REQUEST: // fall through
1053                            case CONTACT_ESTABLISHED:
1054                                    logger.debug("Contacter event ignored");
1055                                    break;
1056        }
1057            }
1058    }