Ignore:
Timestamp:
01/29/20 13:20:24 (4 years ago)
Author:
bnemhaus
Message:

Revised implementation of authenticated Expeditee mail. Motivated by bugs relating to messages not being marked as read and incorrect counting of new messages for users, the Expeditee mail system has been rewritten. The new code not only does not exhibit the previous bugs but is also better engineered. Whilst the MailBay is static (which is in line with the MessageBay), the Mail class is no longer static and must be initialised for each user as they log in.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/expeditee/auth/mail/gui/MailBay.java

    r1376 r1504  
    44import java.nio.file.Path;
    55import java.nio.file.Paths;
    6 import java.util.LinkedList;
     6import java.util.ArrayList;
     7import java.util.Base64;
     8import java.util.HashMap;
    79import java.util.List;
    810import java.util.Map;
    911import java.util.Scanner;
    1012
     13import javax.crypto.SecretKey;
     14
    1115import org.expeditee.auth.AuthenticatorBrowser;
     16import org.expeditee.auth.mail.Mail;
     17import org.expeditee.auth.mail.Mail.MailEntry;
    1218import org.expeditee.core.Clip;
    1319import org.expeditee.core.Colour;
     
    1521import org.expeditee.core.Font;
    1622import org.expeditee.core.Image;
     23import org.expeditee.encryption.Actions;
    1724import org.expeditee.gio.EcosystemManager;
    1825import org.expeditee.gio.GraphicsManager;
     
    2431import org.expeditee.items.Item;
    2532import org.expeditee.items.Text;
    26 import org.expeditee.settings.UserSettings;
    2733
    2834public class MailBay {
     35        // Frameset name for mail
    2936        public static final String EXPEDITEE_MAIL_FRAMESET_NAME = "expediteemail";
    30 
    31         /** The y position of the header to the mail bay. */
    32         private static final int HEADER_OFFSET_Y = 15;
    33        
    34         /** Space between mail messages */
     37       
     38        // Font used for messages in the MailBay
     39        private static final Font MESSAGE_FONT = new Font("Serif-Plain-16");
     40       
     41        // Bay preview area constants.
    3542        private static final int SPACING = 25;
    36        
    37         /** The (x,y) of the top message. */
    38         private static final int MESSAGE_OFFSET_Y = 15 + SPACING;
     43        private static final int OFFSET_Y = 15;
    3944        private static final int OFFSET_X = 20;
    40 
    41         /** Buffer image of the mail window. */
    42         private static Image _mailBuffer;
    43        
    44         /** The list of messages shown in the mail bay. */
    45         private static List<Item> _previewMessages = new LinkedList<Item>();
    46         private static List<Item> _messages = new LinkedList<Item>();
    47        
    48         /** Font used for mail messages. */
    49         private static Font _messageFont = new Font("Serif-Plain-16");
    50 
    51         /** Used to number messages. */
    52         private static int _messageCount;
    53        
    54         /** Creator for creating the mail frames. */
    55         private static FrameCreator _creator = null;
    56        
    57         /** The currently logged in user, consulted when deciding if a new FrameCreator is needed. */
    58         private static String _forUser = UserSettings.UserName.get();
    59        
    60         /** The link that the preview pane displays pointing towards unprocessed messages. */
    61         private static Text _mailLink = new Text(-2, "@Mail", Colour.BLACK, Colour.WHITE);
    62 
    63         /** Wether the link has been drawn before. */
    64         private static boolean isLinkInitialized = false;
    65 
    66         /** The position of the mail link. */
    67         private static int MAIL_LINK_Y_OFFSET = 100;
    68         private static int MAIL_LINK_X = 50;
    69        
    70         /**
    71          * Obtain the two messages shown at the bottom of the screen as a preview to the entire collection of mail.
    72          * @return
    73          */
    74         public static List<Item> getPreviewMessages() {
    75                 return _previewMessages;
    76         }
    77        
    78         /**
    79          * An item is a 'preview mail item' if it is currently being previewed at the bottom of the screen.
    80          * @param i
    81          * @return
    82          */
    83         public static boolean isPreviewMailItem(Item i) {
    84                 return _previewMessages.contains(i) || i == _mailLink;
    85         }
    86        
    87         public static void disconnect() {
    88                 if (_forUser != UserSettings.UserName.get()) {
    89                         _creator = null;
    90                         _forUser = UserSettings.UserName.get();
    91                 }
    92         }
    93                
    94         /**
    95          * Adds a message to the MailBay.
    96          * @param message The basic text of the message, what is displayed in the bay window down the bottom.
    97          * @options A map describing the buttons to be provided as reply options (Button Text, Action to run)
    98          * @return
    99          */
    100         public synchronized static Text addMessage(String timestamp, String sender, String message, String message2, Map<String, String> options) {
     45       
     46        private static final int MAIL_LINK_X = 50;
     47
     48        private static List<Item> previewMessages = new ArrayList<Item>();
     49        private static FrameCreator creator;
     50        private static Mail mail;
     51        private static Text mailLink;
     52        private static Text checkMailAction;
     53       
     54        private static Image mailBufferImage;
     55       
     56        public static Mail getMailClient() {
     57                return mail;
     58        }
     59       
     60        public static Mail getMailClient(String user) {
     61                boolean restore = mail != null;
     62                String oldUser = restore ? mail.getUser() : null;
     63                MailBay.reconnectToUser(user);
     64                Mail ret = mail;
     65                if (restore) {
     66                        MailBay.reconnectToUser(oldUser);
     67                }
     68                return ret;
     69        }
     70
     71        public static List<Item> getPreviewMessages() {
     72                return previewMessages;
     73        }
     74       
     75        public static boolean isPreviewMailItem(Item i) {
     76                return previewMessages.contains(i) || i == getMailLink() || i == checkMailAction;
     77        }
     78       
     79        public static Image getImage(Clip clip, Dimension size) {
     80                // Can't get an image with an invalid size
     81                if (size == null || size.width <= 0 || size.height <= 0) {
     82                        return null;
     83                }
     84               
     85                // Update the buffer
     86                updateBuffer(Item.DEFAULT_BACKGROUND, clip, size);
     87               
     88                // Return the image buffer
     89                return mailBufferImage;
     90        }
     91       
     92        public static Item getMailLink() {
     93                return mailLink;
     94        }
     95       
     96        public static Item getCheckMailAction() {
     97                return checkMailAction;
     98        }
     99       
     100        public static void reconnectToUser(String user) {
     101                mail = new Mail(user);
     102                creator = new FrameCreator(EXPEDITEE_MAIL_FRAMESET_NAME, FrameIO.MAIL_PATH,
     103                                EXPEDITEE_MAIL_FRAMESET_NAME, FrameCreator.ExistingFramesetOptions.AppendAfterLastItem,
     104                                false, AuthenticatorBrowser.PROFILEENCRYPTIONLABEL);
     105                updateLink();
     106        }
     107       
     108        public static void checkMail() {
    101109                // Invalidate whole area
    102110                DisplayController.invalidateArea(DisplayController.getMessageBayPaintArea());
    103                
    104                 // Ensure frame creator
    105                 if (_creator == null || _forUser != UserSettings.UserName.get()) {
    106                         _forUser = UserSettings.UserName.get();
    107                         _creator = new FrameCreator(EXPEDITEE_MAIL_FRAMESET_NAME, FrameIO.MAIL_PATH, EXPEDITEE_MAIL_FRAMESET_NAME, FrameCreator.ExistingFramesetOptions.AppendAfterLastItem, false, AuthenticatorBrowser.PROFILEENCRYPTIONLABEL);
    108                 }
    109                                
    110                 // We have enough space for the header + 2 preview messages
    111                 if (_previewMessages.size() >= 2) {
    112                         _previewMessages.remove(0);
    113                         for (Item i : _previewMessages) {
    114                                 i.setY(i.getY() - SPACING);
    115                         }
    116                 }
    117                                
    118                 // Add new message
    119                 Mail mail = new Mail(message, message2, options);
    120                 Text t = mail.getPreviewMessage(true);
    121                 _messages.add(t);
    122                 Text header = _creator.addText(timestamp + " (From: " + sender + ")", Colour.BLACK, null, null, false);
     111                previewMessages.clear();
     112               
     113                // Get new mail since last time we checked for mail
     114                List<MailEntry> newMail = mail.checkMail();
     115               
     116                // Place up to two previews
     117                Map<String, Integer> countBySender = countBySender(newMail);
     118                String[] senders = countBySender.keySet().toArray(new String[] {});     
     119                for (int i = 0; i < senders.length && i < 3; i++) {
     120                        String sender = senders[i];
     121                        Text text = new Text("You have received " + countBySender.get(sender) + " new messages from " + sender);
     122                        text.setFont(MESSAGE_FONT);
     123                        text.setPosition(MAIL_LINK_X, OFFSET_Y + (SPACING * (i + 1)));
     124                        previewMessages.add(text);
     125                }
     126               
     127                // Add new mail to creator
     128                for (MailEntry mail: newMail) {
     129                        String timestamp = mail.getTimestamp();
     130                        String sender = mail.getSender();
     131                        generateMailHeader(timestamp, sender);
     132                        creator.addText("Subject: " + mail.getSubject(), Colour.BLACK, null, null, false).setFont(MESSAGE_FONT);
     133                        creator.addText(mail.getMessage(), Colour.BLACK, null, null, false).setFont(MESSAGE_FONT);
     134                        Map<String, String> optionsTextActionMap = mail.getOptionsTextActionMap();
     135                        String[] optionsTextActionMapKeyset = optionsTextActionMap.keySet().toArray(new String[] {});
     136                        for (int i = 0; i < optionsTextActionMapKeyset.length; i++) {
     137                                String text = optionsTextActionMapKeyset[i];
     138                                String[] split = text.split(":::");
     139                                String action = optionsTextActionMap.get(text);
     140                                Text t = new Text(split[0]);
     141                                for (int o = 1; o < split.length; o++) {
     142                                        t.addToData(split[o]);
     143                                }
     144                                int y = OFFSET_Y + (previewMessages.size() + i) * SPACING;
     145                                t.setPosition(OFFSET_X, y);
     146                                t.setColor(Colour.BLUE);
     147                                t.setFont(MESSAGE_FONT);
     148                                t.setAction(action);
     149                                creator.addItem(t, false);
     150                        }
     151                }
     152
     153                DisplayController.requestRefresh(true);
     154        }
     155               
     156        public static void decryptOneOffSecureMessage(SecretKey key, List<String> data) {
     157                byte[] subjectBytes = Base64.getDecoder().decode(data.get(3));
     158                String subject = new String(Actions.DecryptSymmetric(subjectBytes, key));
     159                byte[] messageBytes = Base64.getDecoder().decode(data.get(4));
     160                String message = new String(Actions.DecryptSymmetric(messageBytes, key));
     161               
     162                creator.addText(data.get(0) + " (Single-use encrypted message)", Colour.BLACK, null, null, false);
     163                creator.addText(subject, Colour.BLACK, null, null, false).setFont(MESSAGE_FONT);
     164                creator.addText(message, Colour.BLACK, null, null, false).setFont(MESSAGE_FONT);
     165               
     166                for (int i = 5; i < data.size(); i += 2) {
     167                        byte[] optionKeyBytes = Base64.getDecoder().decode(data.get(i));
     168                        String k = new String(org.expeditee.encryption.Actions.DecryptSymmetric(optionKeyBytes, key));
     169                        byte[] optionValueBytes = Base64.getDecoder().decode(data.get(i + 1));
     170                        String v = new String(org.expeditee.encryption.Actions.DecryptSymmetric(optionValueBytes, key));
     171                        creator.addText(k, Colour.BLACK, null, v, false).setFont(MESSAGE_FONT);
     172                }
     173        }
     174
     175        private static void generateMailHeader(String timestamp, String sender) {
     176                Text header = creator.addText(timestamp + " (From: " + sender + ")", Colour.BLACK, null, null, false);
    123177                Path credentialsFilePath = Paths.get(FrameIO.CONTACTS_PATH).resolve(sender + "-credentials").resolve("credentials.inf");
    124178                if (credentialsFilePath.toFile().exists()) {
     
    131185                        }
    132186                }
    133                 for (Text line: mail.getMessage()) {
    134                         _creator.addItem(line.copy(), false);
    135                 }
    136                 t.setLink(_creator.getCurrentFrame().getName());
    137                 _previewMessages.add(t);
    138                 _creator.addSpace(SPACING);
    139                 _creator.save();
    140                
    141                 // Make sure the link points to the latest frame
    142                 _mailLink.setLink(_creator.getCurrent());
    143                
    144                 DisplayController.requestRefresh(true);
    145                
    146                 return t;
    147         }
    148 
    149         /**
    150          * Obtains the image item that is drawn to display the mail bay.
    151          * @param clip
    152          * @param size
    153          * @return
    154          */
    155         public static Image getImage(Clip clip, Dimension size) {
    156                 // Can't get an image with an invalid size
    157                 if (size == null || size.width <= 0 || size.height <= 0) {
    158                         return null;
    159                 }
    160                
    161                 // Update the buffer
    162                 updateBuffer(Item.DEFAULT_BACKGROUND, clip, size);
    163                
    164                 // Return the image buffer
    165                 return _mailBuffer;
    166         }
    167        
    168         public static void clear() {
    169                 getPreviewMessages().clear();
    170                 _messageCount = 0;
    171         }
    172        
    173         public static Item getMailLink() {
    174                 return _mailLink;
    175         }
    176        
    177         public static void ensureLink() {
    178                 if (_mailLink.getLink() == null && FrameIO.canAccessFrame(MailBay.EXPEDITEE_MAIL_FRAMESET_NAME + 1)) {
    179                         _mailLink.setLink(MailBay.EXPEDITEE_MAIL_FRAMESET_NAME + 1);
    180                 }
    181         }       
    182        
    183         /** Updates the image buffer to reflect the current state of the mail bay. */
     187        }
     188       
    184189        private synchronized static void updateBuffer(Colour background, Clip clip, Dimension size) {
    185190                // If the buffer doesn't exist or is the wrong size, recreate it
    186                 if (_mailBuffer == null || !_mailBuffer.getSize().equals(size)) {
    187                         _mailBuffer = Image.createImage(size, true);
     191                if (mailBufferImage == null || !mailBufferImage.getSize().equals(size)) {
     192                        mailBufferImage = Image.createImage(size, true);
    188193                        clip = null; // Need to recreate the entire image;
    189194                        updateSize();
     
    192197                // Prepare graphics
    193198                GraphicsManager g = EcosystemManager.getGraphicsManager();
    194                 g.pushDrawingSurface(_mailBuffer);
     199                g.pushDrawingSurface(mailBufferImage);
    195200                if (clip != null) {
    196201                        g.pushClip(clip);
     
    198203                g.setAntialiasing(true);
    199204                g.clear(background);
    200                 g.setFont(_messageFont);
     205                g.setFont(MESSAGE_FONT);
    201206               
    202207                // Paint header
    203                 FrameGraphics.PaintItem(getHeader(Colour.BLACK));
    204                
    205                 // Paint the mail messages to the screen (preview)
    206                 for (Item message : _previewMessages) {
    207                         if (message != null) {
    208                                 if (clip == null || clip.isNotClipped() || message.isInDrawingArea(clip.getBounds())) {
    209                                         FrameGraphics.PaintItem(message);
    210                                 }
     208                FrameGraphics.PaintItem(getHeader());
     209               
     210                // Paint the mail messages to the bay
     211                for (Item message: previewMessages) {
     212                        if (message == null) { continue; }
     213                       
     214                        if (clip == null || clip.isNotClipped() || message.isInDrawingArea(clip.getBounds())) {
     215                                FrameGraphics.PaintItem(message);
    211216                        }
    212217                }
     
    219224               
    220225                // Paint the link to the mail frame
    221                 if (clip == null || clip.isNotClipped() || _mailLink.isInDrawingArea(clip.getBounds())) {
    222                         FrameGraphics.PaintItem(_mailLink);
     226                if (clip == null || clip.isNotClipped() || mailLink.isInDrawingArea(clip.getBounds())) {
     227                        FrameGraphics.PaintItem(mailLink);
     228                }
     229               
     230                // Paint the action item to check for new mail
     231                if (clip == null || clip.isNotClipped() || checkMailAction.isInDrawingArea(clip.getBounds())) {
     232                        FrameGraphics.PaintItem(checkMailAction);
    223233                }
    224234               
     
    226236        }
    227237       
    228         /** Syncs message bay size according to FrameGraphics max size. */
    229238        private static void updateSize() {
    230                 for (Item i : _previewMessages) {
     239                for (Item i: previewMessages) {
    231240                        if (i != null) {
    232241                                i.setOffset(0, -DisplayController.getMessageBayPaintArea().getMinY());
     
    234243                }
    235244               
    236                 _mailLink.setOffset(0, -DisplayController.getMessageBayPaintArea().getMinY());
    237245                updateLink();
    238246        }
    239247       
    240248        private static void updateLink() {
    241                 if (!isLinkInitialized && DisplayController.getFramePaintArea() != null
    242                                 && DisplayController.getFramePaintAreaWidth() > 0) {
    243                         // set up 'Messages' link on the right hand side
    244                         _mailLink.setPosition(DisplayController.getMessageBayPaintArea().getWidth() - MAIL_LINK_Y_OFFSET,
    245                                         MAIL_LINK_X);
    246                         _mailLink.setOffset(0, -DisplayController.getMessageBayPaintArea().getMinY());
    247                         isLinkInitialized = true;
    248                 } else {
    249                         _mailLink.setPosition(DisplayController.getMessageBayPaintArea().getWidth() - MAIL_LINK_Y_OFFSET,
    250                                         MAIL_LINK_X);
    251                 }
    252         }
    253 
    254         private static Text getHeader(Colour fontColor) {
    255                 Text t = new Text("You have [" + _messages.size() + "] unprocessed messages waiting.");
    256                 if (_messages.size() >= 2) {
    257                         t.setText(t.getText() + "  Two latest below:");
    258                 }
    259                 t.setPosition(OFFSET_X, HEADER_OFFSET_Y);
    260                 t.setOffset(0, -DisplayController.getFramePaintAreaHeight());
    261                 t.setColor(fontColor);
    262                 t.setFont(_messageFont.clone());
    263                 return t;
    264         }
    265        
    266         private static String getMessagePrefix() {
    267                 _messageCount++;
    268                 return "@" + _messageCount + ": ";
    269         }
    270        
    271         private static class Mail {
    272                 private String message;
    273                 private Map<String, String> options;
    274                 private String message2;
    275 
    276                 private Mail(String message, String message2, Map<String, String> options) {
    277                         this.message = message;
    278                         this.message2 = message2;
    279                         this.options = options;
    280                 }
    281                
    282                 private Text getPreviewMessage(boolean usePrefix) {
    283                         Text t = usePrefix ? new Text(getMessagePrefix() + message)     : new Text(message);
    284                         int y = MESSAGE_OFFSET_Y + _previewMessages.size() * SPACING;
    285                         t.setPosition(OFFSET_X, y);
    286                         t.setColor(Colour.BLACK);
    287                         t.setFont(_messageFont.clone());
    288                         return t;
    289                 }
    290                
    291                 private Text getDetailLine() {
    292                         if (message2 == null || message2.isEmpty()) {
    293                                 return null;
    294                         }
    295                         Text t = new Text(message2);
    296                         int y = MESSAGE_OFFSET_Y + _previewMessages.size() * SPACING;
    297                         t.setPosition(OFFSET_X, y);
    298                         t.setColor(Colour.BLACK);
    299                         t.setFont(_messageFont.clone());
    300                         return t;
    301                 }
    302                
    303                 private Text[] getMessage() {
    304                         List<Text> items = new LinkedList<Text>();
    305                         items.add(getPreviewMessage(false));
    306                         Text detail = getDetailLine();
    307                         if (detail != null) {
    308                                 items.add(detail);
     249                mailLink = new Text(-2, "See Mail", Colour.BLACK, Colour.WHITE);
     250                mailLink.setFont(MESSAGE_FONT);
     251                mailLink.setOffset(0, -DisplayController.getMessageBayPaintArea().getMinY());
     252                mailLink.setAnchorRight(OFFSET_X);
     253                mailLink.setAnchorTop(OFFSET_Y);
     254                mailLink.setLink(creator.getCurrent());
     255               
     256                checkMailAction = new Text(-2, "Get New Mail", Colour.BLACK, Colour.WHITE);
     257                checkMailAction.setFont(MESSAGE_FONT);
     258                checkMailAction.setOffset(0, -DisplayController.getMessageBayPaintArea().getMinY());
     259                checkMailAction.setAnchorRight(OFFSET_X);
     260                checkMailAction.setAnchorTop(OFFSET_Y + SPACING);
     261                checkMailAction.setAction("CheckForNewMail");
     262        }
     263
     264        private static Text getHeader() {
     265                Text header = new Text("You have [" + previewMessages.size() + "] new messages since last you check.");
     266                header.setPosition(OFFSET_X, OFFSET_Y);
     267                header.setOffset(0, -DisplayController.getFramePaintAreaHeight());
     268                header.setFont(MESSAGE_FONT);
     269                return header;
     270        }
     271
     272        private static Map<String, Integer> countBySender(List<MailEntry> newMail) {
     273                Map<String, Integer> bySenderCount = new HashMap<String, Integer>();
     274               
     275                for (MailEntry mail: newMail) {
     276                        String sender = mail.getSender();
     277                        if (mail.isSingleUseEncryption()) {
     278                                sender = "Encrypted Sender";
    309279                        }
    310280                       
    311                         int i = items.size();
    312                         for (String content: options.keySet()) {
    313                                 String[] split = content.split(":::");
    314                                 String action = options.get(content);
    315                                 Text t = new Text(split[0]);
    316                                 for (int o = 1; o < split.length; o++) {
    317                                         t.addToData(split[o]);
    318                                 }
    319                                 int y = MESSAGE_OFFSET_Y + (_previewMessages.size() + i) * SPACING;
    320                                 t.setPosition(OFFSET_X, y);
    321                                 t.setColor(Colour.BLUE);
    322                                 t.setFont(_messageFont.clone());
    323                                 t.setAction(action);
    324                                 items.add(t);
    325                                 i++;
    326                         }
    327                        
    328                         return items.toArray(new Text[] {});
    329                 }
     281                        if (bySenderCount.containsKey(sender)) {
     282                                Integer newValue = bySenderCount.get(sender) + 1;
     283                                bySenderCount.put(sender, newValue);
     284                        } else {
     285                                bySenderCount.put(sender, 1);
     286                        }
     287                }
     288               
     289                return bySenderCount;
    330290        }
    331291}
Note: See TracChangeset for help on using the changeset viewer.