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 }