1 module client.client;
2 
3 import core.thread : Thread;
4 import std.socket : Socket, AddressFamily, SocketType, ProtocolType,
5     parseAddress, Address, SocketOSException;
6 import bmessage;
7 import std.stdio;
8 import std.json;
9 import std.string;
10 import client.mail;
11 import server.server;
12 import std.conv : to;
13 import client.exceptions;
14 import std.file;
15 import std.exception;
16 import std.datetime.systime : Clock, SysTime;
17 import server.listener : ButterflyListener;
18 import gogga;
19 
20 public final class ButterflyClient : Thread
21 {
22     /**
23     * The associated listener
24     */
25     public ButterflyListener listener;
26 
27     /**
28     * Socket of the client connection
29     */
30     private Socket clientSocket;
31 
32     /**
33     * Whether or not the server is active
34     */
35     private bool active = true;
36 
37     /**
38     * The type of connection
39     */
40     private enum ClientType
41     {
42         SERVER,
43         CLIENT
44     }
45 
46     private ClientType connectionType;
47 
48     /**
49     * The Mailbox (if client) of the connected
50     * user.
51     */
52     public Mailbox mailbox;
53 
54     this(ButterflyListener listener, Socket clientSocket)
55     {
56         super(&run);
57         this.clientSocket = clientSocket;
58         this.listener = listener;
59     }
60 
61     private void run()
62     {
63         /* The received command block */
64         JSONValue commandBlock;
65 
66         /* The received bytes */
67         byte[] receivedBytes;
68 
69         /* The JSON response to be sent */
70         JSONValue responseBlock;
71         long status = 0;
72         string message;
73 
74         /**
75         * TODO: My error handling in bformat is not good.
76         * A dead connection sitll returns successful write.
77         */
78 
79         /* TODO: Implement loop read-write here */
80         while (active)
81         {
82             gprintln("Awaiting command from client...");
83 
84             /* Await a message from the client */
85             bool recvStatus = receiveMessage(clientSocket, receivedBytes);
86             gprintln(recvStatus);
87 
88             /* If the receive succeeded */
89             if (recvStatus)
90             {
91                 /* Reset the response JSON */
92                 responseBlock = JSONValue();
93                 message.length = 0;
94                 status = 0;
95 
96                 /* TODO: Add error handling catch for all JSON here */
97 
98                 try
99                 {
100                     /* Parse the incoming JSON */
101                     commandBlock = parseJSON(cast(string) receivedBytes);
102                     gprintln("Received response: " ~ commandBlock.toPrettyString());
103 
104                     /* Get the command */
105                     string command = commandBlock["command"].str();
106 
107                     /* TODO: Add command handling here */
108                     if (cmp(command, "authenticate") == 0)
109                     {
110                         /* Get the username and password */
111                         string authUsername = commandBlock["request"]["username"].str();
112                         string authPassword = commandBlock["request"]["password"].str();
113 
114                         /* TODO: Implement authentication */
115                         bool authStatus = authenticate(authUsername, authPassword);
116 
117                         if (authStatus)
118                         {
119                             /**
120                             * If the auth if successful then upgrade to
121                             * a client-type connection.
122                             */
123                             connectionType = ClientType.CLIENT;
124 
125                             /**
126                             * Set the user's associated Mailbox up
127                             */
128                             mailbox = new Mailbox(authUsername);
129                         }
130                         else
131                         {
132                             /* TODO: Error handling for authentication failure */
133                         }
134                     }
135                     /* TODO: Add command handling here */
136             else if (cmp(command, "register") == 0)
137                     {
138                         /* Get the username and password */
139                         string regUsername = commandBlock["request"]["username"].str();
140                         string regPassword = commandBlock["request"]["password"].str();
141 
142                         /* Attempt to register the new account */
143                         register(regUsername, regPassword);
144                     }
145                     else if (cmp(command, "sendMail") == 0)
146                     {
147                         /* Make sure the connection is from a client */
148                         if (connectionType == ClientType.CLIENT)
149                         {
150                             /* TODO: Implement me */
151 
152                             /* Get the mail block */
153                             JSONValue mailBlock = commandBlock["request"]["mail"];
154 
155                             /* Send the mail message */
156                             sendMail(mailBlock);
157                         }
158                         else
159                         {
160                             /* TODO: Add error handling */
161                         }
162                     }
163                     else if (cmp(command, "storeMail") == 0)
164                     {
165                         /* Make sure the connection is from a client */
166                         if (connectionType == ClientType.CLIENT)
167                         {
168                             /* Get the mail block */
169                             JSONValue mailBlock = commandBlock["request"]["mail"];
170 
171                             /* Get the folder to store the mail message in */
172                             Folder storeFolder = new Folder(mailbox,
173                                     commandBlock["request"]["folder"].str());
174 
175                             /* Store the message in the mailbox */
176                             Mail storedMail = storeMail(storeFolder, mailBlock);
177 
178                             /* Set the response to be the mail message's ID */
179                             JSONValue response;
180                             response["mailID"] = storedMail.getMailID();
181                             responseBlock["response"] = response;
182                         }
183                         else
184                         {
185                             /* TODO: Add error handling */
186                         }
187                     }
188                     else if (cmp(command, "editMail") == 0)
189                     {
190                         /* Make sure the connection is from a client */
191                         if (connectionType == ClientType.CLIENT)
192                         {
193                             /* Get the mail block */
194                             JSONValue mailBlock = commandBlock["request"]["mail"];
195 
196                             /* Get the folder the mail message wanting to be edited resides in */
197                             Folder storeFolder = new Folder(mailbox,
198                                     commandBlock["request"]["folder"].str());
199 
200                             /* Get the mail message wanting to be edited */
201                             Mail messageOriginal = new Mail(mailbox, storeFolder,
202                                     commandBlock["request"]["mailID"].str());
203 
204                             /* Update the message with the new data */
205                             Mail updatedMail = editMail(messageOriginal, storeFolder, mailBlock);
206 
207                             responseBlock["response"]["mailID"] = updatedMail.getMailID();
208                         }
209                         else
210                         {
211                             /* TODO: Add error handling */
212                         }
213                     }
214                     else if (cmp(command, "deliverMail") == 0)
215                     {
216                         /* Make sure the connection is from a server */
217                         if (connectionType == ClientType.SERVER)
218                         {
219                             /* Deliver the mail message from the remote host */
220                             deliverMail(commandBlock["request"]["mail"]);
221                         }
222                         else
223                         {
224                             /* TODO: Add error handling */
225                         }
226                     }
227                     else if (cmp(command, "fetchMail") == 0)
228                     {
229                         /* Make sure the connection is from a client */
230                         if (connectionType == ClientType.CLIENT)
231                         {
232                             /* The folder where the mail message is stored */
233                             Folder fetchFolder = new Folder(mailbox,
234                                     commandBlock["request"]["folder"].str());
235 
236                             /* The mail ID of the mail message */
237                             string mailID = commandBlock["request"]["id"].str();
238 
239                             /* Fetch the Mail */
240                             Mail fetchedMail = new Mail(mailbox, fetchFolder, mailID);
241 
242                             /* Set the response */
243                             JSONValue response;
244                             response["mail"] = fetchedMail.getMessage();
245                             responseBlock["response"] = response;
246                         }
247                         else
248                         {
249                             /* TODO: Add error handling */
250                         }
251                     }
252                     else if (cmp(command, "createFolder") == 0)
253                     {
254                         /* Make sure the connection is from a client */
255                         if (connectionType == ClientType.CLIENT)
256                         {
257                             /* Create the new folder */
258                             createFolder(commandBlock["request"]["folderName"].str());
259                         }
260                         else
261                         {
262                             /* TODO: Add error handling */
263                         }
264                     }
265                     else if (cmp(command, "deleteFolder") == 0)
266                     {
267                         /* Make sure the connection is from a client */
268                         if (connectionType == ClientType.CLIENT)
269                         {
270                             /* The folder to be deleted */
271                             Folder deleteFolder = new Folder(mailbox,
272                                     commandBlock["request"]["folder"].str());
273 
274                             /* Delete the folder */
275                             deleteFolder.deleteFolder();
276                         }
277                         else
278                         {
279                             /* TODO: Add error handling */
280                         }
281                     }
282                     else if (cmp(command, "deleteMail") == 0)
283                     {
284                         /* Make sure the connection is from a client */
285                         if (connectionType == ClientType.CLIENT)
286                         {
287                             /* The folder the mail wanting to be deleted resides in */
288                             Folder mailDirectory = new Folder(mailbox,
289                                     commandBlock["request"]["folder"].str());
290 
291                             /* The mail message to be deleted */
292                             Mail mailToDelete = new Mail(mailbox, mailDirectory,
293                                     commandBlock["request"]["mailID"].str());
294 
295                             mailToDelete.deleteMessage();
296                         }
297                         else
298                         {
299                             /* TODO: Add error handling */
300                         }
301                     }
302                     else if (cmp(command, "moveFolder") == 0)
303                     {
304                         /* Make sure the connection is from a client */
305                         if (connectionType == ClientType.CLIENT)
306                         {
307                             /* TODO: Implement me */
308                         }
309                         else
310                         {
311                             /* TODO: Add error handling */
312                         }
313                     }
314                     else if (cmp(command, "moveMail") == 0)
315                     {
316                         /* Make sure the connection is from a client */
317                         if (connectionType == ClientType.CLIENT)
318                         {
319                             /* The folder of the original mail message */
320                             Folder originalMessageFolder = new Folder(mailbox,
321                                     commandBlock["request"]["originalFolder"].str());
322 
323                             /* The original mail message */
324                             Mail originalMailMessage = new Mail(mailbox,
325                                     originalMessageFolder, commandBlock["request"]["mailID"].str());
326 
327                             /* The folder to move the mail message to */
328                             Folder newMailFolder = new Folder(mailbox,
329                                     commandBlock["request"]["newFolder"].str());
330 
331                             /* Move mail message */
332                             Mail newMail = moveMail(originalMessageFolder,
333                                     originalMailMessage, newMailFolder);
334 
335                             /* Set the response */
336                             JSONValue response;
337                             response["mailID"] = newMail.getMailID();
338                             responseBlock["response"] = response;
339                         }
340                         else
341                         {
342                             /* TODO: Add error handling */
343                         }
344                     }
345                     else if (cmp(command, "listMail") == 0)
346                     {
347                         /* Make sure the connection is from a client */
348                         if (connectionType == ClientType.CLIENT)
349                         {
350                             /* Get the folder wanting to be listed */
351                             Folder listFolder = new Folder(mailbox,
352                                     commandBlock["request"]["folderName"].str());
353 
354                             /* Write back an array of mailIDs */
355                             JSONValue response;
356                             response["mailIDs"] = parseJSON(to!(string)(listFolder.getMessages()));
357                             responseBlock["response"] = response;
358                         }
359                         else
360                         {
361                             /* TODO: Add error handling */
362                         }
363                     }
364                     else if (cmp(command, "listFolder") == 0)
365                     {
366                         /* Make sure the connection is from a client */
367                         if (connectionType == ClientType.CLIENT)
368                         {
369                             /* Get the folder wanting to be listed */
370                             Folder listFolder = new Folder(mailbox,
371                                     commandBlock["request"]["folderName"].str());
372 
373                             /* Write back an array of folder names */
374                             JSONValue response;
375                             response["folders"] = parseJSON(to!(string)(listFolder.getFolders()));
376                             responseBlock["response"] = response;
377                         }
378                         else
379                         {
380                             /* TODO: Add error handling */
381                         }
382                     }
383                     else if (cmp(command, "totsiens") == 0)
384                     {
385                         /* Close the connection on next loop condition check */
386                         active = false;
387                     }
388                     else
389                     {
390                         /* TODO: Add error handling for invalid commands */
391                     }
392                 }
393                 catch (JSONException e)
394                 {
395                     /* TODO: Set error message and status code */
396                     status = -2;
397                     message = e.msg;
398                 }
399                 catch (FileException e)
400                 {
401                     /* Status=-1 :: I/O error */
402                     status = -1;
403                     message = e.msg;
404                 }
405                 catch (ErrnoException e)
406                 {
407                     /* Status=-1 :: I/O error */
408                     status = -1;
409                     message = e.msg;
410                 }
411                 catch (ButterflyException e)
412                 {
413                     /* Set the status */
414                     status = e.status;
415                     message = e.msg;
416                 }
417 
418                 /* Generate the `status` block */
419                 JSONValue statusBlock;
420                 statusBlock["code"] = status;
421                 statusBlock["message"] = message;
422 
423                 /* Set the `status` field of the response block */
424                 responseBlock["status"] = statusBlock;
425 
426                 /* Write the response block to the client */
427                 gprintln("Writing back response: " ~ responseBlock.toPrettyString());
428                 bool sendStatus = sendMessage(clientSocket, cast(byte[]) toJSON(responseBlock));
429                 gprintln(sendStatus);
430 
431                 /* If there was an error writing the response back */
432                 if (!sendStatus)
433                 {
434                     /* End the session */
435                     gprintln("Response write back failed");
436                     break;
437                 }
438             }
439             else
440             {
441                 /**
442                 * If we failed to receive, then close the connection
443                 */
444                 break;
445             }
446         }
447 
448         gprintln("Closing session...");
449 
450         /* Close the socket */
451         clientSocket.close();
452     }
453 
454     /**
455 	 * Moves message from one folder, `srcFolder`, to another folder,
456 	 * `dstFolder`.
457 	 */
458     private Mail moveMail(Folder srcFolder, Mail ogMessage, Folder dstFolder)
459     {
460         /* Store a copy of the message in the destination folder `dstFolder` */
461         Mail newMessage = storeMail(dstFolder, ogMessage.getMessage());
462 
463         /* Delete the original message */
464         ogMessage.deleteMessage();
465 
466         return newMessage;
467     }
468 
469     /**
470     * Stores a mail message in the users Mailbox
471     * at in the given Folder, `folder`.
472     */
473     private Mail storeMail(Folder folder, JSONValue mailBlock)
474     {
475         /* Create the Mail message to store it */
476         Mail savedMail = Mail.createMail(mailbox, folder, mailBlock);
477 
478         return savedMail;
479     }
480 
481     /**
482      * Updates the given mail message in the
483      * provided folder with a new message.
484      */
485     private Mail editMail(Mail messageOriginal, Folder storeFolder, JSONValue mailBlock)
486     {
487         Mail updatedMail;
488 
489         /* Delete the old message */
490         messageOriginal.deleteMessage();
491 
492         /* Store the new message in the same folder */
493         updatedMail = Mail.createMail(mailbox, storeFolder, mailBlock);
494 
495         return updatedMail;
496     }
497 
498     private bool authenticate(string username, string password)
499     {
500         /* TODO: Implement me */
501 
502         return true;
503     }
504 
505     private void register(string username, string password)
506     {
507         /**
508         * Check if the account already exists.
509         * If it does then throw an exception.
510         */
511         if (exists("accounts/" ~ username))
512         {
513             /* Status=1 :: Account exists */
514             throw new ButterflyException(1);
515         }
516 
517         /* Create the mailbox for the new user */
518         Mailbox.createMailbox(username);
519 
520         /* Create the account */
521         /* TODO: Implement me */
522     }
523 
524     /**
525     * Create a folder in your mailbox
526     */
527     private Folder createFolder(string folderName)
528     {
529         /* Strip infront or behind slashes */
530         folderName = strip(folderName, "/");
531 
532         /* Seperated paths */
533         string[] seperatedPaths = split(folderName, "/");
534 
535         /* The newly created Folder */
536         Folder newFolder;
537 
538         /* If it is a base folder wanting to be created */
539         if (seperatedPaths.length)
540         {
541             newFolder = mailbox.addBaseFolder(folderName);
542         }
543         /* If it is a nested folder wanting to be created */
544         else
545         {
546             string folderPathExisting = folderName[0 .. lastIndexOf(folderName, "/")];
547             Folder endDirectoryExisting = new Folder(mailbox, folderPathExisting);
548             newFolder = endDirectoryExisting.createFolder(folderName[lastIndexOf(folderName,
549                         "/") + 1 .. folderName.length]);
550         }
551 
552         return newFolder;
553     }
554 
555     /**
556     * Given the address of the mail block, applying incoming
557     * mail filters to the mail message.
558     *
559     * Returns `true` if we are to outright reject this incoming
560     * mail.
561     */
562     private bool filterMailIncoming(JSONValue* mailBlock)
563     {
564         /* Add the received time stamp */
565         (*mailBlock)["receivedTimestamp"] = Clock.currTime().toString();
566 
567         /* TODO: Add plugin-based filtering here */
568         /* TODO: Filter using bester */
569 
570         /* TODO: Implement rejection */
571         return false;
572     }
573 
574     /**
575     * Delivers the mail to the local users
576     */
577     private void deliverMail(JSONValue mailBlock)
578     {
579         /* Filter the mail */
580         bool reject = filterMailIncoming(&mailBlock);
581 
582         /* Check to see if we must reject this mail */
583         if (reject)
584         {
585             /* TODO: Implement me */
586         }
587 
588         /* Get a list of the recipients of the mail message */
589         string[] recipients;
590         foreach (JSONValue recipient; mailBlock["recipients"].array())
591         {
592             recipients ~= recipient.str();
593         }
594 
595         /* Store the mail to each of the recipients */
596         foreach (string recipient; recipients)
597         {
598             /* Get the mail address */
599             string[] mailAddress = split(recipient, "@");
600 
601             /* Get the username */
602             string username = mailAddress[0];
603 
604             /* Get the domain */
605             string domain = mailAddress[1];
606 
607             /**
608             * Check if the domain of this recipient is this server
609             * or if it is a remote server.
610             */
611             if (cmp(domain, listener.getDomain()) == 0)
612             {
613                 gprintln("Storing mail message to " ~ recipient ~ " ...");
614 
615                 /* Get the Mailbox of a given user */
616                 Mailbox userMailbox = new Mailbox(username);
617 
618                 /* Get the Inbox folder */
619                 Folder inboxFolder = new Folder(userMailbox, "Inbox");
620 
621                 /* Store the message in their Inbox folder */
622                 Mail.createMail(userMailbox, inboxFolder, mailBlock);
623 
624                 gprintln("Stored mail message");
625             }
626         }
627     }
628 
629     /**
630     * Given the address of the mail block, applying outgoing
631     * mail filters to the mail message.
632     *
633     * Returns `true` if we are to outright reject this outgoing
634     * mail.
635     */
636     private bool filterMailOutgoing(JSONValue* mailBlock)
637     {
638         /* Add the from field to the mail block */
639         (*mailBlock)["from"] = mailbox.username ~ "@" ~ listener.getDomain();
640 
641         /* Add the sent time stamp */
642         (*mailBlock)["sentTimestamp"] = Clock.currTime().toString();
643 
644         /* TODO: Add plugin-based filtering here */
645         /* TODO: Filter using bester */
646 
647         /* TODO: Implement rejection */
648         return false;
649     }
650 
651     /**
652     * Sends the mail message `mail` to the servers
653     * listed in the recipients field.
654     */
655     public void sendMail(JSONValue mailBlock, bool placeInSentBox = true)
656     {
657         /* Filter the mail */
658         bool reject = filterMailOutgoing(&mailBlock);
659 
660         /* Check to see if we must reject this mail */
661         if (reject)
662         {
663             /* TODO: Implement me */
664         }
665 
666         /* Get a list of the recipients of the mail message */
667         string[] recipients;
668         foreach (JSONValue recipient; mailBlock["recipients"].array())
669         {
670             recipients ~= recipient.str();
671         }
672 
673         /* List of server's failed to deliver to */
674         string[] failedRecipients;
675 
676         /* List of remote recipients */
677         string[] remoteRecipients;
678 
679         /* Send the mail to each of the recipients */
680         foreach (string recipient; recipients)
681         {
682             gprintln("Sending mail message to " ~ recipient ~ " ...");
683 
684             /* Get the mail address */
685             string[] mailAddress = split(recipient, "@");
686 
687             /* Get the username */
688             string username = mailAddress[0];
689 
690             /* Get the domain */
691             string domain = mailAddress[1];
692 
693             /**
694             * Check if the domain of this recipient is this server
695             * or if it is a remote server.
696             */
697             if (listener.getServer().isLocalDomain(domain))
698             {
699                 gprintln("Local delivery occurring...");
700 
701                 /* TODO: Add failed delivery here too */
702                 if (!Mailbox.isMailbox(username))
703                 {
704                     /* Append failed recipient to array of failed recipients */
705                     failedRecipients ~= recipient;
706                     continue;
707                 }
708 
709                 /* Get the Mailbox of a given user */
710                 Mailbox userMailbox = new Mailbox(username);
711 
712                 /* Get the Inbox folder */
713                 Folder inboxFolder = new Folder(userMailbox, "Inbox");
714 
715                 /* Filter mail incoming (for local) */
716                 reject = filterMailIncoming(&mailBlock);
717 
718                 /* Check to see if we must reject this mail */
719                 if (reject)
720                 {
721                     /* TODO: Implement me */
722                 }
723 
724                 /* Store the message in their Inbox folder */
725                 Mail.createMail(userMailbox, inboxFolder, mailBlock);
726             }
727             else
728             {
729                 /* Tally up all non-local recipients for off-thread delivery */
730                 remoteRecipients ~= recipient;
731             }
732         }
733 
734         import client.sender : MailSender;
735 
736         /**
737         * Create a new MailSender for delivering remote mail
738         * off of this thread
739         */
740         MailSender remoteMailSender = new MailSender(remoteRecipients,
741                 mailBlock, failedRecipients, this);
742 
743         gprintln("Mail delivered (there may be remote mail delivery ongoing)");
744 
745         /* Store the message in this user's "Sent" folder */
746         if (placeInSentBox)
747         {
748             Folder sentFolder = new Folder(mailbox, "Sent");
749 
750             /* Store the message in their Sent folder */
751             Mail.createMail(mailbox, sentFolder, mailBlock);
752 
753             gprintln("Saved mail message to sent folder");
754         }
755     }
756 }