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 }