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