From 20d47dbe50c2abc54e9557a61b60825e32da18d7 Mon Sep 17 00:00:00 2001 From: Timothy Prepscius Date: Tue, 16 Jul 2013 22:27:16 -0400 Subject: [PATCH] added more files --- .gitignore | 46 ++ java/core/.classpath | 24 + java/core/.project | 17 + java/core/src/core/app/AppConstants.java | 170 ++++++ java/core/src/core/app/MailViewUtil.java | 58 ++ .../src/core/app/Mailiverse-AutoLaunch.plist | 18 + java/core/src/core/app/Mailiverse-AutoRun.vbs | 1 + .../app/Mailiverse-Integrate-MacMail.script | 13 + java/core/src/core/app/Updater.java | 65 ++ java/core/src/core/app/UpdaterGUI.java | 103 ++++ java/core/src/core/app/User.java | 43 ++ java/core/src/core/app/UserUtil.java | 209 +++++++ .../src/core/constants/ConstantsClient.java | 38 ++ .../constants/ConstantsClientPlatform.java | 26 + .../src/core/constants/ConstantsDropbox.java | 11 + .../constants/ConstantsEnvironmentKeys.java | 16 + .../src/core/constants/ConstantsMailJson.java | 34 ++ .../constants/ConstantsPushNotifications.java | 10 + java/core/src/core/constants/ConstantsS3.java | 10 + .../src/core/constants/ConstantsServer.java | 46 ++ .../src/core/constants/ConstantsSettings.java | 9 + .../src/core/constants/ConstantsStorage.java | 31 + .../src/core/constants/ConstantsVersion.java | 10 + .../key/auth/GetKeyServerEnvironment.java | 31 + .../src/core/key/auth/KeyServerAuthTest.java | 34 ++ .../core/key/auth/KeyServerAuthenticator.java | 137 +++++ .../auth/KeyServerAuthenticatorNoThread.java | 96 +++ .../key/auth/KeyServerAuthenticatorSync.java | 58 ++ .../key/auth/PutKeyServerEnvironment.java | 29 + .../core/key/server/KeyServerSessionDb.java | 71 +++ .../core/key/server/KeyServerUserSession.java | 58 ++ .../src/core/key/server/sql/KeyUserDb.java | 29 + .../streamserver/BogusSslContextFactory.java | 146 +++++ .../BogusTrustManagerFactory.java | 74 +++ .../key/streamserver/KeyStreamServerMain.java | 67 ++ .../key/streamserver/SRPProtocolHandler.java | 161 +++++ .../key/streamserver/SSLContextGenerator.java | 42 ++ .../mail/auth/GetMailServerEnvironment.java | 31 + .../core/mail/auth/MailServerAuthTest.java | 53 ++ .../mail/auth/MailServerAuthenticator.java | 273 +++++++++ .../auth/MailServerAuthenticatorNoThread.java | 188 ++++++ .../auth/MailServerAuthenticatorSync.java | 77 +++ .../mail/auth/PutMailServerEnvironment.java | 29 + java/core/src/core/mail/client/Actions.java | 170 ++++++ .../src/core/mail/client/ArrivalsMonitor.java | 16 + .../mail/client/ArrivalsMonitorDefault.java | 185 ++++++ .../core/mail/client/ArrivalsProcessor.java | 357 +++++++++++ .../src/core/mail/client/CacheManager.java | 344 +++++++++++ java/core/src/core/mail/client/Client.java | 137 +++++ java/core/src/core/mail/client/Constants.java | 30 + .../core/mail/client/CyclicalFileCheck.java | 119 ++++ .../src/core/mail/client/EventDispatcher.java | 62 ++ .../src/core/mail/client/EventPropagator.java | 104 ++++ java/core/src/core/mail/client/EventType.java | 13 + java/core/src/core/mail/client/Events.java | 52 ++ java/core/src/core/mail/client/Indexer.java | 435 +++++++++++++ .../src/core/mail/client/Initializer.java | 118 ++++ java/core/src/core/mail/client/Mailer.java | 154 +++++ java/core/src/core/mail/client/Master.java | 151 +++++ .../mail/client/MasterCacheSerializer.java | 44 ++ java/core/src/core/mail/client/Servent.java | 30 + java/core/src/core/mail/client/Store.java | 86 +++ .../core/mail/client/TrackingConnector.java | 158 +++++ .../src/core/mail/client/cache/Cache.java | 288 +++++++++ .../core/mail/client/cache/CacheFlush.java | 30 + .../core/mail/client/cache/CacheState.java | 12 + java/core/src/core/mail/client/cache/ID.java | 126 ++++ .../core/mail/client/cache/IndexedCache.java | 149 +++++ .../client/cache/IndexedCacheSerializer.java | 41 ++ .../core/src/core/mail/client/cache/Info.java | 287 +++++++++ .../core/src/core/mail/client/cache/Item.java | 89 +++ .../mail/client/cache/ItemCacheFactory.java | 32 + .../mail/client/cache/ItemCollection.java | 72 +++ .../core/mail/client/cache/ItemFactory.java | 10 + .../src/core/mail/client/cache/ItemOwner.java | 10 + .../mail/client/cache/ItemSerializer.java | 13 + .../mail/client/cache/ItemSerializerSync.java | 44 ++ .../core/src/core/mail/client/cache/JSON.java | 576 ++++++++++++++++++ .../src/core/mail/client/cache/LoadState.java | 13 + .../src/core/mail/client/cache/Operation.java | 25 + .../src/core/mail/client/cache/Store.java | 239 ++++++++ .../core/mail/client/cache/StoreFactory.java | 19 + .../core/mail/client/cache/StoreLibrary.java | 354 +++++++++++ .../mail/client/cache/StoreSerializer.java | 72 +++ .../core/src/core/mail/client/cache/Type.java | 17 + .../src/core/mail/client/cache/Version.java | 60 ++ .../core/mail/client/model/AddressBook.java | 122 ++++ .../core/mail/client/model/Attachment.java | 119 ++++ .../core/mail/client/model/Attachments.java | 126 ++++ .../core/src/core/mail/client/model/Body.java | 386 ++++++++++++ .../core/mail/client/model/ConstantsMisc.java | 12 + .../core/mail/client/model/Conversation.java | 190 ++++++ .../core/mail/client/model/Dictionary.java | 282 +++++++++ .../src/core/mail/client/model/Direction.java | 11 + .../src/core/mail/client/model/Folder.java | 66 ++ .../mail/client/model/FolderDefinition.java | 167 +++++ .../core/mail/client/model/FolderFilter.java | 67 ++ .../mail/client/model/FolderFilterSet.java | 18 + .../mail/client/model/FolderFilterSimple.java | 27 + .../core/mail/client/model/FolderMaster.java | 88 +++ .../core/mail/client/model/FolderPart.java | 155 +++++ .../mail/client/model/FolderRepository.java | 29 + .../src/core/mail/client/model/FolderSet.java | 246 ++++++++ .../src/core/mail/client/model/Header.java | 325 ++++++++++ .../src/core/mail/client/model/Identity.java | 187 ++++++ .../core/src/core/mail/client/model/Mail.java | 197 ++++++ .../src/core/mail/client/model/Model.java | 38 ++ .../core/mail/client/model/ModelFactory.java | 44 ++ .../mail/client/model/ModelSerializer.java | 86 +++ .../src/core/mail/client/model/Original.java | 78 +++ .../core/mail/client/model/Original.java.no | 149 +++++ .../core/mail/client/model/Recipients.java | 207 +++++++ .../src/core/mail/client/model/Settings.java | 77 +++ .../mail/client/model/TransportState.java | 142 +++++ .../client/model/UnregisteredIdentity.java | 25 + .../mail/core/MimeMessageRetainMessageId.java | 47 ++ .../src/core/mail/core/SMTPAuthenticator.java | 24 + java/core/src/core/mail/db/MailUserDb.java | 58 ++ .../AddNullIVsToExistingUserBlocks.java | 95 +++ .../mail/deploy/TransferDataToBase64.java | 181 ++++++ .../src/core/mail/deploy/sql/ANIV_finish.sql | 3 + .../src/core/mail/deploy/sql/ANIV_prepare.sql | 2 + .../core/mail/deploy/sql/ANIV_select_next.sql | 1 + .../src/core/mail/deploy/sql/ANIV_set.sql | 1 + .../src/core/mail/deploy/sql/Catalog.java | 12 + .../mail/deploy/sql/MAIL_EXTRA_create_db.sql | 11 + .../mail/deploy/sql/PAYMENT_create_db.sql | 12 + .../mail/deploy/sql/TDB64_create_column.sql | 2 + .../mail/deploy/sql/TDB64_finish_blocks.sql | 5 + .../core/mail/deploy/sql/TDB64_finish_vs.sql | 9 + .../mail/deploy/sql/TDB64_select_next.sql | 1 + .../core/mail/deploy/sql/TDB64_set_block.sql | 1 + .../src/core/mail/deploy/sql/TDB64_set_vs.sql | 1 + .../deploy/sql/TDB64_user_create_columns.sql | 3 + .../handler/DropboxGetAccessTokenPair.java | 44 ++ .../src/core/mail/handler/MailetHandler.java | 16 + .../mail/handler/MailetHandlerDefault.java | 119 ++++ .../mail/handler/MailetHandlerDropbox.java | 27 + .../core/mail/handler/MailetHandlerS3.java | 32 + .../core/mail/handler/UserInformation.java | 35 ++ .../mail/handler/UserInformationFactory.java | 62 ++ .../src/core/mail/push/ApplePushService.java | 175 ++++++ java/core/src/core/mail/push/PushDb.java | 51 ++ .../src/core/mail/push/PushDispatcher.java | 78 +++ java/core/src/core/mail/relay/LocalRelay.java | 128 ++++ .../core/mail/storage/AWSStorageCommon.java | 27 + .../core/mail/storage/AWSStorageCreation.java | 220 +++++++ .../core/mail/storage/AWSStorageDelete.java | 155 +++++ .../streamserver/BogusSslContextFactory.java | 146 +++++ .../BogusTrustManagerFactory.java | 74 +++ .../streamserver/MailServerSessionDb.java | 176 ++++++ .../streamserver/MailServerUserSession.java | 74 +++ .../streamserver/MailStreamServerMain.java | 52 ++ .../mail/streamserver/SRPProtocolHandler.java | 150 +++++ .../streamserver/SSLContextGenerator.java | 42 ++ .../src/core/mail/util/JavaMailToJSON.java | 290 +++++++++ .../src/core/mail/util/RemoveUnwritable.java | 13 + java/core/src/core/mail/util/ShowUser.java | 33 + .../org/timepedia/exporter/client/Export.java | 9 + .../timepedia/exporter/client/Exportable.java | 10 + .../timepedia/exporter/client/NoExport.java | 10 + 161 files changed, 14418 insertions(+) create mode 100644 .gitignore create mode 100644 java/core/.classpath create mode 100644 java/core/.project create mode 100644 java/core/src/core/app/AppConstants.java create mode 100644 java/core/src/core/app/MailViewUtil.java create mode 100644 java/core/src/core/app/Mailiverse-AutoLaunch.plist create mode 100644 java/core/src/core/app/Mailiverse-AutoRun.vbs create mode 100644 java/core/src/core/app/Mailiverse-Integrate-MacMail.script create mode 100644 java/core/src/core/app/Updater.java create mode 100644 java/core/src/core/app/UpdaterGUI.java create mode 100644 java/core/src/core/app/User.java create mode 100644 java/core/src/core/app/UserUtil.java create mode 100644 java/core/src/core/constants/ConstantsClient.java create mode 100644 java/core/src/core/constants/ConstantsClientPlatform.java create mode 100644 java/core/src/core/constants/ConstantsDropbox.java create mode 100644 java/core/src/core/constants/ConstantsEnvironmentKeys.java create mode 100644 java/core/src/core/constants/ConstantsMailJson.java create mode 100644 java/core/src/core/constants/ConstantsPushNotifications.java create mode 100644 java/core/src/core/constants/ConstantsS3.java create mode 100644 java/core/src/core/constants/ConstantsServer.java create mode 100644 java/core/src/core/constants/ConstantsSettings.java create mode 100644 java/core/src/core/constants/ConstantsStorage.java create mode 100644 java/core/src/core/constants/ConstantsVersion.java create mode 100644 java/core/src/core/key/auth/GetKeyServerEnvironment.java create mode 100644 java/core/src/core/key/auth/KeyServerAuthTest.java create mode 100644 java/core/src/core/key/auth/KeyServerAuthenticator.java create mode 100644 java/core/src/core/key/auth/KeyServerAuthenticatorNoThread.java create mode 100644 java/core/src/core/key/auth/KeyServerAuthenticatorSync.java create mode 100644 java/core/src/core/key/auth/PutKeyServerEnvironment.java create mode 100644 java/core/src/core/key/server/KeyServerSessionDb.java create mode 100644 java/core/src/core/key/server/KeyServerUserSession.java create mode 100644 java/core/src/core/key/server/sql/KeyUserDb.java create mode 100644 java/core/src/core/key/streamserver/BogusSslContextFactory.java create mode 100644 java/core/src/core/key/streamserver/BogusTrustManagerFactory.java create mode 100644 java/core/src/core/key/streamserver/KeyStreamServerMain.java create mode 100644 java/core/src/core/key/streamserver/SRPProtocolHandler.java create mode 100644 java/core/src/core/key/streamserver/SSLContextGenerator.java create mode 100644 java/core/src/core/mail/auth/GetMailServerEnvironment.java create mode 100644 java/core/src/core/mail/auth/MailServerAuthTest.java create mode 100644 java/core/src/core/mail/auth/MailServerAuthenticator.java create mode 100644 java/core/src/core/mail/auth/MailServerAuthenticatorNoThread.java create mode 100644 java/core/src/core/mail/auth/MailServerAuthenticatorSync.java create mode 100644 java/core/src/core/mail/auth/PutMailServerEnvironment.java create mode 100644 java/core/src/core/mail/client/Actions.java create mode 100644 java/core/src/core/mail/client/ArrivalsMonitor.java create mode 100644 java/core/src/core/mail/client/ArrivalsMonitorDefault.java create mode 100644 java/core/src/core/mail/client/ArrivalsProcessor.java create mode 100644 java/core/src/core/mail/client/CacheManager.java create mode 100644 java/core/src/core/mail/client/Client.java create mode 100644 java/core/src/core/mail/client/Constants.java create mode 100644 java/core/src/core/mail/client/CyclicalFileCheck.java create mode 100644 java/core/src/core/mail/client/EventDispatcher.java create mode 100644 java/core/src/core/mail/client/EventPropagator.java create mode 100644 java/core/src/core/mail/client/EventType.java create mode 100644 java/core/src/core/mail/client/Events.java create mode 100644 java/core/src/core/mail/client/Indexer.java create mode 100644 java/core/src/core/mail/client/Initializer.java create mode 100644 java/core/src/core/mail/client/Mailer.java create mode 100644 java/core/src/core/mail/client/Master.java create mode 100644 java/core/src/core/mail/client/MasterCacheSerializer.java create mode 100644 java/core/src/core/mail/client/Servent.java create mode 100644 java/core/src/core/mail/client/Store.java create mode 100644 java/core/src/core/mail/client/TrackingConnector.java create mode 100644 java/core/src/core/mail/client/cache/Cache.java create mode 100644 java/core/src/core/mail/client/cache/CacheFlush.java create mode 100644 java/core/src/core/mail/client/cache/CacheState.java create mode 100644 java/core/src/core/mail/client/cache/ID.java create mode 100644 java/core/src/core/mail/client/cache/IndexedCache.java create mode 100644 java/core/src/core/mail/client/cache/IndexedCacheSerializer.java create mode 100644 java/core/src/core/mail/client/cache/Info.java create mode 100644 java/core/src/core/mail/client/cache/Item.java create mode 100644 java/core/src/core/mail/client/cache/ItemCacheFactory.java create mode 100644 java/core/src/core/mail/client/cache/ItemCollection.java create mode 100644 java/core/src/core/mail/client/cache/ItemFactory.java create mode 100644 java/core/src/core/mail/client/cache/ItemOwner.java create mode 100644 java/core/src/core/mail/client/cache/ItemSerializer.java create mode 100644 java/core/src/core/mail/client/cache/ItemSerializerSync.java create mode 100644 java/core/src/core/mail/client/cache/JSON.java create mode 100644 java/core/src/core/mail/client/cache/LoadState.java create mode 100644 java/core/src/core/mail/client/cache/Operation.java create mode 100644 java/core/src/core/mail/client/cache/Store.java create mode 100644 java/core/src/core/mail/client/cache/StoreFactory.java create mode 100644 java/core/src/core/mail/client/cache/StoreLibrary.java create mode 100644 java/core/src/core/mail/client/cache/StoreSerializer.java create mode 100644 java/core/src/core/mail/client/cache/Type.java create mode 100644 java/core/src/core/mail/client/cache/Version.java create mode 100644 java/core/src/core/mail/client/model/AddressBook.java create mode 100644 java/core/src/core/mail/client/model/Attachment.java create mode 100644 java/core/src/core/mail/client/model/Attachments.java create mode 100644 java/core/src/core/mail/client/model/Body.java create mode 100644 java/core/src/core/mail/client/model/ConstantsMisc.java create mode 100644 java/core/src/core/mail/client/model/Conversation.java create mode 100644 java/core/src/core/mail/client/model/Dictionary.java create mode 100644 java/core/src/core/mail/client/model/Direction.java create mode 100644 java/core/src/core/mail/client/model/Folder.java create mode 100644 java/core/src/core/mail/client/model/FolderDefinition.java create mode 100644 java/core/src/core/mail/client/model/FolderFilter.java create mode 100644 java/core/src/core/mail/client/model/FolderFilterSet.java create mode 100644 java/core/src/core/mail/client/model/FolderFilterSimple.java create mode 100644 java/core/src/core/mail/client/model/FolderMaster.java create mode 100644 java/core/src/core/mail/client/model/FolderPart.java create mode 100644 java/core/src/core/mail/client/model/FolderRepository.java create mode 100644 java/core/src/core/mail/client/model/FolderSet.java create mode 100644 java/core/src/core/mail/client/model/Header.java create mode 100644 java/core/src/core/mail/client/model/Identity.java create mode 100644 java/core/src/core/mail/client/model/Mail.java create mode 100644 java/core/src/core/mail/client/model/Model.java create mode 100644 java/core/src/core/mail/client/model/ModelFactory.java create mode 100644 java/core/src/core/mail/client/model/ModelSerializer.java create mode 100644 java/core/src/core/mail/client/model/Original.java create mode 100644 java/core/src/core/mail/client/model/Original.java.no create mode 100644 java/core/src/core/mail/client/model/Recipients.java create mode 100644 java/core/src/core/mail/client/model/Settings.java create mode 100644 java/core/src/core/mail/client/model/TransportState.java create mode 100644 java/core/src/core/mail/client/model/UnregisteredIdentity.java create mode 100644 java/core/src/core/mail/core/MimeMessageRetainMessageId.java create mode 100644 java/core/src/core/mail/core/SMTPAuthenticator.java create mode 100644 java/core/src/core/mail/db/MailUserDb.java create mode 100644 java/core/src/core/mail/deploy/AddNullIVsToExistingUserBlocks.java create mode 100644 java/core/src/core/mail/deploy/TransferDataToBase64.java create mode 100644 java/core/src/core/mail/deploy/sql/ANIV_finish.sql create mode 100644 java/core/src/core/mail/deploy/sql/ANIV_prepare.sql create mode 100644 java/core/src/core/mail/deploy/sql/ANIV_select_next.sql create mode 100644 java/core/src/core/mail/deploy/sql/ANIV_set.sql create mode 100644 java/core/src/core/mail/deploy/sql/Catalog.java create mode 100644 java/core/src/core/mail/deploy/sql/MAIL_EXTRA_create_db.sql create mode 100644 java/core/src/core/mail/deploy/sql/PAYMENT_create_db.sql create mode 100644 java/core/src/core/mail/deploy/sql/TDB64_create_column.sql create mode 100644 java/core/src/core/mail/deploy/sql/TDB64_finish_blocks.sql create mode 100644 java/core/src/core/mail/deploy/sql/TDB64_finish_vs.sql create mode 100644 java/core/src/core/mail/deploy/sql/TDB64_select_next.sql create mode 100644 java/core/src/core/mail/deploy/sql/TDB64_set_block.sql create mode 100644 java/core/src/core/mail/deploy/sql/TDB64_set_vs.sql create mode 100644 java/core/src/core/mail/deploy/sql/TDB64_user_create_columns.sql create mode 100644 java/core/src/core/mail/handler/DropboxGetAccessTokenPair.java create mode 100644 java/core/src/core/mail/handler/MailetHandler.java create mode 100644 java/core/src/core/mail/handler/MailetHandlerDefault.java create mode 100644 java/core/src/core/mail/handler/MailetHandlerDropbox.java create mode 100644 java/core/src/core/mail/handler/MailetHandlerS3.java create mode 100644 java/core/src/core/mail/handler/UserInformation.java create mode 100644 java/core/src/core/mail/handler/UserInformationFactory.java create mode 100644 java/core/src/core/mail/push/ApplePushService.java create mode 100644 java/core/src/core/mail/push/PushDb.java create mode 100644 java/core/src/core/mail/push/PushDispatcher.java create mode 100644 java/core/src/core/mail/relay/LocalRelay.java create mode 100644 java/core/src/core/mail/storage/AWSStorageCommon.java create mode 100644 java/core/src/core/mail/storage/AWSStorageCreation.java create mode 100644 java/core/src/core/mail/storage/AWSStorageDelete.java create mode 100644 java/core/src/core/mail/streamserver/BogusSslContextFactory.java create mode 100644 java/core/src/core/mail/streamserver/BogusTrustManagerFactory.java create mode 100644 java/core/src/core/mail/streamserver/MailServerSessionDb.java create mode 100644 java/core/src/core/mail/streamserver/MailServerUserSession.java create mode 100644 java/core/src/core/mail/streamserver/MailStreamServerMain.java create mode 100644 java/core/src/core/mail/streamserver/SRPProtocolHandler.java create mode 100644 java/core/src/core/mail/streamserver/SSLContextGenerator.java create mode 100644 java/core/src/core/mail/util/JavaMailToJSON.java create mode 100644 java/core/src/core/mail/util/RemoveUnwritable.java create mode 100644 java/core/src/core/mail/util/ShowUser.java create mode 100644 java/core/src/core/org/timepedia/exporter/client/Export.java create mode 100644 java/core/src/core/org/timepedia/exporter/client/Exportable.java create mode 100644 java/core/src/core/org/timepedia/exporter/client/NoExport.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df8a012 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +lib/ + +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so +*.d + +*.jar +*.war +*.a +*.lib +*.exe + + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log +*.sqlite + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Icon? +ehthumbs.db +Thumbs.db diff --git a/java/core/.classpath b/java/core/.classpath new file mode 100644 index 0000000..6b2134c --- /dev/null +++ b/java/core/.classpath @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/core/.project b/java/core/.project new file mode 100644 index 0000000..ffcc9d0 --- /dev/null +++ b/java/core/.project @@ -0,0 +1,17 @@ + + + Mailiverse.Core + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/java/core/src/core/app/AppConstants.java b/java/core/src/core/app/AppConstants.java new file mode 100644 index 0000000..b1c716c --- /dev/null +++ b/java/core/src/core/app/AppConstants.java @@ -0,0 +1,170 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ + +package core.app; + +import java.io.IOException; +import java.net.URLDecoder; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import core.util.Streams; +import core.util.Strings; + + +public class AppConstants +{ + public static String + USER_HOME, + DOCUMENTS, + AUTORUNS, + RUNNING_JAR, + RUNNING_JAR_FILE, + RUNNING_JAR_DIRECTORY, + RUNNING_APP, + RUNNING_APP_PARENT, + PROGRAM_FILES, + DOCUMENTS_CLIENT, + DOCUMENTS_CLIENT_REDIRECT, + AUTORUN_SCRIPT, + OS; + + static final String CLIENT_JAR = "Mailiverse.jar"; + static final String SETUP_JAR = "Setup.jar"; + + static final String INTEGRATE_MACMAIL_SCRIPT = "Mailiverse-Integrate-MacMail.script"; + static final String QUERY_DIRECTORIES_VBSCRIPT = "QueryDirectories.vbs"; + + static final String AUTORUN_MAC = "Mailiverse-AutoLaunch.plist"; + static final String AUTORUN_WINDOWS = "Mailiverse-AutoRun.vbs"; + + public static final String HELP_PAGE = "http://www.mailiverse.com/help.html"; + public static final String MANUAL_S3 = "http://www.mailiverse.com//manual-s3.html"; + public static final String MANUAL_DROPBOX = "http://www.mailiverse.com/manual-s3.html"; + public static final String MANUAL_RSA = "http://www.mailiverse.com/manual-rsa.html"; + + + static { + queryDirectories(); + } + + public static String toOS (String app) + { + try + { + if (OS.startsWith("Windows")) + { + if (Pattern.matches("^/[a-zA-Z]:.*", app)) + app = app.substring(1); + + app = "\"" + URLDecoder.decode(app, "UTF-8").replace("/", "\\") + "\""; + } + } + catch (Exception e) + { + e.printStackTrace(); + } + + return app; + } + + static public void queryDirectories () + { + OS = System.getProperty ("os.name"); + + USER_HOME = System.getProperty("user.home"); + RUNNING_JAR = UserUtil.class.getProtectionDomain().getCodeSource().getLocation().getPath(); + + { + Pattern pattern = Pattern.compile("(.+)/(.+?)"); + Matcher matcher = pattern.matcher(RUNNING_JAR); + if (matcher.matches()) + { + RUNNING_JAR_DIRECTORY = matcher.group(1); + RUNNING_JAR_FILE = matcher.group(2); + } + } + + if (OS.startsWith("Mac")) + { + PROGRAM_FILES = "/Applications"; + DOCUMENTS = USER_HOME + "/Documents"; + AUTORUNS = USER_HOME + "/Library/LaunchAgents"; + + { + Pattern pattern = Pattern.compile("(.+)/Contents/Resources/Java/.+?"); + Matcher matcher = pattern.matcher(RUNNING_JAR); + if (matcher.matches()) + RUNNING_APP = matcher.group(1); + } + + { + Pattern pattern = Pattern.compile("(.+)/Contents/.+?/Contents/Resources/Java/.+?"); + Matcher matcher = pattern.matcher(RUNNING_JAR); + if (matcher.matches()) + RUNNING_APP_PARENT = matcher.group(1); + } + + AUTORUN_SCRIPT = AUTORUN_MAC; + } + else + if (OS.startsWith("Windows")) + { + try + { + Process p = Runtime.getRuntime().exec( + new String[] { "cscript", "//Nologo", toOS(RUNNING_JAR_DIRECTORY + "/" + QUERY_DIRECTORIES_VBSCRIPT) } + ); + String output = Streams.readFullyString(p.getInputStream(), "UTF-8"); + String[] lines = Strings.splitLines(output); + + DOCUMENTS = lines[0]; + AUTORUNS = lines[1]; + PROGRAM_FILES = lines[2]; + + RUNNING_APP = RUNNING_JAR; + } + catch (IOException e) + { + e.printStackTrace(); + } + + AUTORUN_SCRIPT = AUTORUN_WINDOWS; + } + else + { + // linux + PROGRAM_FILES = "/usr/local/bin"; + DOCUMENTS = USER_HOME + "/Documents"; + AUTORUNS = null; + RUNNING_APP = RUNNING_JAR; + AUTORUN_SCRIPT = null; + } + + DOCUMENTS_CLIENT = DOCUMENTS + "/Mailiverse"; + DOCUMENTS_CLIENT_REDIRECT = DOCUMENTS + "/Mailiverse.redirect"; + + /* + String[] strings = { + USER_HOME, + DOCUMENTS, + AUTORUNS, + RUNNING_JAR, + RUNNING_JAR_FILE, + RUNNING_JAR_DIRECTORY, + RUNNING_APP, + RUNNING_APP_PARENT, + PROGRAM_FILES, + DOCUMENTS_CLIENT, + DOCUMENTS_CLIENT_REDIRECT, + AUTORUN_SCRIPT, + OS + }; + + for (String string : strings) + System.out.println(string); + */ + } +} diff --git a/java/core/src/core/app/MailViewUtil.java b/java/core/src/core/app/MailViewUtil.java new file mode 100644 index 0000000..bdfb02c --- /dev/null +++ b/java/core/src/core/app/MailViewUtil.java @@ -0,0 +1,58 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ + +package core.app; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; + +import core.util.Environment; +import core.util.Streams; + + +public class MailViewUtil +{ + public static File createTempDirectory() + throws IOException + { + final File temp; + + temp = File.createTempFile("temp", Long.toString(System.nanoTime())); + + if(!(temp.delete())) + { + throw new IOException("Could not delete temp file: " + temp.getAbsolutePath()); + } + + if(!(temp.mkdir())) + { + throw new IOException("Could not create temp directory: " + temp.getAbsolutePath()); + } + + return (temp); + } + + public static void integrateOSXMail (String name) throws Exception + { + String script = + Streams.readFullyString( + MailViewUtil.class.getResourceAsStream(AppConstants.INTEGRATE_MACMAIL_SCRIPT), + "UTF-8" + ).replace("#NAME#", name); + + File temporary = new File(createTempDirectory().toString() + "/" + AppConstants.INTEGRATE_MACMAIL_SCRIPT); + + FileWriter writer = new FileWriter(temporary); + writer.write(script); + writer.close(); + + if (!temporary.exists()) + throw new Exception("Failed to write " + AppConstants.INTEGRATE_MACMAIL_SCRIPT); + + Runtime.getRuntime().exec( new String[] { "osascript", temporary.getAbsolutePath() } ); + } +} diff --git a/java/core/src/core/app/Mailiverse-AutoLaunch.plist b/java/core/src/core/app/Mailiverse-AutoLaunch.plist new file mode 100644 index 0000000..e87ab16 --- /dev/null +++ b/java/core/src/core/app/Mailiverse-AutoLaunch.plist @@ -0,0 +1,18 @@ + + + + + Label + Mailiverse + Program + #TARGET# + LimitLoadToSessionType + Aqua + RunAtLoad + + StandardErrorPath + /dev/null + StandardOutPath + /dev/null + + \ No newline at end of file diff --git a/java/core/src/core/app/Mailiverse-AutoRun.vbs b/java/core/src/core/app/Mailiverse-AutoRun.vbs new file mode 100644 index 0000000..9fbc40a --- /dev/null +++ b/java/core/src/core/app/Mailiverse-AutoRun.vbs @@ -0,0 +1 @@ +WScript.CreateObject( "WScript.Shell" ).Run("#TARGET#") diff --git a/java/core/src/core/app/Mailiverse-Integrate-MacMail.script b/java/core/src/core/app/Mailiverse-Integrate-MacMail.script new file mode 100644 index 0000000..14632a7 --- /dev/null +++ b/java/core/src/core/app/Mailiverse-Integrate-MacMail.script @@ -0,0 +1,13 @@ +tell application "Mail" + + set _password to "" + set _name to "#NAME#" + set _email to "#NAME#@mailiverse.com" + set _fullname to "#NAME#" + + set newacct to make new pop account with properties {name:"Mailiverse.#NAME#", user name:_name, password:_password, uses ssl:false, server name:"localhost", port:3110, full name:_fullname, email addresses:{_email}} + set addsmtp to make new smtp server with properties {name:"Mailiverse.#NAME#", server name:"localhost", uses ssl:false, port:3025, user name:_name, password:"", authentication:("axct" as constant)} + + set smtp server of newacct to addsmtp + +end tell \ No newline at end of file diff --git a/java/core/src/core/app/Updater.java b/java/core/src/core/app/Updater.java new file mode 100644 index 0000000..be8329c --- /dev/null +++ b/java/core/src/core/app/Updater.java @@ -0,0 +1,65 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ + +package core.app; + +import java.net.URL; +import java.net.URLConnection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +import core.constants.ConstantsClient; +import core.constants.ConstantsVersion; +import core.util.Streams; + + +public class Updater extends Thread +{ + static public final String VERSION_URL = "http://www.mailiverse.com/version/client"; + static public final String DESCRIPTION_URL = "http://www.mailiverse.com/version/client.description.txt"; + + public void run () + { + try + { + URL url = new URL(VERSION_URL); + URLConnection c = url.openConnection(); + String response = Streams.readFullyString(c.getInputStream(), "UTF-8"); + + response = response.replace("\n", " "); + Pattern pattern = Pattern.compile ("^(.+?),(.*)$"); + Matcher matcher = pattern.matcher(response); + + if (matcher.matches()) + { + String currentVersion = matcher.group(1); + String downloadURL = matcher.group(2); + + if (!currentVersion.equals(ConstantsVersion.CLIENT)) + { + String description = Streams.readFullyString(new URL(DESCRIPTION_URL).openConnection().getInputStream(), "UTF-8"); + UpdaterGUI.run(description, downloadURL); + } + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + public static Thread execute () + { + Thread thread = new Updater(); + thread.start(); + return thread; + } + + public static void main (String[] args) throws InterruptedException + { + execute().join(); + } +} diff --git a/java/core/src/core/app/UpdaterGUI.java b/java/core/src/core/app/UpdaterGUI.java new file mode 100644 index 0000000..9cf963d --- /dev/null +++ b/java/core/src/core/app/UpdaterGUI.java @@ -0,0 +1,103 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ + +package core.app; + +import java.awt.BorderLayout; +import java.awt.EventQueue; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import javax.swing.SpringLayout; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JButton; +import java.awt.event.ActionListener; +import java.awt.event.ActionEvent; + +public class UpdaterGUI extends JFrame +{ + + private JPanel contentPane; + String description; + String url; + private JTextArea txtrDescription; + + /** + * Launch the application. + */ + public static void main(String[] args) + { + run("None", "None"); + } + + public static void run (final String description, final String url) + { + EventQueue.invokeLater(new Runnable() { + public void run() + { + try + { + UpdaterGUI frame = new UpdaterGUI(); + frame.bind(description, url); + frame.setVisible(true); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + }); + } + + /** + * Create the frame. + */ + public UpdaterGUI() + { + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setBounds(100, 100, 450, 300); + contentPane = new JPanel(); + contentPane.setBorder(new EmptyBorder(5, 5, 5, 5)); + setContentPane(contentPane); + SpringLayout sl_contentPane = new SpringLayout(); + contentPane.setLayout(sl_contentPane); + + JButton btnOpenDownloadsPage = new JButton("Open Downloads Page"); + btnOpenDownloadsPage.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + onOpen(); + } + }); + sl_contentPane.putConstraint(SpringLayout.HORIZONTAL_CENTER, btnOpenDownloadsPage, 0, SpringLayout.HORIZONTAL_CENTER, contentPane); + sl_contentPane.putConstraint(SpringLayout.SOUTH, btnOpenDownloadsPage, -5, SpringLayout.SOUTH, contentPane); + contentPane.add(btnOpenDownloadsPage); + + JScrollPane scrollPane = new JScrollPane(); + sl_contentPane.putConstraint(SpringLayout.NORTH, scrollPane, 5, SpringLayout.NORTH, contentPane); + sl_contentPane.putConstraint(SpringLayout.WEST, scrollPane, 5, SpringLayout.WEST, contentPane); + sl_contentPane.putConstraint(SpringLayout.EAST, scrollPane, -5, SpringLayout.EAST, contentPane); + sl_contentPane.putConstraint(SpringLayout.SOUTH, scrollPane, -5, SpringLayout.NORTH, btnOpenDownloadsPage); + contentPane.add(scrollPane); + + txtrDescription = new JTextArea(); + txtrDescription.setText("Description"); + scrollPane.setViewportView(txtrDescription); + } + + public void bind (String description, String url) + { + this.description = description; + txtrDescription.setText(description); + + this.url = url; + } + + public void onOpen () + { + UserUtil.openPageInDefaultBrowser(url); + } +} diff --git a/java/core/src/core/app/User.java b/java/core/src/core/app/User.java new file mode 100644 index 0000000..0d07c83 --- /dev/null +++ b/java/core/src/core/app/User.java @@ -0,0 +1,43 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ + +package core.app; + +import core.constants.ConstantsClient; +import core.constants.ConstantsEnvironmentKeys; +import core.constants.ConstantsServer; +import core.crypt.CryptorRSA; +import core.crypt.CryptorRSAAES; +import core.crypt.CryptorRSAFactoryEnvironment; +import core.exceptions.CryptoException; +import core.util.Environment; + +public class User +{ + public User (Environment environment) throws Exception + { + this.environment = environment; + this.clientEnvironment = environment.childEnvironment(ConstantsEnvironmentKeys.CLIENT_ENVIRONMENT); + this.name = clientEnvironment.checkGet(ConstantsEnvironmentKeys.CLIENT_NAME); + this.email = name + ConstantsClient.ATHOST; + this.password = clientEnvironment.checkGet(ConstantsEnvironmentKeys.CLIENT_PASSWORD); + this.cryptor = new CryptorRSAAES(CryptorRSAFactoryEnvironment.create(clientEnvironment)); + } + + public String name; + public String email; + public String password; + public Environment environment; + public Environment clientEnvironment; + public CryptorRSAAES cryptor; + + public boolean alreadyEnsuredDirectories = false; + + public void authenticate (String name, String password) throws Exception + { + if (!this.name.equals(name) || !this.password.equals(password) ) + throw new Exception ("Authentication failure"); + } +} diff --git a/java/core/src/core/app/UserUtil.java b/java/core/src/core/app/UserUtil.java new file mode 100644 index 0000000..3c606eb --- /dev/null +++ b/java/core/src/core/app/UserUtil.java @@ -0,0 +1,209 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ + +package core.app; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import core.util.Streams; + + +public class UserUtil +{ + static public String getConfigurationDirectory () throws Exception + { + File redirect = new File(AppConstants.DOCUMENTS_CLIENT_REDIRECT); + if (redirect.exists() && redirect.isFile()) + return Streams.readFullyString(new FileInputStream(redirect.getPath()), "UTF-8").trim(); + + return AppConstants.DOCUMENTS_CLIENT; + } + + static public boolean hasConfigurationDirectory () throws Exception + { + File configurationDirectory = new File(getConfigurationDirectory()); + return configurationDirectory.exists(); + } + + /* + static public void writeConfiguration (String user, String password, Environment e) throws Exception + { + PBE pbe = new PBE(password.toCharArray(), PBE.DEFAULT_SALT_2, PBE.DEFAULT_ITERATIONS, PBE.DEFAULT_KEYLENGTH); + + File configurationDirectory = new File(getConfigurationDirectory()); + + if (!configurationDirectory.exists()) + { + if (!configurationDirectory.mkdirs()) + throw new Exception ("Unable to create directory for Client configuration."); + } + + Environment.toStore(new EncryptedZipFileConnector(pbe, getConfigurationDirectory() + "/" + user + ".mv1"), e); + } + + static public Environment readConfiguration (String user, String password) + { + try + { + PBE pbe = new PBE(password.toCharArray(), PBE.DEFAULT_SALT_2, PBE.DEFAULT_ITERATIONS, PBE.DEFAULT_KEYLENGTH); + + Environment e = Environment.fromStore( + new EncryptedZipFileConnector(pbe, getConfigurationDirectory() + "/" + user + ".mv1") + ); + + if (!e.containsKey(ConstantsEnvironmentKeys.VERSION)) + return null; + + return e; + } + catch (Exception e) + { + e.printStackTrace(); + return null; + } + } + */ + + public static void openPageInDefaultBrowser(String url) + { + try + { + java.awt.Desktop.getDesktop().browse(java.net.URI.create(url)); + } + catch (java.io.IOException e) + { + System.out.println(e.getMessage()); + } + } + + public static void startClientFromSetup () + { + try + { + if (AppConstants.OS.startsWith("Mac")) + { + String app = AppConstants.toOS(AppConstants.RUNNING_APP_PARENT); + System.out.println("Attempting to start " + app); + Runtime.getRuntime().exec( new String[] { "open", app, "--args", "--install-autorun" } ); + } + else + { + String app = AppConstants.toOS(AppConstants.RUNNING_JAR_DIRECTORY + "/" + AppConstants.CLIENT_JAR); + System.out.println("Attempting to start " + app); + Runtime.getRuntime().exec( new String[] { "java", "-jar", app, "--install-autorun" } ); + } + } + catch (IOException e) + { + e.printStackTrace(); + } + + } + + public static void startSetupFromClient () + { + try + { + if (AppConstants.OS.startsWith("Mac")) + { + String app = AppConstants.toOS(AppConstants.RUNNING_APP + "/Contents/Setup.app"); + System.out.println("Attempting to start " + app); + Runtime.getRuntime().exec( new String[] { "open", app } ); + } + else + { + String app = AppConstants.toOS(AppConstants.RUNNING_JAR_DIRECTORY + "/" + AppConstants.SETUP_JAR); + System.out.println("Attempting to start " + app); + Runtime.getRuntime().exec( new String[] { "java", "-jar", app } ); + } + } + catch (IOException e) + { + e.printStackTrace(); + } + + } + + public static void enableAutoLaunch (String app, boolean enable) throws IOException + { + if (AppConstants.OS.startsWith("Linux")) + return; + + String launchFileName = AppConstants.AUTORUNS + "/" + AppConstants.AUTORUN_SCRIPT; + + if (enable) + { + System.out.println("Attempting to create autorun " + launchFileName + " pointing to " + app); + + File directory = new File(AppConstants.AUTORUNS); + if (!directory.exists()) + directory.mkdirs(); + + String launchString = + Streams.readFullyString( + UserUtil.class.getResourceAsStream(AppConstants.AUTORUN_SCRIPT), + "UTF-8" + ); + + if (AppConstants.OS.startsWith("Mac")) + app = app + "/Contents/MacOS/JavaApplicationStub"; + if (AppConstants.OS.startsWith("Windows")) + app = app.replace("\\", "\\\\").replace("\"", "\"\""); + + launchString = launchString.replace("#TARGET#", app); + + FileOutputStream fos = new FileOutputStream(launchFileName); + PrintWriter writer = new PrintWriter(fos); + writer.write(launchString); + writer.close(); + } + else + { + System.out.println("Attempting to delete autorun " + launchFileName); + + File f = new File (launchFileName); + if (f.exists()) + f.delete(); + } + } + + public static boolean isAutoLaunchEnabled () + { + if (AppConstants.OS.startsWith("Linux")) + return false; + + String launchFileName = AppConstants.AUTORUNS + "/" + AppConstants.AUTORUN_SCRIPT; + + File f = new File (launchFileName); + return f.exists(); + } + + public static void bringApplicationToFront () + { + if (AppConstants.OS.startsWith("Mac")) + { + try + { + String application = "Mailiverse"; + System.out.println("Attempting to bring application to front via applescript."); + + Runtime.getRuntime().exec ( + new String[] { + "osascript", + "-e", + "tell application \"" + application + "\" to activate" + } + ); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + } +} diff --git a/java/core/src/core/constants/ConstantsClient.java b/java/core/src/core/constants/ConstantsClient.java new file mode 100644 index 0000000..1fa738e --- /dev/null +++ b/java/core/src/core/constants/ConstantsClient.java @@ -0,0 +1,38 @@ +package core.constants; + +public class ConstantsClient +{ + public static final String + HOST = ConstantsClientPlatform.HOST, + TOMCAT_HOST = ConstantsClientPlatform.TOMCAT_HOST, + AUTH_HOST = ConstantsClientPlatform.AUTH_HOST, + WEB_HOST = ConstantsClientPlatform.WEB_HOST; + + public static final String ATHOST = "@" + HOST; + public static final String WEB_SERVER_URL = "https://" + WEB_HOST; + + public static final String SERVER_TOMCAT = TOMCAT_HOST + "/Mailiverse/"; + public static final String WEB_SERVER_TOMCAT = "https://" + SERVER_TOMCAT; + + public static final String KEY_AUTH_HOST = AUTH_HOST; + public static final int KEY_AUTH_PORT = 7000; + + public static final String MAIL_AUTH_HOST = AUTH_HOST; + public static final int MAIL_AUTH_PORT = 7001; + + public static final String MAIL_SERVER_WEBSOCKET = "wss://" + SERVER_TOMCAT + "MailServer"; + public static final String KEY_SERVER_WEBSOCKET = "wss://" + SERVER_TOMCAT + "KeyServer"; + + // Mailiverse + public static final String DROPBOX_APPKEY = "YOUR_APPKEY"; + public static final String DROPBOX_APPSECRET = "YOUR_APPSECRET"; + + public static final int MAXIMUM_MAIL_SIZE = 1024 * 1024 * 1; + public static final String WEB_AUTHORIZED_URL = WEB_SERVER_URL + "/DropboxAuthorized.html"; + + public static final String DROPBOX_AUTH_URL = + "https://www.dropbox.com/1/oauth/authorize" + + "?oauth_token=REQUEST_TOKEN_KEY" + + "&oauth_callback=" + WEB_AUTHORIZED_URL + + "&locale=en"; +} diff --git a/java/core/src/core/constants/ConstantsClientPlatform.java b/java/core/src/core/constants/ConstantsClientPlatform.java new file mode 100644 index 0000000..fc8df98 --- /dev/null +++ b/java/core/src/core/constants/ConstantsClientPlatform.java @@ -0,0 +1,26 @@ +package core.constants; + +public class ConstantsClientPlatform +{ + public static final boolean DEBUG = false; + + public static final String HOST, AUTH_HOST, TOMCAT_HOST, WEB_HOST; + + static + { + if (DEBUG) + { + HOST = "mailiverse.com"; + AUTH_HOST = "red"; + TOMCAT_HOST = "YOUR_DEV_TOMCAT:8080"; + WEB_HOST = "YOUR_DEV_WEB:8000"; + } + else + { + HOST = "mailiverse.com"; + AUTH_HOST = "mail.mailiverse.com"; + TOMCAT_HOST = "mail.mailiverse.com"; + WEB_HOST = "www.mailiverse.com"; + } + } +} diff --git a/java/core/src/core/constants/ConstantsDropbox.java b/java/core/src/core/constants/ConstantsDropbox.java new file mode 100644 index 0000000..2f7073c --- /dev/null +++ b/java/core/src/core/constants/ConstantsDropbox.java @@ -0,0 +1,11 @@ +package core.constants; + +public class ConstantsDropbox { + + public static final String + DropboxAppKey = "DropboxAppKey", + DropboxAppSecret = "DropboxAppSecret", + DropboxTokenKey = "DropboxTokenKey", + DropboxTokenSecret = "DropboxTokenSecret", + DropboxUserPrefix = "DropboxUserPrefix"; +} diff --git a/java/core/src/core/constants/ConstantsEnvironmentKeys.java b/java/core/src/core/constants/ConstantsEnvironmentKeys.java new file mode 100644 index 0000000..375864e --- /dev/null +++ b/java/core/src/core/constants/ConstantsEnvironmentKeys.java @@ -0,0 +1,16 @@ +package core.constants; + +public class ConstantsEnvironmentKeys +{ + public static String HANDLER = "handler"; + public static String IDENTITY = "identity"; + public static String SMTP_PASSWORD = "smtpPassword"; + public static final String PUBLIC_ENCRYPTION_KEY = "RSA-PublicKey"; + public static final String PRIVATE_DECRYPTION_KEY = "RSA-PrivateKey"; + public static final String CLIENT_NAME = "name"; + public static final String CLIENT_PASSWORD = "password"; + public static final String CONFIGURATION_VERSION = "version"; + public static final String VERSION = "version"; + public static final String CLIENT_ENVIRONMENT = "client"; + public static final String SERVER_ENVIRONMENT = "server"; +} diff --git a/java/core/src/core/constants/ConstantsMailJson.java b/java/core/src/core/constants/ConstantsMailJson.java new file mode 100644 index 0000000..5cdafcb --- /dev/null +++ b/java/core/src/core/constants/ConstantsMailJson.java @@ -0,0 +1,34 @@ +package core.constants; + +public class ConstantsMailJson +{ + public static final String + Original = "original", + + To = "to", + Cc = "cc", + Bcc = "bcc", + From = "from", + ReplyTo = "reply-to", + UIDL = "uidl", + + Class = "class", + Value = "value", + Headers = "headers", + Addresses = "addresses", + Content = "content", + + String = "string", + Bytes = "bytes", + MultiPart = "part", + Unknown = "unknown", + + Dates = "dates", + Received = "received", + Sent= "sent", + Written = "written", + + Name = "name", + Email = "email", + EmbeddedCryptors = "cryptors"; +} diff --git a/java/core/src/core/constants/ConstantsPushNotifications.java b/java/core/src/core/constants/ConstantsPushNotifications.java new file mode 100644 index 0000000..90ffdcd --- /dev/null +++ b/java/core/src/core/constants/ConstantsPushNotifications.java @@ -0,0 +1,10 @@ +package core.constants; + +public class ConstantsPushNotifications +{ + public static final String + USER = "user", + NOTIFICATION_TYPE = "notification_type", + DEVICE_TYPE = "device_type", + DEVICE_ID = "device_id"; +} diff --git a/java/core/src/core/constants/ConstantsS3.java b/java/core/src/core/constants/ConstantsS3.java new file mode 100644 index 0000000..165094d --- /dev/null +++ b/java/core/src/core/constants/ConstantsS3.java @@ -0,0 +1,10 @@ +package core.constants; + +public class ConstantsS3 { + + public static final String + AWSAccessKeyId = "AWSAccessKeyId", + AWSSecretKey = "AWSSecretKey", + AWSBucketName = "AWSBucketName", + AWSBucketRegion = "AWSBucketRegion"; +} diff --git a/java/core/src/core/constants/ConstantsServer.java b/java/core/src/core/constants/ConstantsServer.java new file mode 100644 index 0000000..e4ffc7f --- /dev/null +++ b/java/core/src/core/constants/ConstantsServer.java @@ -0,0 +1,46 @@ +package core.constants; + +public class ConstantsServer +{ + public static final boolean DEBUG = System.getenv("PRODUCTION")==null; + + public static final String LOCAL_MAIL_SERVER, DBCONNECTION_PREFIX; + public static final String KEY_SERVER; + + static + { + if (DEBUG) + { + System.out.println("Running DEBUG Mode"); + + LOCAL_MAIL_SERVER = "red"; + DBCONNECTION_PREFIX = "jdbc:mysql://red/"; + KEY_SERVER = "red"; + } + else + { + System.out.println("Running PRODUCTION Mode"); + + KEY_SERVER = "localhost"; + + // the mail server has to be the full name, or else the SSL certificate fails + LOCAL_MAIL_SERVER = "mail.mailiverse.com"; + DBCONNECTION_PREFIX = "jdbc:mysql://localhost/"; + } + } + + public static final String SMTP_HOST = LOCAL_MAIL_SERVER; + public static final int SMTP_PORT = 25; + + public static final String LOCAL_SMTP_HOST = "YOUR_LOCAL_SMTP_HOST"; + public static final String LOCAL_SMTP_PORT = "10025"; + + public static final String KEY_AUTH_HOST = KEY_SERVER; + public static final int KEY_AUTH_PORT = 7000; + + public static final String MAIL_AUTH_HOST = KEY_SERVER; + public static final int MAIL_AUTH_PORT = 7001; + public static final int MAXIMUM_MAIL_SIZE = 1024 * 1024 * 1; + + public static final int AUTH_TIMEOUT = 45; +} diff --git a/java/core/src/core/constants/ConstantsSettings.java b/java/core/src/core/constants/ConstantsSettings.java new file mode 100644 index 0000000..db78c66 --- /dev/null +++ b/java/core/src/core/constants/ConstantsSettings.java @@ -0,0 +1,9 @@ +package core.constants; + +public class ConstantsSettings +{ + public static final String + USERNAME = "name", + SIGNATURE = "signature"; + +} diff --git a/java/core/src/core/constants/ConstantsStorage.java b/java/core/src/core/constants/ConstantsStorage.java new file mode 100644 index 0000000..c831031 --- /dev/null +++ b/java/core/src/core/constants/ConstantsStorage.java @@ -0,0 +1,31 @@ +package core.constants; + +public class ConstantsStorage +{ + public final static String + HANDLER_DROPBOX = "DB", + HANDLER_S3 = "S3"; + + public final static String + IN = "In", + OUT = "Out", + JSON = "_Json", + + NEW = "Mail", + NEW_IN = NEW + "/" + IN, + NEW_OUT = NEW + "/" + OUT, + + NEW_IN_JSON = NEW_IN + JSON, + NEW_OUT_JSON = NEW_OUT + JSON, + + CACHE = "Cache", + CACHE_PREFIX = CACHE + "/"; + + public static int LARGE_MESSAGE_SIZE = 20 * 1024; + + public static final int FLUSH_LOCK_TIME_SECONDS = 10; + public static final int FLUSH_LOCK_TIME_ALLOWED_BEFORE_RELOCK_SECONDS = 5; + + public static final int MAIL_CHECK_LOCK_TIME_SECONDS = 120; + public static final int MAIL_CHECK_LOCK_TIME_ALLOWED_BEFORE_RELOCK_SECONDS = 60; +} diff --git a/java/core/src/core/constants/ConstantsVersion.java b/java/core/src/core/constants/ConstantsVersion.java new file mode 100644 index 0000000..dbad585 --- /dev/null +++ b/java/core/src/core/constants/ConstantsVersion.java @@ -0,0 +1,10 @@ +package core.constants; + +public class ConstantsVersion +{ + static public final String + APP_NAME = "Mailiverse", + LOGIN = "2.0", + CONFIGURATION = "2.0", + CLIENT = "0.3"; +} diff --git a/java/core/src/core/key/auth/GetKeyServerEnvironment.java b/java/core/src/core/key/auth/GetKeyServerEnvironment.java new file mode 100644 index 0000000..0affd9a --- /dev/null +++ b/java/core/src/core/key/auth/GetKeyServerEnvironment.java @@ -0,0 +1,31 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package key.auth; + +import java.io.FileWriter; + +import core.util.Environment; +import core.util.JSONSerializer; + +public class GetKeyServerEnvironment +{ + public static void main (String[] args) throws Exception + { + KeyServerAuthenticatorSync auth = new KeyServerAuthenticatorSync(); + + if (args.length != 3) + { + System.out.println("Arguments: name password outFile"); + throw new IllegalArgumentException(); + } + + Environment e = auth.get(args[0], args[1], null); + + FileWriter writer = new FileWriter(args[2]); + writer.write(new String(JSONSerializer.serialize(e))); + writer.flush(); + writer.close(); + } +} diff --git a/java/core/src/core/key/auth/KeyServerAuthTest.java b/java/core/src/core/key/auth/KeyServerAuthTest.java new file mode 100644 index 0000000..f3f4f00 --- /dev/null +++ b/java/core/src/core/key/auth/KeyServerAuthTest.java @@ -0,0 +1,34 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package key.auth; + +import core.constants.ConstantsEnvironmentKeys; +import core.crypt.CryptorRSA; +import core.crypt.CryptorRSAAES; +import core.crypt.CryptorRSAFactory; +import core.crypt.CryptorRSAFactoryEnvironment; +import core.util.Environment; +import core.util.Strings; + +public class KeyServerAuthTest +{ + public static void main (String[] args) throws Exception + { + KeyServerAuthenticatorSync key = new KeyServerAuthenticatorSync(); + + Environment e = key.get("USERXXXX@mailiverse.com", "PASSWORD", null); + for (String k : e.keySet()) + { + System.out.format("%s -> %s\n", k, e.get(k)); + } + + Environment client = e.childEnvironment(ConstantsEnvironmentKeys.CLIENT_ENVIRONMENT); + CryptorRSA cryptorRSA = CryptorRSAFactoryEnvironment.create (client); + CryptorRSAAES cryptorRSAAES = new CryptorRSAAES(cryptorRSA); + + byte[] bytes = cryptorRSAAES.encrypt(Strings.toBytes("This message was encrypted and decrypted?")); + System.out.println(Strings.toString(cryptorRSAAES.decrypt(bytes))); + } +} diff --git a/java/core/src/core/key/auth/KeyServerAuthenticator.java b/java/core/src/core/key/auth/KeyServerAuthenticator.java new file mode 100644 index 0000000..7eb4045 --- /dev/null +++ b/java/core/src/core/key/auth/KeyServerAuthenticator.java @@ -0,0 +1,137 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package key.auth; + +import core.client.ClientUserSession; +import core.client.messages.Get; +import core.client.messages.Put; +import core.client.messages.Response; +import core.constants.ConstantsClient; +import core.constants.ConstantsServer; +import core.crypt.Cryptor; +import core.io.IoChain; +import core.io.IoChainBase64; +import core.io.IoChainNewLinePackets; +import core.io.IoChainSocket; +import core.io.IoChainThread; +import core.crypt.KeyPairFromPasswordCryptor; +import core.crypt.KeyPairFromPassword; +import core.srp.client.SRPClientListener; +import core.srp.client.SRPClientUserSession; +import core.callback.Callback; +import core.util.Environment; +import core.util.JSONSerializer; +import core.util.Streams; +import core.util.Zip; +import core.callback.CallbackDefault; +import core.callbacks.IoStop; + +public class KeyServerAuthenticator +{ + public static final int DEFAULT_PORT = ConstantsClient.KEY_AUTH_PORT; + public static final String DEFAULT_HOST = ConstantsClient.KEY_AUTH_HOST; + + String host; + int port; + + KeyPairFromPassword keyPair = null; + Cryptor cryptor; + Thread running = null; + + public KeyServerAuthenticator(String host, int port) + { + this.host = host; + this.port = port; + } + + public KeyServerAuthenticator () + { + this(DEFAULT_HOST, DEFAULT_PORT); + } + + public Thread get (String user, String password, Callback callback, SRPClientListener listener) throws Exception + { + keyPair = new KeyPairFromPassword (password); + keyPair.generate(); + + cryptor = new KeyPairFromPasswordCryptor (keyPair); + + ClientUserSession session = new ClientUserSession( + new Get(), + getFinished_().addCallback(callback), + new SRPClientUserSession ( + user, keyPair, + new IoChainBase64( + new IoChainNewLinePackets( + new IoChainSocket(host, port) + ) + ), + listener + ) + ); + + running = new IoChainThread(session); + running.start(); + + return running; + } + + public Callback getFinished_ () + { + return new CallbackDefault () { + @Override + public void onSuccess(Object... args) throws Exception + { + running = null; + + IoChain io = (IoChain)args[0]; + io.stop(); + + Object arg = args[1]; + if (arg instanceof Exception) + throw (Exception)arg; + + byte[] block = cryptor.decrypt((byte[])arg); + block = Zip.inflate(block); + Environment e = JSONSerializer.deserialize(block); + next(e); + } + }; + } + + public Thread put (String user, String password, Environment environment, Callback callback, SRPClientListener listener) throws Exception + { + keyPair = new KeyPairFromPassword (password); + keyPair.generate(); + + cryptor = new KeyPairFromPasswordCryptor (keyPair); + + byte[] block = cryptor.encrypt(Zip.deflate(JSONSerializer.serialize(environment))); + + ClientUserSession session = new ClientUserSession( + new Put(block), + putFinished_().addCallback(callback), + new SRPClientUserSession (user, keyPair, + new IoChainBase64 ( + new IoChainNewLinePackets( + new IoChainSocket(host, port) + ) + ), + listener + ) + ); + + running = new IoChainThread(session); + running.start(); + + return running; + } + + public Callback putFinished_ () + { + return new IoStop(); + } +} + diff --git a/java/core/src/core/key/auth/KeyServerAuthenticatorNoThread.java b/java/core/src/core/key/auth/KeyServerAuthenticatorNoThread.java new file mode 100644 index 0000000..3479b98 --- /dev/null +++ b/java/core/src/core/key/auth/KeyServerAuthenticatorNoThread.java @@ -0,0 +1,96 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package key.auth; + +import core.client.ClientUserSession; +import core.client.messages.Get; +import core.client.messages.Put; +import core.crypt.KeyPairFromPasswordCryptor; +import core.crypt.Cryptor; +import core.crypt.KeyPairFromPassword; +import core.io.IoChain; +import core.io.IoChainBase64; +import core.io.IoChainNewLinePackets; +import core.srp.client.SRPClientListener; +import core.srp.client.SRPClientUserSession; +import core.util.Environment; +import core.util.JSONSerializer; +import core.util.Zip; + +import core.callback.Callback; +import core.callback.CallbackChain; +import core.callback.CallbackDefault; +import core.callbacks.*; + +public class KeyServerAuthenticatorNoThread +{ + public static Callback getFinished_ (Cryptor cryptor) + { + return new IoStop() + .addCallback(cryptor.decrypt_()) + .addCallback(Zip.inflate_()) + .addCallback(new JSONDeserialize()); + } + + public static Callback putFinished_ () + { + return new IoStop(); + } + + public static Callback get_ (String user, KeyPairFromPassword keyPair, IoChain sender, SRPClientListener listener) throws Exception + { + return new CallbackDefault(user, keyPair, sender, listener) { + public void onSuccess(Object... arguments) throws Exception { + String user = V(0); + KeyPairFromPassword keyPair = V(1); + IoChain sender = V(2); + SRPClientListener listener = V(3); + + new ClientUserSession( + new Get(), + getFinished_(new KeyPairFromPasswordCryptor(keyPair)) + .setReturn(callback), + new SRPClientUserSession (user, keyPair, + new IoChainBase64( + new IoChainNewLinePackets( + sender + ) + ), + listener + ) + ).run(); + } + }; + } + + public static Callback put_ (String user, KeyPairFromPassword keyPair, Environment environment, IoChain sender, SRPClientListener listener) + { + return + new JSONSerialize() + .addCallback(Zip.deflate_()) + .addCallback(new KeyPairFromPasswordCryptor (keyPair).encrypt_()) + .addCallback(new CallbackDefault(user, keyPair, sender, listener) { + public void onSuccess(Object... arguments) throws Exception { + byte[] block = (byte[])arguments[0]; + String user = (String)V(0); + KeyPairFromPassword keyPair = (KeyPairFromPassword)V(1); + IoChain sender = (IoChain)V(2); + SRPClientListener listener = V(3); + new ClientUserSession( + new Put(block), + putFinished_().setReturn(callback), + new SRPClientUserSession (user, keyPair, + new IoChainBase64( + new IoChainNewLinePackets( + sender + ) + ), + listener + ) + ).run(); + } + }); + } +} diff --git a/java/core/src/core/key/auth/KeyServerAuthenticatorSync.java b/java/core/src/core/key/auth/KeyServerAuthenticatorSync.java new file mode 100644 index 0000000..6b3fbe7 --- /dev/null +++ b/java/core/src/core/key/auth/KeyServerAuthenticatorSync.java @@ -0,0 +1,58 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package key.auth; + +import core.callback.Callback; +import core.srp.client.SRPClientListener; +import core.util.Environment; + + +public class KeyServerAuthenticatorSync +{ + KeyServerAuthenticator authenticator; + Object[] result; + + private class ResultSetter extends Callback + { + public void invoke (Object... args) + { + result = args; + } + } + + public KeyServerAuthenticatorSync (String server, int port) + { + authenticator = new KeyServerAuthenticator(server, port); + } + + public KeyServerAuthenticatorSync () + { + authenticator = new KeyServerAuthenticator(); + } + + public Environment get (String user, String password, SRPClientListener listener) throws Exception + { + result = null; + Thread thread = authenticator.get(user, password, new ResultSetter(), listener); + thread.join(); + + if (result[0] instanceof Exception) + throw (Exception)result[0]; + + return (Environment)result[0]; + } + + public Environment put (String user, String password, Environment environment, SRPClientListener listener) throws Exception + { + result = null; + Thread thread = authenticator.put(user, password, environment, new ResultSetter(), listener); + thread.join(); + + if (result[0] instanceof Exception) + throw (Exception)result[0]; + + return (Environment)result[0]; + } +} diff --git a/java/core/src/core/key/auth/PutKeyServerEnvironment.java b/java/core/src/core/key/auth/PutKeyServerEnvironment.java new file mode 100644 index 0000000..198dc60 --- /dev/null +++ b/java/core/src/core/key/auth/PutKeyServerEnvironment.java @@ -0,0 +1,29 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package key.auth; + +import java.io.FileInputStream; + +import core.util.Environment; +import core.util.JSONSerializer; +import core.util.Streams; + +public class PutKeyServerEnvironment +{ + public static void main (String[] args) throws Exception + { + KeyServerAuthenticatorSync auth = new KeyServerAuthenticatorSync(); + + if (args.length != 4) + { + System.out.println("Arguments: name password outFile"); + throw new IllegalArgumentException(); + } + + FileInputStream reader = new FileInputStream(args[2]); + Environment e = JSONSerializer.deserialize(Streams.readFullyString(reader, "UTF-8").getBytes("UTF-8")); + auth.put(args[0], args[1], e, null); + } +} diff --git a/java/core/src/core/key/server/KeyServerSessionDb.java b/java/core/src/core/key/server/KeyServerSessionDb.java new file mode 100644 index 0000000..7622f95 --- /dev/null +++ b/java/core/src/core/key/server/KeyServerSessionDb.java @@ -0,0 +1,71 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package key.server; + +import java.math.BigInteger; + +import key.streamserver.SRPProtocolHandler; + +import core.exceptions.PublicMessageException; +import core.srp.server.SRPServerUserSessionDb; +import core.util.LogOut; +import core.util.Triple; + +import core.server.srp.db.UserDb; + +public class KeyServerSessionDb implements SRPServerUserSessionDb +{ + static LogOut log = new LogOut(SRPProtocolHandler.class); + UserDb db; + + public KeyServerSessionDb (UserDb db) + { + this.db = db; + } + + public void setBlock (String userName, byte[] bytes) throws Exception + { + log.debug("setBlock", userName); + db.setBlock(userName, bytes); + } + + public byte[] getBlock (String userName) throws Exception + { + log.debug("getBlock", userName); + return db.getBlock(userName); + } + + @Override + public Triple getUserVVS(String userName) throws Exception + { + return db.getVVS(userName); + } + + @Override + public void createUser(String version, String userName, BigInteger v, BigInteger s, byte[] extra) throws Exception + { + throw new PublicMessageException("Not supported"); + } + + @Override + public void rateLimitFailure(String userName) throws Exception + { +// db.rateLimitFailure(userName); + } + + @Override + public void markFailure(String userName) throws Exception + { + log.debug("markFailure", userName); + db.markFailure(userName); + } + + @Override + public void testCreate(String version, String userName) throws Exception + { + log.debug("testCreate", version, userName); + db.testCreateUser(version, userName); + } +} diff --git a/java/core/src/core/key/server/KeyServerUserSession.java b/java/core/src/core/key/server/KeyServerUserSession.java new file mode 100644 index 0000000..be97690 --- /dev/null +++ b/java/core/src/core/key/server/KeyServerUserSession.java @@ -0,0 +1,58 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package key.server; + +import org.json.JSONObject; + +import core.client.messages.Get; +import core.client.messages.Message; +import core.client.messages.Put; +import core.client.messages.Response; +import core.io.IoChain; +import core.srp.server.SRPServerUserSession; +import core.util.SimpleSerializer; +import core.server.captcha.Captcha; +import core.server.mailextra.MailExtraDb; + +public class KeyServerUserSession extends IoChain +{ + String userName; + KeyServerSessionDb db; + Captcha captcha; + + public KeyServerUserSession (KeyServerSessionDb db, SRPServerUserSession sender) throws Exception + { + super(sender); + + this.captcha = new Captcha(); + this.db = db; + } + + @Override + public void open () + { + userName = ((SRPServerUserSession)sender).getUserName(); + } + + @Override + public void onReceive (byte[] bytes) throws Exception + { + Message message = SimpleSerializer.deserialize(bytes); + + if (message instanceof Put) + { + db.setBlock(userName, ((Put)message).getBlock()); + send(SimpleSerializer.serialize(new Response(db.getBlock(userName)))); + } + else + if (message instanceof Get) + { + sender.send(SimpleSerializer.serialize(new Response(db.getBlock(userName)))); + } + else + throw new Exception("Unknown message type"); + } + +} diff --git a/java/core/src/core/key/server/sql/KeyUserDb.java b/java/core/src/core/key/server/sql/KeyUserDb.java new file mode 100644 index 0000000..2cab725 --- /dev/null +++ b/java/core/src/core/key/server/sql/KeyUserDb.java @@ -0,0 +1,29 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package key.server.sql; + +import java.io.IOException; +import java.sql.SQLException; + +import core.server.srp.db.UserDb; +import core.server.srp.db.sql.Catalog; + +public class KeyUserDb extends UserDb +{ + public KeyUserDb () + { + super (new Catalog()); + } + + public byte[] getBlock (String userName) throws IOException, SQLException + { + return super.getKeyBlock(userName); + } + + public byte[] setBlock(String userName, byte[] block) throws IOException, SQLException + { + return super.setKeyBlock(userName, block); + } +} diff --git a/java/core/src/core/key/streamserver/BogusSslContextFactory.java b/java/core/src/core/key/streamserver/BogusSslContextFactory.java new file mode 100644 index 0000000..887e133 --- /dev/null +++ b/java/core/src/core/key/streamserver/BogusSslContextFactory.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package key.streamserver; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.Security; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; + +/** + * Factory to create a bogus SSLContext. + * + * @author Apache MINA Project + */ +public class BogusSslContextFactory { + + /** + * Protocol to use. + */ + private static final String PROTOCOL = "TLS"; + + private static final String KEY_MANAGER_FACTORY_ALGORITHM; + + static { + String algorithm = Security + .getProperty("ssl.KeyManagerFactory.algorithm"); + if (algorithm == null) { + algorithm = KeyManagerFactory.getDefaultAlgorithm(); + } + + KEY_MANAGER_FACTORY_ALGORITHM = algorithm; + } + + /** + * Bougus Server certificate keystore file name. + */ + private static final String BOGUS_KEYSTORE = "bogus.cert"; + + // NOTE: The keystore was generated using keytool: + // keytool -genkey -alias bogus -keysize 512 -validity 3650 + // -keyalg RSA -dname "CN=bogus.com, OU=XXX CA, + // O=Bogus Inc, L=Stockholm, S=Stockholm, C=SE" + // -keypass boguspw -storepass boguspw -keystore bogus.cert + + /** + * Bougus keystore password. + */ + private static final char[] BOGUS_PW = { 'b', 'o', 'g', 'u', 's', 'p', 'w' }; + + private static SSLContext serverInstance = null; + + private static SSLContext clientInstance = null; + + /** + * Get SSLContext singleton. + * + * @return SSLContext + * @throws java.security.GeneralSecurityException + * + */ + public static SSLContext getInstance(boolean server) + throws GeneralSecurityException { + SSLContext retInstance = null; + if (server) { + synchronized(BogusSslContextFactory.class) { + if (serverInstance == null) { + try { + serverInstance = createBougusServerSslContext(); + } catch (Exception ioe) { + throw new GeneralSecurityException( + "Can't create Server SSLContext:" + ioe); + } + } + } + retInstance = serverInstance; + } else { + synchronized (BogusSslContextFactory.class) { + if (clientInstance == null) { + clientInstance = createBougusClientSslContext(); + } + } + retInstance = clientInstance; + } + return retInstance; + } + + private static SSLContext createBougusServerSslContext() + throws GeneralSecurityException, IOException { + // Create keystore + KeyStore ks = KeyStore.getInstance("JKS"); + InputStream in = null; + try { + in = BogusSslContextFactory.class + .getResourceAsStream(BOGUS_KEYSTORE); + ks.load(in, BOGUS_PW); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ignored) { + } + } + } + + // Set up key manager factory to use our key store + KeyManagerFactory kmf = KeyManagerFactory + .getInstance(KEY_MANAGER_FACTORY_ALGORITHM); + kmf.init(ks, BOGUS_PW); + + // Initialize the SSLContext to work with our key managers. + SSLContext sslContext = SSLContext.getInstance(PROTOCOL); + sslContext.init(kmf.getKeyManagers(), + BogusTrustManagerFactory.X509_MANAGERS, null); + + return sslContext; + } + + private static SSLContext createBougusClientSslContext() + throws GeneralSecurityException { + SSLContext context = SSLContext.getInstance(PROTOCOL); + context.init(null, BogusTrustManagerFactory.X509_MANAGERS, null); + return context; + } + +} diff --git a/java/core/src/core/key/streamserver/BogusTrustManagerFactory.java b/java/core/src/core/key/streamserver/BogusTrustManagerFactory.java new file mode 100644 index 0000000..1ec1e3a --- /dev/null +++ b/java/core/src/core/key/streamserver/BogusTrustManagerFactory.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package key.streamserver; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactorySpi; +import javax.net.ssl.X509TrustManager; + +/** + * Bogus trust manager factory. Creates BogusX509TrustManager + * + * @author Apache MINA Project + */ +class BogusTrustManagerFactory extends TrustManagerFactorySpi { + + static final X509TrustManager X509 = new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] x509Certificates, + String s) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] x509Certificates, + String s) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + + static final TrustManager[] X509_MANAGERS = new TrustManager[] { X509 }; + + public BogusTrustManagerFactory() { + } + + @Override + protected TrustManager[] engineGetTrustManagers() { + return X509_MANAGERS; + } + + @Override + protected void engineInit(KeyStore keystore) throws KeyStoreException { + // noop + } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) + throws InvalidAlgorithmParameterException { + // noop + } +} diff --git a/java/core/src/core/key/streamserver/KeyStreamServerMain.java b/java/core/src/core/key/streamserver/KeyStreamServerMain.java new file mode 100644 index 0000000..0386e0f --- /dev/null +++ b/java/core/src/core/key/streamserver/KeyStreamServerMain.java @@ -0,0 +1,67 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package key.streamserver; + +import java.net.InetSocketAddress; +import java.nio.charset.Charset; + +import key.server.sql.KeyUserDb; + +import org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder; +import org.apache.mina.filter.codec.ProtocolCodecFilter; +import org.apache.mina.filter.codec.textline.TextLineCodecFactory; +import org.apache.mina.filter.logging.LoggingFilter; +import org.apache.mina.filter.ssl.SslFilter; +import org.apache.mina.transport.socket.nio.NioSocketAcceptor; +import core.constants.ConstantsServer; + + +/** + * (Entry point) NetCat client. NetCat client connects to the specified + * endpoint and prints out received data. NetCat client disconnects + * automatically when no data is read for 10 seconds. + * + * @author Apache MINA Project + */ +public class KeyStreamServerMain +{ + public static void main(String[] args) throws Exception + { + Class.forName("com.mysql.jdbc.Driver"); + KeyUserDb userDb = new KeyUserDb(); + userDb.ensureTables(); + + // Create TCP/IP connector. + NioSocketAcceptor acceptor = new NioSocketAcceptor(); + + DefaultIoFilterChainBuilder chain = acceptor.getFilterChain(); + + // Start communication. + acceptor.getFilterChain().addLast("logger", new LoggingFilter()); + TextLineCodecFactory textLineCodec = new TextLineCodecFactory(Charset.forName("UTF-8")); + textLineCodec.setDecoderMaxLineLength(50 * 1000); + textLineCodec.setEncoderMaxLineLength(50 * 1000); + acceptor.getFilterChain().addLast( + "codec", + new ProtocolCodecFilter(textLineCodec) + ); + + acceptor.setHandler(new SRPProtocolHandler()); + acceptor.bind(new InetSocketAddress(ConstantsServer.KEY_AUTH_PORT)); + + System.out.println("Listening on port " + ConstantsServer.KEY_AUTH_PORT); + } + + private static void addSSLSupport(DefaultIoFilterChainBuilder chain) throws Exception + { + SSLContextGenerator sslContextGenerator = new SSLContextGenerator(); + SslFilter sslFilter = new SslFilter(sslContextGenerator.getSslContext()); + sslFilter.setUseClientMode(false); + sslFilter.setWantClientAuth(true); + + chain.addLast("sslFilter", sslFilter); + System.out.println("SSL ON"); + } +} diff --git a/java/core/src/core/key/streamserver/SRPProtocolHandler.java b/java/core/src/core/key/streamserver/SRPProtocolHandler.java new file mode 100644 index 0000000..706ca36 --- /dev/null +++ b/java/core/src/core/key/streamserver/SRPProtocolHandler.java @@ -0,0 +1,161 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package key.streamserver; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import key.server.KeyServerSessionDb; +import key.server.KeyServerUserSession; +import key.server.sql.KeyUserDb; +import mail.streamserver.MailServerSessionDb; + +import org.apache.mina.core.buffer.IoBuffer; +import org.apache.mina.core.service.IoHandler; +import org.apache.mina.core.service.IoHandlerAdapter; +import org.apache.mina.core.session.IdleStatus; +import org.apache.mina.core.session.IoSession; + +import core.constants.ConstantsServer; +import core.crypt.CryptorRSAAES; +import core.crypt.CryptorRSAJCE; +import core.io.IoChainBase64; +import core.io.IoChainFinishedException; +import core.io.IoChainNewLinePackets; +import core.io.IoChainAccumulator; +import core.srp.server.SRPServerUserSession; +import core.util.ExternalResource; +import core.util.InternalResource; +import core.util.LogNull; +import core.util.LogOut; + + +/** + * {@link IoHandler} implementation for NetCat client. This class extended + * {@link IoHandlerAdapter} for convenience. + * + * @author Apache MINA Project + */ +public class SRPProtocolHandler extends IoHandlerAdapter +{ + static final int TIMEOUT_SECONDS = ConstantsServer.AUTH_TIMEOUT; + static LogOut log = new LogOut(SRPProtocolHandler.class); + + CryptorRSAAES cryptorRSA; + KeyUserDb db; + Map sessions = new HashMap(); + + public SRPProtocolHandler () throws Exception + { + db = new KeyUserDb(); + cryptorRSA = new CryptorRSAAES(new CryptorRSAJCE(ExternalResource.getResourceAsStream(getClass(), "keystore.jks"), null)); + } + + @Override + public void exceptionCaught(IoSession session, Throwable cause) + { + log.debug("exceptionCaught", cause); + session.close(true); + } + + @Override + public void sessionOpened(IoSession session) + { + log.debug("sessionOpened",session); + + // Set reader idle time to 10 seconds. + // sessionIdle(...) method will be invoked when no data is read + // for 10 seconds. + session.getConfig().setIdleTime(IdleStatus.READER_IDLE, TIMEOUT_SECONDS); + + try + { + KeyServerSessionDb sessionDb = new KeyServerSessionDb(db); + KeyServerUserSession userSession = + new KeyServerUserSession( + sessionDb, + new SRPServerUserSession(cryptorRSA, sessionDb, + new IoChainBase64( + new IoChainNewLinePackets( + new IoChainAccumulator() + ) + ) + ) + ); + + userSession.run(); + sessions.put(session, userSession); + } + catch (Exception e) + { + log.debug("sessionOpened caught", e); + session.close(true); + } + } + + @Override + public void sessionClosed(IoSession session) throws Exception + { + log.debug("sessionClosed ", session," Total ",session.getReadBytes()," byte(s)"); + KeyServerUserSession userSession = sessions.get(session); + sessions.remove(session); + + if (userSession != null) + userSession.stop(); + } + + @Override + public void sessionIdle(IoSession session, IdleStatus status) { + // Close the connection if reader is idle. + if (status == IdleStatus.READER_IDLE) { + session.close(true); + } + } + + public void write (IoSession session, byte[] data) + { + log.debug("writing"); + + byte[] bytes = data; + IoBuffer out = IoBuffer.allocate(bytes.length); + out.setAutoExpand(true); + out.put (bytes); + out.flip(); + + session.write(out); + } + + @Override + public void messageReceived(IoSession session, Object message) { + log.debug("messageReceived"); + + try + { + IoChainAccumulator userSession = + (IoChainAccumulator)sessions.get(session).getFinalSender(); + + // add back the new line + String str = message.toString() + "\n"; + userSession.receive(str.getBytes()); + + List packets = userSession.getAndClearPackets(); + for(byte[] packet: packets) + write(session, packet); + + Exception e = userSession.getAndClearException(); + if (e != null) + throw e; + + if (userSession.isClosed()) + throw new IoChainFinishedException(); + } + catch (Exception e) + { + log.debug("messageReceived caught", e); + session.close(true); + } + } +} \ No newline at end of file diff --git a/java/core/src/core/key/streamserver/SSLContextGenerator.java b/java/core/src/core/key/streamserver/SSLContextGenerator.java new file mode 100644 index 0000000..2bf073b --- /dev/null +++ b/java/core/src/core/key/streamserver/SSLContextGenerator.java @@ -0,0 +1,42 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package key.streamserver; + +import java.io.File; +import java.security.KeyStore; +import javax.net.ssl.SSLContext; +import org.apache.mina.filter.ssl.KeyStoreFactory; +import org.apache.mina.filter.ssl.SslContextFactory; + +import core.util.Streams; + + +public class SSLContextGenerator +{ + public SSLContext getSslContext() throws Exception + { + SSLContext sslContext = null; + + final KeyStoreFactory keyStoreFactory = new KeyStoreFactory(); + keyStoreFactory.setData(Streams.readFullyBytes(this.getClass().getResourceAsStream("keystore.jks"))); + keyStoreFactory.setPassword("password"); + + final KeyStoreFactory trustStoreFactory = new KeyStoreFactory(); + trustStoreFactory.setData(Streams.readFullyBytes(this.getClass().getResourceAsStream("truststore.jks"))); + trustStoreFactory.setPassword("password"); + + final SslContextFactory sslContextFactory = new SslContextFactory(); + final KeyStore keyStore = keyStoreFactory.newInstance(); + sslContextFactory.setKeyManagerFactoryKeyStore(keyStore); + + final KeyStore trustStore = trustStoreFactory.newInstance(); + sslContextFactory.setTrustManagerFactoryKeyStore(trustStore); + sslContextFactory.setKeyManagerFactoryKeyStorePassword("password"); + sslContext = sslContextFactory.newInstance(); + System.out.println("SSL provider is: " + sslContext.getProvider()); + + return sslContext; + } +} \ No newline at end of file diff --git a/java/core/src/core/mail/auth/GetMailServerEnvironment.java b/java/core/src/core/mail/auth/GetMailServerEnvironment.java new file mode 100644 index 0000000..3189ae9 --- /dev/null +++ b/java/core/src/core/mail/auth/GetMailServerEnvironment.java @@ -0,0 +1,31 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.auth; + +import java.io.FileWriter; + +import core.util.Environment; +import core.util.JSONSerializer; + +public class GetMailServerEnvironment +{ + public static void main (String[] args) throws Exception + { + MailServerAuthenticatorSync auth = new MailServerAuthenticatorSync(); + + if (args.length != 3) + { + System.out.println("Arguments: name password outFile"); + throw new IllegalArgumentException(); + } + + Environment e = auth.get(args[0], args[1]); + + FileWriter writer = new FileWriter(args[2]); + writer.write(new String(JSONSerializer.serialize(e))); + writer.flush(); + writer.close(); + } +} diff --git a/java/core/src/core/mail/auth/MailServerAuthTest.java b/java/core/src/core/mail/auth/MailServerAuthTest.java new file mode 100644 index 0000000..65a6102 --- /dev/null +++ b/java/core/src/core/mail/auth/MailServerAuthTest.java @@ -0,0 +1,53 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.auth; + +import java.math.BigInteger; +import java.util.Random; + + +import core.constants.ConstantsEnvironmentKeys; +import core.util.Environment; + + +public class MailServerAuthTest +{ + public static void main (String[] args) throws Exception + { + Random random = new Random(); + MailServerAuthenticatorSync auth = new MailServerAuthenticatorSync(); + + String user = "test-" + BigInteger.valueOf(Math.abs(random.nextLong())).toString(32); + user = user + "@mailiverse.com"; + + String password = "password-" + BigInteger.valueOf(Math.abs(random.nextLong())).toString(32); + String token = "3ee1vpbih0cl"; + + System.out.println("user " + user); + + System.out.println("testCreate"); + auth.test(user); + + System.out.println("create"); + auth.create(user, password, token); + + Environment e = new Environment(); + e.put(ConstantsEnvironmentKeys.SMTP_PASSWORD, "abcdef"); + + System.out.println("put"); + e.put("hi", "there"); + auth.put(user, password, e); + + System.out.println("get"); + e = auth.get(user, password); + for (String k : e.keySet()) + { + System.out.format("%s -> %s\n", k, e.get(k)); + } + + System.out.println("delete"); + auth.delete(user, password); + } +} diff --git a/java/core/src/core/mail/auth/MailServerAuthenticator.java b/java/core/src/core/mail/auth/MailServerAuthenticator.java new file mode 100644 index 0000000..559cc1a --- /dev/null +++ b/java/core/src/core/mail/auth/MailServerAuthenticator.java @@ -0,0 +1,273 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.auth; + +import core.client.ClientCreateSession; +import core.client.ClientTestCreateSession; +import core.client.ClientUserSession; +import core.client.messages.Delete; +import core.client.messages.Get; +import core.client.messages.Put; +import core.client.messages.Response; +import core.constants.ConstantsClient; +import core.constants.ConstantsServer; +import core.crypt.CryptorRSAAES; +import core.crypt.CryptorRSAFactory; +import core.crypt.CryptorRSAJCE; +import core.exceptions.PublicMessageException; +import core.io.IoChain; +import core.io.IoChainBase64; +import core.io.IoChainNewLinePackets; +import core.io.IoChainSocket; +import core.io.IoChainThread; +import core.crypt.KeyPairFromPassword; +import core.srp.SRPPackets; +import core.srp.client.SRPClientUserSession; +import core.callback.CallbackWithVariables; +import core.callback.Callback; +import core.callbacks.IoStop; +import core.util.Environment; +import core.util.ExternalResource; +import core.util.InternalResource; +import core.util.JSONSerializer; +import core.util.SimpleSerializer; + +public class MailServerAuthenticator +{ + public static final int DEFAULT_PORT = ConstantsClient.MAIL_AUTH_PORT; + public static final String DEFAULT_HOST = ConstantsClient.MAIL_AUTH_HOST; + + String host; + int port; + + KeyPairFromPassword keyPair = null; + Thread running = null; + + public MailServerAuthenticator (String host, int port) + { + this.host = host; + this.port = port; + } + + public MailServerAuthenticator () + { + this(DEFAULT_HOST, DEFAULT_PORT); + } + + public Thread testCreate (String user, Callback callback) throws Exception + { + CryptorRSAAES cryptor = new CryptorRSAAES(CryptorRSAFactory.fromResources(null, ExternalResource.getResourceAsStream(MailServerAuthenticator.class, "truststore.jks"))); + + ClientTestCreateSession session = new ClientTestCreateSession( + cryptor, user, + new CallbackWithVariables (callback) { + public void invoke (Object... args) + { + testCreateFinished((Callback)V(0), args); + } + }, + new IoChainBase64( + new IoChainNewLinePackets( + new IoChainSocket(host, port) + ) + ) + ); + + running = new IoChainThread(session); + running.start(); + + return running; + } + + public void testCreateFinished (Callback callback, Object[] args) + { + try + { + running = null; + + IoChain io = (IoChain)args[0]; + io.stop(); + + Object arg = args[1]; + if (arg instanceof Exception) + throw (Exception)arg; + + SRPPackets.PacketInit_ServerResponse response = SimpleSerializer.deserialize((byte[])arg); + if (!response.succeeded) + throw new PublicMessageException(response.reason); + + callback.invoke(true); + } + catch (Exception e) + { + callback.invoke(e); + } + } + + + public Thread create (String user, String password, String token, Callback callback) throws Exception + { + keyPair = new KeyPairFromPassword (password); + keyPair.generate(); + + CryptorRSAAES cryptor = new CryptorRSAAES(CryptorRSAFactory.fromResources(null, ExternalResource.getResourceAsStream(MailServerAuthenticator.class, "truststore.jks"))); + + ClientCreateSession session = new ClientCreateSession( + cryptor, user, keyPair, SimpleSerializer.serialize(token), + new CallbackWithVariables (callback) { + public void invoke (Object... args) + { + createFinished((Callback)V(0), args); + } + }, + new IoChainBase64( + new IoChainNewLinePackets( + new IoChainSocket(host, port) + ) + ) + ); + + running = new IoChainThread(session); + running.start(); + + return running; + } + + public void createFinished (Callback callback, Object[] args) + { + try + { + running = null; + + IoChain io = (IoChain)args[0]; + io.stop(); + + Object arg = args[1]; + if (arg instanceof Exception) + throw (Exception)arg; + + SRPPackets.PacketInit_ServerResponse response = SimpleSerializer.deserialize((byte[])arg); + if (!response.succeeded) + throw new PublicMessageException(response.reason); + + callback.invoke(true); + } + catch (Exception e) + { + callback.invoke(e); + } + } + + public Thread delete (String user, String password, Callback callback) throws Exception + { + keyPair = new KeyPairFromPassword (password); + keyPair.generate(); + + ClientUserSession session = new ClientUserSession( + new Delete(), + new IoStop().setReturn(callback), + new SRPClientUserSession( + user, keyPair, + new IoChainBase64( + new IoChainNewLinePackets( + new IoChainSocket(host, port) + ) + ), + null + ) + ); + + running = new IoChainThread(session); + running.start(); + + return running; + } + + + public Thread get (String user, String password, Callback callback) throws Exception + { + keyPair = new KeyPairFromPassword (password); + keyPair.generate(); + + ClientUserSession session = new ClientUserSession( + new Get(), + new CallbackWithVariables (callback) { + public void invoke (Object... args) + { + getOrPutFinished((Callback)V(0), args); + } + }, + new SRPClientUserSession( + user, keyPair, + new IoChainBase64( + new IoChainNewLinePackets( + new IoChainSocket(host, port) + ) + ), + null + ) + ); + + running = new IoChainThread(session); + running.start(); + + return running; + } + + public void getOrPutFinished (Callback callback, Object[] args) + { + try + { + running = null; + + IoChain io = (IoChain)args[0]; + io.stop(); + + Object arg = args[1]; + if (arg instanceof Exception) + throw (Exception)arg; + + Response response = (Response)arg; + Environment e = JSONSerializer.deserialize(response.getBlock()); + callback.invoke(e); + } + catch (Exception e) + { + callback.invoke(e); + } + } + + public Thread put (String user, String password, Environment environment, Callback callback) throws Exception + { + keyPair = new KeyPairFromPassword (password); + keyPair.generate(); + + byte[] block = JSONSerializer.serialize(environment); + + ClientUserSession session = new ClientUserSession( + new Put(block), + new CallbackWithVariables (callback) { + public void invoke (Object... args) + { + getOrPutFinished((Callback)V(0), args); + } + }, + new SRPClientUserSession( + user, keyPair, + new IoChainBase64( + new IoChainNewLinePackets( + new IoChainSocket(host, port) + ) + ), + null + ) + ); + + running = new IoChainThread(session); + running.start(); + + return running; + } +} diff --git a/java/core/src/core/mail/auth/MailServerAuthenticatorNoThread.java b/java/core/src/core/mail/auth/MailServerAuthenticatorNoThread.java new file mode 100644 index 0000000..097724c --- /dev/null +++ b/java/core/src/core/mail/auth/MailServerAuthenticatorNoThread.java @@ -0,0 +1,188 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.auth; + +import core.callback.Callback; +import core.callback.CallbackDefault; +import core.callbacks.IoStop; +import core.callbacks.JSONDeserialize; +import core.callbacks.JSONSerialize; +import core.client.ClientCreateSession; +import core.client.ClientTestCreateSession; +import core.client.ClientUserSession; +import core.client.messages.Delete; +import core.client.messages.Get; +import core.client.messages.Put; +import core.crypt.CryptorRSAAES; +import core.crypt.CryptorRSAFactory; +import core.crypt.KeyPairFromPassword; +import core.exceptions.PublicMessageException; +import core.io.IoChain; +import core.io.IoChainBase64; +import core.io.IoChainNewLinePackets; +import core.srp.SRPPackets; +import core.srp.client.SRPClientListener; +import core.srp.client.SRPClientUserSession; +import core.util.Environment; +import core.util.InternalResource; +import core.util.SimpleSerializer; + +public class MailServerAuthenticatorNoThread +{ + public static Callback testCreate_ (String user, IoChain sender) + { + return new CallbackDefault(user, sender) { + public void onSuccess(Object... arguments) throws Exception { + String user = V(0); + IoChain sender = V(1); + CryptorRSAAES cryptor = new CryptorRSAAES(CryptorRSAFactory.fromResources(null, InternalResource.getResourceAsStream(MailServerAuthenticatorNoThread.class, "truststore.jks"))); + + new ClientTestCreateSession( + cryptor, user, + createFinished_().setReturn(callback), + new IoChainBase64( + new IoChainNewLinePackets( + sender + ) + ) + ).run(); + } + }; + } + + public static Callback create_ (String user, KeyPairFromPassword keyPair, String token, IoChain sender) + { + return new CallbackDefault(user, keyPair, token, sender) { + public void onSuccess(Object... arguments) throws Exception { + String user = V(0); + KeyPairFromPassword keyPair = V(1); + String token = V(2); + IoChain sender = V(3); + CryptorRSAAES cryptor = new CryptorRSAAES(CryptorRSAFactory.fromResources(null, InternalResource.getResourceAsStream(MailServerAuthenticatorNoThread.class, "truststore.jks"))); + + new ClientCreateSession( + cryptor, user, keyPair, SimpleSerializer.serialize(token), + createFinished_().setReturn(callback), + new IoChainBase64( + new IoChainNewLinePackets( + sender + ) + ) + ).run(); + } + }; + } + + public static Callback createFinished_ () + { + return new IoStop() + .addCallback(new JSONDeserialize()) + .addCallback(new CallbackDefault(){ + + @Override + public void onSuccess(Object... arguments) throws Exception { + SRPPackets.PacketInit_ServerResponse response = (SRPPackets.PacketInit_ServerResponse)arguments[0]; + if (!response.succeeded) + throw new PublicMessageException(response.reason); + + next(true); + } + }); + } + + public static Callback get_ (String user, KeyPairFromPassword keyPair, IoChain sender, SRPClientListener listener) throws Exception + { + return new CallbackDefault(user, keyPair, sender, listener) { + public void onSuccess(Object... arguments) throws Exception { + String user = V(0); + KeyPairFromPassword keyPair = V(1); + IoChain sender = V(2); + SRPClientListener listener = V(3); + + new ClientUserSession( + new Get(), + getFinished_().setReturn(callback), + new SRPClientUserSession (user, keyPair, + new IoChainBase64( + new IoChainNewLinePackets( + sender + ) + ), + listener + ) + ).run(); + } + }; + } + + public static Callback put_ (String user, KeyPairFromPassword keyPair, Environment environment, IoChain sender, SRPClientListener listener) + { + return new JSONSerialize() + .addCallback(new CallbackDefault(user, sender, keyPair, listener) { + public void onSuccess(Object... arguments) throws Exception { + byte[] block = (byte[])arguments[0]; + String user = (String)V(0); + IoChain sender = (IoChain)V(1); + KeyPairFromPassword keyPair = (KeyPairFromPassword)V(2); + SRPClientListener listener = V(3); + + new ClientUserSession( + new Put(block), + putFinished_().setReturn(callback), + new SRPClientUserSession (user, keyPair, + new IoChainBase64( + new IoChainNewLinePackets( + sender + ) + ), + listener + ) + ).run(); + } + }); + } + + public static Callback delete_ (String user, KeyPairFromPassword keyPair, IoChain sender, SRPClientListener listener) + { + return + new CallbackDefault(user, sender, keyPair, listener) { + public void onSuccess(Object... arguments) throws Exception { + String user = (String)V(0); + IoChain sender = (IoChain)V(1); + KeyPairFromPassword keyPair = (KeyPairFromPassword)V(2); + SRPClientListener listener = V(3); + + new ClientUserSession( + new Delete(), + deleteFinished_().setReturn(callback), + new SRPClientUserSession (user, keyPair, + new IoChainBase64( + new IoChainNewLinePackets( + sender + ) + ), + listener + ) + ).run(); + } + }; + } + + public static Callback deleteFinished_ () + { + return new IoStop(); + } + + public static Callback getFinished_ () + { + return new IoStop().addCallback(new JSONDeserialize()); + } + + public static Callback putFinished_ () + { + return new IoStop(); + } + +} diff --git a/java/core/src/core/mail/auth/MailServerAuthenticatorSync.java b/java/core/src/core/mail/auth/MailServerAuthenticatorSync.java new file mode 100644 index 0000000..e13eebf --- /dev/null +++ b/java/core/src/core/mail/auth/MailServerAuthenticatorSync.java @@ -0,0 +1,77 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.auth; + +import core.callback.Callback; +import core.util.Environment; + + +public class MailServerAuthenticatorSync +{ + MailServerAuthenticator authenticator; + Object[] result; + + private class ResultSetter extends Callback + { + public void invoke (Object... args) + { + result = args; + } + } + + public MailServerAuthenticatorSync () + { + authenticator = new MailServerAuthenticator(); + } + + public void create (String user, String password, String token) throws Exception + { + Thread thread = authenticator.create(user, password, token, new ResultSetter()); + thread.join(); + + if (result[0] instanceof Exception) + throw (Exception)result[0]; + } + + public void test (String user) throws Exception + { + Thread thread = authenticator.testCreate(user, new ResultSetter()); + thread.join(); + + if (result[0] instanceof Exception) + throw (Exception)result[0]; + } + + public Environment get (String user, String password) throws Exception + { + Thread thread = authenticator.get(user, password, new ResultSetter()); + thread.join(); + + if (result[0] instanceof Exception) + throw (Exception)result[0]; + + return (Environment)result[0]; + } + + public Environment put (String user, String password, Environment environment) throws Exception + { + Thread thread = authenticator.put(user, password, environment, new ResultSetter()); + thread.join(); + + if (result[0] instanceof Exception) + throw (Exception)result[0]; + + return (Environment)result[0]; + } + + public void delete(String user, String password) throws Exception + { + Thread thread = authenticator.delete(user, password, new ResultSetter()); + thread.join(); + + if (result[0] instanceof Exception) + throw (Exception)result[0]; + } +} diff --git a/java/core/src/core/mail/auth/PutMailServerEnvironment.java b/java/core/src/core/mail/auth/PutMailServerEnvironment.java new file mode 100644 index 0000000..989d5be --- /dev/null +++ b/java/core/src/core/mail/auth/PutMailServerEnvironment.java @@ -0,0 +1,29 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.auth; + +import java.io.FileInputStream; + +import core.util.Environment; +import core.util.JSONSerializer; +import core.util.Streams; + +public class PutMailServerEnvironment +{ + public static void main (String[] args) throws Exception + { + MailServerAuthenticatorSync auth = new MailServerAuthenticatorSync(); + + if (args.length != 3) + { + System.out.println("Arguments: name password outFile"); + throw new IllegalArgumentException(); + } + + FileInputStream reader = new FileInputStream(args[2]); + Environment e = JSONSerializer.deserialize(Streams.readFullyString(reader, "UTF-8").getBytes("UTF-8")); + auth.put(args[0], args[1], e); + } +} diff --git a/java/core/src/core/mail/client/Actions.java b/java/core/src/core/mail/client/Actions.java new file mode 100644 index 0000000..6a47739 --- /dev/null +++ b/java/core/src/core/mail/client/Actions.java @@ -0,0 +1,170 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import java.util.Date; + +import mail.client.model.Attachments; +import mail.client.model.Body; +import mail.client.model.Conversation; +import mail.client.model.Header; +import mail.client.model.Mail; +import mail.client.model.Recipients; +import mail.client.model.TransportState; + +import core.callback.Callback; +import core.callback.CallbackDefault; +import core.constants.ConstantsSettings; +import core.constants.ConstantsEnvironmentKeys; +import core.util.LogNull; +import core.util.Pair; + + +public class Actions extends Servent +{ + static LogNull log = new LogNull(Actions.class); + + public Body calculateSignaturedReplyBody (Mail mail) + { + String signature = getMaster().getCacheManager().getSettings().get(ConstantsSettings.SIGNATURE, ""); + return mail.calculateReply(signature); + } + + public Pair newMail () throws Exception + { + Mail mail = master.getCacheManager().newMail ( + new Header ( + null, // external key + null, // original key + null, + master.getIdentity(), + new Recipients(), + null, + new Date(), + TransportState.fromList(TransportState.DRAFT), + null + ), + new Body (), + new Attachments () + ); + + Conversation conversation = master.getIndexer().newMail(mail); + return new Pair(conversation,mail); + } + + public void saveMail (Conversation conversation, Mail mail) + { + mail.getHeader().setDate(new Date()); + mail.markDirty(); + + conversation.itemChanged(mail); + master.getIndexer().conversationChanged(conversation); + } + + public void deleteMail (Conversation conversation, Mail mail) + { + conversation.removeItem(mail); + master.getCacheManager().deleteMail(mail); + master.getStore().deleteMail(mail); + + if (conversation.getNumItems()==0) + { + master.getIndexer().removeConversation(conversation); + master.getCacheManager().deleteConversation(conversation); + } + else + { + master.getIndexer().conversationChanged(conversation); + } + } + + public Callback deleteMail_ (Conversation conversation, Mail mail) + { + return new CallbackDefault(conversation, mail) + { + public void onSuccess(Object... arguments) throws Exception + { + Conversation conversation = V(0); + Mail mail = V(1); + deleteMail(conversation, mail); + next(arguments); + } + }; + } + + public void deleteConversation (Conversation conversation) + { + Mail mails[] = conversation.getItems().toArray(new Mail[0]); + + for (Mail mail : mails) + mail.apply(deleteMail_(conversation, mail)); + } + + public Mail replyToAll (Conversation conversation, Mail mail) throws Exception + { + return reply ( + new Recipients (mail.getHeader().calculateReplyAll(getMaster()), null, null, null), + conversation, mail, calculateSignaturedReplyBody(mail) + ); + } + + public Mail replyTo (Conversation conversation, Mail mail) throws Exception + { + return reply ( + new Recipients (mail.getHeader().calculateReplyTo(getMaster()), null, null, null), + conversation, mail, calculateSignaturedReplyBody(mail) + ); + } + + public Mail forward (Conversation conversation, Mail mail) throws Exception + { + return reply ( + new Recipients (null,null,null,null), + conversation, mail, new Body(mail.getBody()) + ); + } + + public void sendMail (Conversation conversation, Mail mail) + { + log.debug("Actions.sendMail"); + + Mailer sendMail = master.getMailer(); + + sendMail.sendMail( + master.getEnvironment().get(ConstantsEnvironmentKeys.SMTP_PASSWORD), + conversation, + mail + ); + } + + public void reindexConversation (Conversation conversation) + { + master.getIndexer().conversationChanged(conversation); + } + + public Mail reply (Recipients recipients, Conversation conversation, Mail mail, Body body) throws Exception + { + Mail reply = master.getCacheManager().newMail ( + new Header ( + null, // external key + null, // original key + null, + master.getIdentity(), + recipients, + mail.getHeader().getSubject(), + new Date(), + TransportState.fromList(TransportState.DRAFT), + null + ), + body != null ? body : new Body(), + new Attachments () + ); + + conversation.addItem(reply); + master.getIndexer().replyMail(conversation, reply); + + return reply; + } +} diff --git a/java/core/src/core/mail/client/ArrivalsMonitor.java b/java/core/src/core/mail/client/ArrivalsMonitor.java new file mode 100644 index 0000000..f967c9e --- /dev/null +++ b/java/core/src/core/mail/client/ArrivalsMonitor.java @@ -0,0 +1,16 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +public abstract class ArrivalsMonitor extends Servent +{ + public ArrivalsMonitor () + { + } + + public abstract void check (); + + public abstract boolean isChecking (); +} diff --git a/java/core/src/core/mail/client/ArrivalsMonitorDefault.java b/java/core/src/core/mail/client/ArrivalsMonitorDefault.java new file mode 100644 index 0000000..6bca80a --- /dev/null +++ b/java/core/src/core/mail/client/ArrivalsMonitorDefault.java @@ -0,0 +1,185 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import mail.client.model.Direction; + +import core.callback.Callback; +import core.callback.CallbackChain; +import core.callback.CallbackDefault; +import core.callback.CallbackWithVariables; +import core.connector.FileInfo; +import core.connector.async.AsyncStoreConnector; +import core.connector.async.AsyncStoreConnectorBase64; +import core.connector.async.AsyncStoreConnectorEncrypted; +import core.connector.async.Lock; +import core.constants.ConstantsStorage; +import core.crypt.Cryptor; +import core.util.LogNull; + +public class ArrivalsMonitorDefault extends ArrivalsMonitor +{ + static LogNull log = new LogNull(ArrivalsMonitorDefault.class); + static final int NUM_FILES_BEFORE_CACHE = 10; + static final int NUM_FILES_IN_CHECK = 15; + AsyncStoreConnector store; + + boolean checking = false; + Object in,out; + List files; + int totalFilesFound=0; + int numFilesWillCheck=0; + Lock checkMailLock; + + public ArrivalsMonitorDefault (Cryptor cryptor, AsyncStoreConnector connector) + { + this.store = new AsyncStoreConnectorEncrypted(cryptor, connector); + checkMailLock = + new Lock( + new AsyncStoreConnectorBase64(connector), + ConstantsStorage.NEW_IN_JSON + "/checkMail.lock", + ConstantsStorage.MAIL_CHECK_LOCK_TIME_SECONDS, + ConstantsStorage.MAIL_CHECK_LOCK_TIME_ALLOWED_BEFORE_RELOCK_SECONDS + ); + } + + public boolean isChecking () + { + return checking; + } + + @Override + public void check () + { + if (checking) + return; + + log.debug("check"); + + in = null; + out = null; + totalFilesFound = numFilesWillCheck = 0; + + CallbackChain callbackChain = new CallbackChain() + .addCallback(master.getEventPropagator().signal_(Events.CheckBegin, (Object[])null)) + .addCallback(master.getCacheManager().update_()) + .addCallback(checkMailLock.lock_()) + .addCallback(check_directory_(ConstantsStorage.NEW_IN_JSON, Direction.IN)) + .addCallback(check_directory_(ConstantsStorage.NEW_OUT_JSON, Direction.OUT)) + .addCallback(check_combine_()) + .addCallback(new CyclicalFileCheck(this)) + .addCallback(check_end_()); + + // we do not unlock the checkMailLock, because I think this throws + // too many variables into the equation + + checking = true; + callbackChain.invoke(); + } + + public Callback check_end_() + { + return + new CallbackDefault() { + + @Override + public void onSuccess(Object... arguments) throws Exception { + checking = false; + + master.getEventPropagator().signal(Events.CheckSuccess, (Object[])null); + master.getEventPropagator().signal(Events.CheckEnd, (Object[])null); + } + + public void onFailure(Exception e) { + checking = false; + + master.getEventPropagator().signal(Events.CheckFailure, "Failed"); + master.getEventPropagator().signal(Events.CheckEnd, (Object[])null); + } + }; + } + + public Callback check_directory_ (String path, Direction direction) + { + log.debug("check directory", path); + + return store.list_(path + "/").addCallback(check_accumulate_(direction)); + } + + public Callback check_accumulate_ (Direction direction) + { + return new CallbackDefault(direction) { + public void onSuccess(Object... arguments) throws Exception { + Direction direction = V(0); + if (direction == Direction.IN) + in = arguments[0]; + else + out = arguments[0]; + + next(); + } + }; + } + + public Callback check_combine_ () + { + return new CallbackDefault() { + public void onSuccess(Object...arguments) throws Exception { + + log.debug("check_combine"); + checkMailLock.testLock((List)in); + + files = new ArrayList(); + if (in instanceof List) + { + List lin = (List)in; + log.debug("check_combine","in", lin.size()); + + for (FileInfo file : lin) + { + if (file.path.endsWith(".lock")) + continue; + + if (master.getArrivalsProcessor().alreadyProcessed(file.path)) + continue; + + log.debug("found file",file); + + files.add(file); + file.user = Direction.IN; + } + } + if (out instanceof List) + { + List lout = (List)out; + log.debug("check_combine","out", lout.size()); + for (FileInfo file : lout) + { + if (master.getArrivalsProcessor().alreadyProcessed(file.path)) + continue; + + files.add(file); + file.user = Direction.OUT; + } + } + + totalFilesFound = files.size(); + + Collections.sort(files, new FileInfo.SortByDateAscending()); + List segment = files.subList(0, Math.min(files.size(), NUM_FILES_IN_CHECK)); + files = segment; + + numFilesWillCheck = files.size(); + + log.debug("check_combine","final", files.size()); + next(); + } + }; + } +} diff --git a/java/core/src/core/mail/client/ArrivalsProcessor.java b/java/core/src/core/mail/client/ArrivalsProcessor.java new file mode 100644 index 0000000..a9bad26 --- /dev/null +++ b/java/core/src/core/mail/client/ArrivalsProcessor.java @@ -0,0 +1,357 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import java.util.ArrayList; + +import java.util.Date; +import java.util.List; + +/* +import javax.mail.Address; +import javax.mail.BodyPart; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import javax.mail.internet.MimeMessage.RecipientType; +*/ + +import core.callback.Callback; +import core.callback.CallbackDefault; +import core.callbacks.Single; +import core.constants.ConstantsMailJson; +import core.crypt.CryptorAES; +import core.util.Base64; +import core.util.JSON_; +import core.util.JSON_.JSONException; +import core.util.LogNull; +import core.util.Strings; + +import mail.client.cache.ID; +import mail.client.model.Attachment; +import mail.client.model.Attachments; +import mail.client.model.Body; +import mail.client.model.Dictionary; +import mail.client.model.Direction; +import mail.client.model.Header; +import mail.client.model.Identity; +import mail.client.model.Mail; +import mail.client.model.Recipients; +import mail.client.model.TransportState; +import mail.client.model.UnregisteredIdentity; + + +public class ArrivalsProcessor extends Servent +{ + static LogNull log = new LogNull(ArrivalsProcessor.class); + + @SuppressWarnings("serial") + static class DuplicateMailException extends Exception {}; + + + public void processSuccess (Direction direction, String externalKey, Date date, byte[] inputStream) throws Exception + { + Indexer indexer = master.getIndexer(); + + log.debug("processSuccess"); + try + { + Mail mail = processStream (direction, externalKey, date, inputStream); + + indexer.addMail(mail); + } + catch (DuplicateMailException e) + { + log.debug("Marking duplicate"); + indexer.addDuplicate(externalKey, date); + } + catch (Exception e) + { + throw e; + } + } + + public static String decode (String s) + { + return Strings.toString(Base64.decode(s)); + } + + public static String getFirstHeader(Object message, String s, String def) throws JSONException + { + Object a = JSON_.getArray(message, "headers"); + + if (a != null) + { + s = s.toLowerCase(); + for (int i=0; i", "$1"); + log.debug("found embedded id", id); + + if (id != null) + { + CacheManager cacheManager = master.getCacheManager(); + + Mail mail = cacheManager.getMail(ID.fromString(id)); + if (mail!= null) + { + log.debug("cache has the mail"); + mail.apply(setExternalKeys_(externalKey, originalKey)); + } + } + } + + throw new DuplicateMailException(); + } + } + + String subject = getFirstHeader(content, "subject", ""); + + Identity author = null; + Recipients recipients = new Recipients (); + + if (JSON_.has(message, ConstantsMailJson.Addresses)) + { + Object addresses = JSON_.getObject(message, ConstantsMailJson.Addresses); + if (JSON_.has(addresses, ConstantsMailJson.From)) + { + Object jAddresses = JSON_.getArray(addresses, ConstantsMailJson.From); + if (JSON_.size(jAddresses) > 0) + { + Object jia = JSON_.getObject(jAddresses, 0); + author = master.getAddressBook().getIdentity( + new UnregisteredIdentity( + JSON_.has(jia,ConstantsMailJson.Name) ? + decode(JSON_.getString(jia,ConstantsMailJson.Name)) : null, + JSON_.has(jia, ConstantsMailJson.Email) ? + decode(JSON_.getString(jia, ConstantsMailJson.Email)) : null + ) + ); + } + } + + String[] buckets = { ConstantsMailJson.To, ConstantsMailJson.Cc, ConstantsMailJson.Bcc, ConstantsMailJson.ReplyTo }; + + for (String bucket : buckets) + { + if (JSON_.has(addresses,bucket)) + { + Object jAddresses = JSON_.getArray(addresses, bucket); + for (int i=0; i")); + + Body body = new Body(); + Attachments attachments = new Attachments(); + + List contents = new ArrayList(); + if (content != null) + contents.add(content); + + while (contents.size() > 0) + { + Object c = contents.get(0); + contents.remove(0); + + String clazz = JSON_.getString(c, ConstantsMailJson.Class); + Object value = JSON_.has(c, ConstantsMailJson.Value) ? + JSON_.get(c, ConstantsMailJson.Value) : null; + String contentType = getFirstHeader(c, "Content-Type", "text/plain"); + + if (clazz.equals(ConstantsMailJson.String)) + { + String valueString = JSON_.asString(value); + + /* + if (contentType.startsWith("encrypted/block")) + { + String jsonBlock = Strings.toString(embeddedCryptor.decrypt(Base64.decode(valueString))); + Object json = JSON_.parse(jsonBlock); + + subject = JSON_.has(json, "subject") ? JSON_.getString(json, "subject") : null; + contents.add(JSON_.getObject(json, "content")); + } + else + */ + if (contentType.startsWith("text/html")) + { + if (body.getHTML() == null) + body.setHTML(valueString); + } + else + if (contentType.startsWith("text/plain")) + { + if (body.getText() == null) + body.setText(valueString); + } + } + else + if (clazz.equals(ConstantsMailJson.MultiPart)) + { + Object valueParts = (Object)value; + for (int i=0; i +{ + static LogNull log = new LogNull(CacheManager.class); + + Cache masterCache; + IndexedCache cacheMail; + IndexedCache cacheConversation; + IndexedCache cacheFolder; + Settings settings; + + boolean isCaching = false; + + ModelFactory itemFactory; + + StoreLibrary library; + AsyncStoreConnector connector; + + public CacheManager (CryptorSeed cryptorSeed, AsyncStoreConnector connector) + { + this.connector = connector; + this.itemFactory = new ModelFactory(this); + library = new StoreLibrary(cryptorSeed, new StoreFactory(85 * 1024), connector); + } + + public void onModelDirty () + { + log.debug("markDirty"); + master.getEventPropagator().signal(Events.CacheDirty, (Object[])null); + } + + public void start () + { + log.debug("start"); + + JSON json = getMaster().getJSON(); + + this.masterCache = new Cache( + null, + new MasterCacheSerializer(json), + library.instantiate("I", null, false) // false because I'm manually initiating below .start(...) + ); + + this.settings = new Settings(this); + this.settings.setId(Constants.SETTINGS_ID); + this.masterCache.link(settings); + + ItemSerializer itemSerializer = new ModelSerializer(json); + + this.cacheMail = new IndexedCache( + new ItemCacheFactory ("M", library, itemFactory, itemSerializer) + ); + this.cacheMail.setId(Constants.MAIL_ID); + masterCache.link(cacheMail); + + this.cacheConversation = new IndexedCache( + new ItemCacheFactory ("C", library, itemFactory, itemSerializer) + ); + this.cacheConversation.setId(Constants.CONVERSATION_ID); + masterCache.link(cacheConversation); + + this.cacheFolder = new IndexedCache( + new ItemCacheFactory ("F", library, itemFactory, itemSerializer) + ); + this.cacheFolder.setId(Constants.FOLDER_ID); + masterCache.link(cacheFolder); + + //------------------------------------------------------------- + + Callback countDown = + new CountDown( + 4, + getMaster().getEventPropagator().signal_(Events.Initialize_IndexedCacheLoadComplete) + ); + + // some how I need to watch when the indexCaches have been loaded + settings.apply(new Split(countDown)); + cacheMail.apply(new Split(countDown)); + cacheConversation.apply(new Split(countDown)); + cacheFolder.apply(new Split(countDown)); + + //------------------------------------------------------------- + + library.start( + new SuccessFailure( + getMaster().getEventPropagator().signal_(Events.Initialize_IndexedCacheAcquired, (Object[])null), + getMaster().getEventPropagator().signal_(Events.Initialize_IndexedCacheLoadFailed, (Object[])null) + ) + ); + } + + public void deserializeIndexCaches () + { + log.debug("deserializeIndexCaches"); + } + + public void firstRunInitialization () + { + log.debug("firstRunInitialization"); + + // we create the cache the root folders are in + cacheFolder.newCache(Indexer.KnownFolderIds.RootCache); + + cacheMail.markCreate(); + cacheConversation.markCreate(); + cacheFolder.markCreate(); + masterCache.markCreate(); + + settings.markCreate(); + settings.set(Settings.VERSION, Settings.CURRENT_VERSION); + } + + public Settings getSettings () + { + return settings; + } + + public String createUIDL (ID id) + { + return "<" + id + ConstantsClient.ATHOST + ">"; + } + + public Mail newMail(Header header, Body body, Attachments attachments) throws Exception + { + log.debug("newMail"); + + + Mail mail = (Mail)itemFactory.instantiate(Type.Mail); + cacheMail.put(mail); + + mail.setHeader(header); + + // if there is no UIDL we supply one, based off of the external key + if (header.getUIDL()==null) + header.setUIDL(createUIDL(mail.getId())); + + mail.setBody(body); + mail.setAttachments(attachments); + + master.getEventPropagator().signalOnce(Events.NewMail, mail); + return mail; + } + + public void deleteMail(Mail mail) + { + log.debug("deleteMail"); + master.getEventPropagator().signalOnce(Events.DeleteMail, mail); + mail.markDeleted(); + } + + public Conversation newConversation (Mail mail) throws Exception + { + log.debug("newConversation"); + + Conversation conversation = (Conversation)itemFactory.instantiate(Type.Conversation); + cacheConversation.put(conversation); + + conversation.addItem(mail); + master.getEventPropagator().signalOnce(Events.NewConversation, conversation); + + return conversation; + } + + public void deleteConversation(Conversation conversation) + { + log.debug("deleteConversation"); + master.getEventPropagator().signalOnce(Events.DeleteConversation, conversation); + conversation.markDeleted(); + } + + public Folder newFolder(Type type, FolderDefinition folderDefinition) + { + log.debug("newFolder", type); + Folder folder = (Folder)itemFactory.instantiate(type); + cacheFolder.put(folder); + + folder.setFolderDefinition(folderDefinition); + master.getEventPropagator().signalOnce(Events.NewFolder, folder); + + return folder; + } + + public Folder linkFolder(ID id, Type type, FolderDefinition folderDefinition) + { + log.debug("newFolder", type, id); + Folder folder = (Folder)itemFactory.instantiate(type); + cacheFolder.link(id, folder); + + folder.setFolderDefinition(folderDefinition); + master.getEventPropagator().signalOnce(Events.NewFolder, folder); + + return folder; + } + + public Mail getMail(ID uid) + { + return (Mail) cacheMail.getAndAcquire(Type.Mail, uid); + } + + public void putMail(Mail m) + { + cacheMail.put(m); + } + + public Conversation getConversation(ID uid) + { + return (Conversation) cacheConversation.getAndAcquire(Type.Conversation, uid); + } + + public void putConversation(Conversation c) + { + cacheConversation.put(c); + } + + public Folder getFolder(Type type, ID id) + { + return (Folder)cacheFolder.getAndAcquire(type, id); + } + + public void putFolder(Folder f) + { + cacheFolder.put(f); + } + + public boolean isFullyCached () + { + return (!library.hasDirtyChildren() && !masterCache.hasDirtyChildren()); + } + + public Callback onCacheFinished_ () + { + return new CallbackDefault() { + public void onSuccess(Object... arguments) throws Exception { + isCaching = false; + if (isFullyCached()) + master.getEventPropagator().signal(Events.CacheClean, (Object[])null); + + master.getEventPropagator().signal(Events.CacheSuccess, (Object[])null); + master.getEventPropagator().signal(Events.CacheEnd, (Object[])null); + + next(arguments); + } + + public void onFailure (Exception e) + { + isCaching = false; + master.getEventPropagator().signal(Events.CacheFailure, e); + master.getEventPropagator().signal(Events.CacheEnd, (Object[])null); + + next(e); + } + }; + } + + public void flush () + { + if (isCaching) + return; + + if (getMaster().getArrivalsMonitor().isChecking()) + return; + + if (isFullyCached()) + return; + + doFlush(); + } + + protected void doFlush () + { + log.debug("doFlush"); + + isCaching = true; + master.getEventPropagator().signal(Events.CacheBegin, (Object[])null); + + masterCache.debug_() + .addCallback(masterCache.flush_()) + .addCallback(masterCache.checkClean_()) + .addCallback(masterCache.debug_()) + .addCallback(library.flush_()) + .addCallback(masterCache.debug_()) + .addCallback(onCacheFinished_()) + .invoke(); + } + + public Callback update_ () + { + return library.update_(false); + } + + public void update () + { + update_().invoke(); + } + + public void debug () + { + masterCache.debug_().invoke(); + } + + public void onSettingsChanged (Settings settings) + { + getMaster().getIdentity().setName(settings.get(ConstantsSettings.USERNAME)); + } +} diff --git a/java/core/src/core/mail/client/Client.java b/java/core/src/core/mail/client/Client.java new file mode 100644 index 0000000..eddc36e --- /dev/null +++ b/java/core/src/core/mail/client/Client.java @@ -0,0 +1,137 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import mail.client.cache.JSON; +import mail.client.model.AddressBook; +import mail.client.model.Identity; +import mail.client.model.UnregisteredIdentity; +import core.connector.async.AsyncStoreConnector; +import core.connector.async.AsyncStoreConnectorBase64; +import core.connector.async.AsyncStoreConnectorEncrypted; +import core.connector.dropbox.ClientInfoDropbox; +import core.connector.dropbox.async.ConnectorDropbox; +import core.connector.s3.ClientInfoS3; +import core.connector.s3.async.S3Connector; +import core.constants.ConstantsEnvironmentKeys; +import core.constants.ConstantsClient; +import core.constants.ConstantsStorage; +import core.crypt.Cryptor; +import core.crypt.CryptorNone; +import core.crypt.CryptorRSA; +import core.crypt.CryptorRSAAES; +import core.crypt.CryptorSeed; +import core.crypt.CryptorRSAFactoryEnvironment; +import core.util.Environment; +import core.util.HttpDelegate; +import core.util.LogNull; +import core.util.LogOut; + +public class Client +{ + static LogNull log = new LogNull (Client.class); + public static String HOST = ConstantsClient.KEY_AUTH_HOST; + public static int KEYSERVER_PORT = ConstantsClient.KEY_AUTH_PORT; + public static int SENDMAIL_PORT = 8080; + + HttpDelegate httpDelegate; + Master master; + + public static Client start (Environment e, String identityString, HttpDelegate httpDelegate, EventDispatcher eventDispatcher) throws Exception + { + return new Client(e, identityString, httpDelegate, eventDispatcher); + } + + public Client (Environment e, String identityString, HttpDelegate httpDelegate, EventDispatcher eventDispatcher) throws Exception + { + ArrivalsMonitor arrivalsMonitor = null; + this.httpDelegate = httpDelegate; + + Environment mailBoxEnvironment = e.childEnvironment("client"); + + log.debug("environment"); + for (String k : mailBoxEnvironment.keySet()) + log.debug(k, mailBoxEnvironment.get(k)); + + AddressBook addressBook = new AddressBook(); + + Identity identity = addressBook.getIdentity( + new UnregisteredIdentity(identityString) + ); + identity.setPrimary(true); + + String handler = mailBoxEnvironment.get(ConstantsEnvironmentKeys.HANDLER); + CryptorRSA cryptorRSA = CryptorRSAFactoryEnvironment.create (mailBoxEnvironment); + CryptorRSAAES cryptorRSAAES = new CryptorRSAAES(cryptorRSA); + + AsyncStoreConnector connector = null; + + if (handler.equals(ConstantsStorage.HANDLER_DROPBOX)) + { + Environment dbEnvironment = mailBoxEnvironment.childEnvironment(handler); + ClientInfoDropbox clientInfo = new ClientInfoDropbox (dbEnvironment); + connector = new ConnectorDropbox(clientInfo, httpDelegate); + } + else + if (handler.equals(ConstantsStorage.HANDLER_S3)) + { + Environment s3Environment = mailBoxEnvironment.childEnvironment(handler); + ClientInfoS3 clientInfo = new ClientInfoS3 (s3Environment); + connector = new S3Connector(clientInfo, httpDelegate); + } + else + { + throw new Exception ("Unknown handler"); + } + + TrackingConnector trackingConnector = new TrackingConnector(connector); + arrivalsMonitor = new ArrivalsMonitorDefault(cryptorRSAAES, trackingConnector); + + CryptorSeed cryptorSeed = new CryptorSeed(cryptorRSA.getPrivateKey()); + + CacheManager manager = new CacheManager( + cryptorSeed, + new AsyncStoreConnectorBase64 ( + trackingConnector + ) + ); + + master = + new Master( + new Store(cryptorRSAAES, trackingConnector), + identity, + mailBoxEnvironment, + new Indexer (), + addressBook, + new ArrivalsProcessor (), + arrivalsMonitor, + eventDispatcher, + new Actions(), + new Mailer(httpDelegate), + cryptorRSAAES, + manager, + new JSON() + ); + + trackingConnector.setMaster(master); + + master.start(); + } + + public HttpDelegate getHttpDelegate () + { + return httpDelegate; + } + + public Master getMaster () + { + return master; + } + + public void stop () + { + master = null; + } +} diff --git a/java/core/src/core/mail/client/Constants.java b/java/core/src/core/mail/client/Constants.java new file mode 100644 index 0000000..a6732ef --- /dev/null +++ b/java/core/src/core/mail/client/Constants.java @@ -0,0 +1,30 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import mail.client.cache.ID; + +public class Constants +{ + static final public String DEFAULT = "Default"; + + public static final String + PART = "_PART#"; + + public static final String + REPOSITORY = "Repository", + ALL = "All", + INBOX = "Inbox", + SENT = "Sent", + SPAM = "Spam", + DRAFTS = "Drafts", + TRASH = "Trash"; + + public static final ID + SETTINGS_ID = ID.fromLong(1), + MAIL_ID = ID.fromLong(2), + CONVERSATION_ID = ID.fromLong(3), + FOLDER_ID = ID.fromLong(4); +} diff --git a/java/core/src/core/mail/client/CyclicalFileCheck.java b/java/core/src/core/mail/client/CyclicalFileCheck.java new file mode 100644 index 0000000..53710b9 --- /dev/null +++ b/java/core/src/core/mail/client/CyclicalFileCheck.java @@ -0,0 +1,119 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import java.util.Date; + +import mail.client.model.Direction; +import core.callback.Callback; +import core.callback.CallbackChain; +import core.callback.CallbackDefault; +import core.callback.CallbackWithVariables; +import core.connector.FileInfo; +import core.util.LogNull; +import core.util.LogOut; + +public class CyclicalFileCheck extends CallbackDefault +{ + static LogNull log = new LogNull(CyclicalFileCheck.class); + public int numChecked = 0; + + ArrivalsMonitorDefault monitor; + + public CyclicalFileCheck(ArrivalsMonitorDefault monitor) + { + this.monitor = monitor; + } + + public void onSuccess (Object...arguments) throws Exception { + + log.debug("check_next_file"); + ArrivalsProcessor arrivalsProcessor = monitor.master.getArrivalsProcessor(); + + FileInfo file = null; + if (!monitor.files.isEmpty()) + { + file = monitor.files.get(0); + monitor.files.remove(0); + } + + log.debug("found file",file); + + numChecked++; + + arrivalsProcessor.getMaster().getEventPropagator().signal( + Events.CheckStep, "" + numChecked + ":" + monitor.numFilesWillCheck + ":" + monitor.totalFilesFound + ); + + /* + if (file == null || (++numChecked % monitor.NUM_FILES_BEFORE_CACHE == 0)) + { + try + { + monitor.master.getCacheManager().flush(); + } + catch (Exception e) + { + log.debug("ignoring during flush, because don't want to interrupt arrivals", e); + } + } + */ + + if (file != null) + { + log.debug("going to process file",file.path); + + monitor.checkMailLock.relock_().invoke(); + + monitor.store.get_() + .addCallback(new CheckFileResult((Direction)file.user, file.path, file.date, this)) + .invoke(file.path); + } + else + { + log.debug("no file, to proceeding in callback chain"); + next(); + } + } + + class CheckFileResult extends CallbackDefault + { + CheckFileResult (Direction direction, String path, Date date, Callback callback) + { + super(direction, path, date); + this.callback = callback; + } + + public void onSuccess(Object...arguments) throws Exception { + Direction direction = V(0); + String path = V(1); + Date date = V(2); + + log.debug("check_file_result", path); + ArrivalsProcessor arrivalsProcessor = monitor.master.getArrivalsProcessor(); + + byte[] data = (byte[])arguments[0]; + + log.debug ("handling ", path); + + try + { + arrivalsProcessor.processSuccess( + direction, + path, + date, + data + ); + } + catch (Exception e) + { + e.printStackTrace(); + arrivalsProcessor.processFailure(direction, path, date, e); + } + + next(); + } + }; +} diff --git a/java/core/src/core/mail/client/EventDispatcher.java b/java/core/src/core/mail/client/EventDispatcher.java new file mode 100644 index 0000000..d97b256 --- /dev/null +++ b/java/core/src/core/mail/client/EventDispatcher.java @@ -0,0 +1,62 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import core.util.LogNull; +import core.util.Pair; + + +public class EventDispatcher extends EventPropagator +{ + static LogNull log = new LogNull(EventDispatcher.class); + List> eventQueue = new LinkedList>(); +// Collections.synchronizedList(new LinkedList>() +// ); + + Set onced = new HashSet(); + + public synchronized void prepareForDispatch () + { + eventQueue.add(null); + onced.clear(); + } + + public void dispatchEvents () + { + prepareForDispatch(); + + Pair next = null; + while ((next = eventQueue.get(0))!=null) + { + eventQueue.remove(0); + doSignal(next.first, next.second); + } + + eventQueue.remove(0); + } + + @Override + public void signalOnce (String event, Object...parameters) + { + log.debug("signalOnce", event); + if (onced.contains(event)) + return; + + onced.add(event); + + signal(event, parameters); + } + + @Override + public void signal (String event, Object... parameters) + { + log.debug("signal",event,parameters); + eventQueue.add(new Pair(event, parameters)); + } +} diff --git a/java/core/src/core/mail/client/EventPropagator.java b/java/core/src/core/mail/client/EventPropagator.java new file mode 100644 index 0000000..06b5a02 --- /dev/null +++ b/java/core/src/core/mail/client/EventPropagator.java @@ -0,0 +1,104 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import core.callback.Callback; +import core.callback.CallbackDefault; +import core.util.LogNull; +import core.util.LogOut; +import core.util.Pair; + +public class EventPropagator +{ + static LogNull log = new LogNull(EventPropagator.class); + + public static final String INVOKE = "__INVOKE__"; + + Map > > listeners = new HashMap> >(); + + public void add (String event, Object tag, Callback callback) + { + if (!listeners.containsKey(event)) + listeners.put(event, new ArrayList>()); + + listeners.get(event).add(new Pair(tag, callback)); + } + + public void remove (String event, Object tag) + { + List> callbacks = listeners.get(event); + if (callbacks != null) + { + ArrayList> remove = new ArrayList>(); + + for (Pair callback : callbacks) + { + if (callback.first.equals(tag)) + remove.add(callback); + } + + for (Pair callback : remove) + { + callbacks.remove(callback); + } + } + } + + public void signalOnce (String event, Object...parameters) + { + signal(event, parameters); + } + + public void signal (String event, Object...parameters) + { + log.debug("signal",event, parameters); + + doSignal(event, parameters); + } + + public Callback signal_ (String event, Object...parameters) + { + log.debug("signal_",event, parameters); + + return new CallbackDefault(event, parameters) { + public void onSuccess(Object... arguments) throws Exception { + String event = V(0); + Object[] parameters = V(1); + signal(event, parameters); + + next(); + } + }; + } + + protected void doSignal (String event, Object...parameters) + { + log.debug("doSignal",event, parameters); + + if (event.equals(INVOKE)) + { + Callback c = (Callback)parameters[0]; + Object[] params = new Object[parameters.length-1]; + System.arraycopy(parameters, 1, params, 0, parameters.length-1); + c.invoke(params); + } + + List> callbacks = listeners.get(event); + if (callbacks != null) + { + for (Pair callback : callbacks) + { + callback.second.invoke(parameters); + } + } + } +} diff --git a/java/core/src/core/mail/client/EventType.java b/java/core/src/core/mail/client/EventType.java new file mode 100644 index 0000000..d1b9d23 --- /dev/null +++ b/java/core/src/core/mail/client/EventType.java @@ -0,0 +1,13 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +public enum EventType { + INITIAL_CACHE_RETRIEVAL_FINISHED, + REFRESH, + REPLY, + ARRIVALS_COMPLETED, + MAIL_RETRIEVED +}; diff --git a/java/core/src/core/mail/client/Events.java b/java/core/src/core/mail/client/Events.java new file mode 100644 index 0000000..aaf0f3e --- /dev/null +++ b/java/core/src/core/mail/client/Events.java @@ -0,0 +1,52 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +public class Events +{ + public static final String + Login = "onLogin", + Initialize_Start = "Initialize_Start", + Initialize_IndexedCacheAcquired = "Initialize_IndexedCacheAcquired", + Initialize_IndexedCacheLoadFailed = "Initialize_IndexedCacheLoadFailed", + Initialize_IndexedCacheLoadComplete = "Initialize_IndexedCacheLoadComplete", + Initialize_FolderLoadComplete = "Initialize_FolderLoadComplete", + FirstRunInitialization = "onFirstRunInitialization", + Initialized = "onInitialized", + NewMail = "onNewMail", + DeleteMail = "onDeleteMail", + LoadMail = "onLoadMail", + LogNull = "onLogNull", + NewFolder = "onNewFolder", + DeleteFolder = "onDeleteFolder", + LoadFolder = "onLoadFolder", + LoadFolderPart = "onLoadFolderPart", + NewConversation = "onNewConversation", + ChangedConversation = "onChangedConversation", + DeleteConversation = "onDeleteConversation", + LoadConversation = "onLoadConversation", + LoadConversationBlock = "onLoadConversationBlock", + SendSucceeded = "onSendSucceeded", + SendFailed = "onSendFailed", + CheckRequest = "onCheckRequest", + CheckBegin = "onCheckBegin", + CheckSuccess = "onCheckSuccess", + CheckFailure = "onCheckFailure", + CheckEnd = "onCheckEnd", + CheckStep = "onCheckStep", + UploadBegin = "onUploadBegin", + UploadEnd = "onUploadEnd", + DownloadBegin = "onDownloadBegin", + DownloadEnd = "onDownloadEnd", + OriginalLoaded = "onOriginalLoaded", + CacheDirty = "onCacheDirty", + CacheClean = "onCacheClean", + CacheFailure = "onCacheFailure", + CacheBegin = "onCacheBegin", + CacheEnd = "onCacheEnd", + CacheSuccess = "onCacheSuccess", + LoadAttachments = "onAttachmentsLoaded", + LoadAttachmentsFailed = "onAttachmentsLoadedFailed"; +} diff --git a/java/core/src/core/mail/client/Indexer.java b/java/core/src/core/mail/client/Indexer.java new file mode 100644 index 0000000..f47ea1f --- /dev/null +++ b/java/core/src/core/mail/client/Indexer.java @@ -0,0 +1,435 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import java.util.Date; +import java.util.List; + +import core.callback.Callback; +import core.callback.CallbackChain; +import core.callback.CallbackDefault; +import core.callbacks.CountDown; +import core.callbacks.Single; +import core.callbacks.Split; +import core.util.LogNull; +import core.util.LogOut; +import mail.client.cache.ID; +import mail.client.cache.Type; +import mail.client.model.Body; +import mail.client.model.Conversation; +import mail.client.model.Dictionary; +import mail.client.model.Direction; +import mail.client.model.Folder; +import mail.client.model.FolderDefinition; +import mail.client.model.FolderFilter; +import mail.client.model.FolderMaster; +import mail.client.model.FolderRepository; +import mail.client.model.FolderSet; +import mail.client.model.Header; +import mail.client.model.Mail; +import mail.client.model.TransportState; + +public class Indexer extends Servent +{ + static LogNull log = new LogNull(Indexer.class); + + public static class KnownFolderIds { + static final + ID RootCache = ID.fromLong(1), + SystemFoldersId = ID.combine(RootCache, ID.fromLong(1)), + RepositoryId = ID.combine(RootCache, ID.fromLong(2)), + AllId = ID.combine(RootCache, ID.fromLong(3)), + InboxId = ID.combine(RootCache, ID.fromLong(4)), + SentId = ID.combine(RootCache, ID.fromLong(5)), + DraftsId = ID.combine(RootCache, ID.fromLong(6)), + SpamId = ID.combine(RootCache, ID.fromLong(7)), + TrashId = ID.combine(RootCache, ID.fromLong(8)), + + UserFoldersId = ID.combine(RootCache, ID.fromLong(100)); + + }; + + FolderMaster systemFolders; + FolderSet userFolders; + FolderRepository repository; + FolderFilter spam; + + public Indexer () + { + } + + public void firstRunInitialization () + { + log.debug("firstRunInitialization"); + + CacheManager cache = master.getCacheManager(); + + systemFolders = (FolderMaster) + cache.linkFolder( + KnownFolderIds.SystemFoldersId, + Type.FolderMaster, + new FolderDefinition("system") + ); + systemFolders.markCreate(); + + userFolders = (FolderSet) + cache.linkFolder( + KnownFolderIds.UserFoldersId, + Type.FolderFilterSet, + new FolderDefinition("user") + ); + userFolders.markCreate(); + + repository = (FolderRepository)cache.linkFolder( + KnownFolderIds.RepositoryId, + Type.FolderRepository, + new FolderDefinition(Constants.REPOSITORY) + ); + repository.markCreate(); + systemFolders.addFolder(repository); + + Folder fs; + fs = cache.linkFolder( + KnownFolderIds.AllId, + Type.FolderFilter, + new FolderDefinition(Constants.ALL) + .setState( + null, + TransportState.fromList(TransportState.TRASH,TransportState.SPAM) + ) + ); + fs.markCreate(); + systemFolders.addFolder(fs); + + fs = cache.linkFolder( + KnownFolderIds.InboxId, + Type.FolderFilter, + new FolderDefinition(Constants.INBOX) + .setState( + TransportState.fromList(TransportState.RECEIVED), + TransportState.fromList(TransportState.TRASH,TransportState.SPAM) + ) + ); + fs.markCreate(); + systemFolders.addFolder(fs); + + fs = cache.linkFolder( + KnownFolderIds.SentId, + Type.FolderFilter, + new FolderDefinition(Constants.SENT) + .setState( + TransportState.fromList(TransportState.SENT), + TransportState.fromList(TransportState.TRASH,TransportState.SPAM) + ) + ); + fs.markCreate(); + systemFolders.addFolder(fs); + + fs = cache.linkFolder( + KnownFolderIds.DraftsId, + Type.FolderFilter, + new FolderDefinition(Constants.DRAFTS) + .setState( + TransportState.fromList(TransportState.DRAFT, TransportState.SENDING), + TransportState.fromList(TransportState.TRASH, TransportState.SPAM) + ) + ); + fs.markCreate(); + systemFolders.addFolder(fs); + + spam = (FolderFilter) + cache.linkFolder( + KnownFolderIds.SpamId, + Type.FolderFilter, + new FolderDefinition(Constants.SPAM) + .setState( + TransportState.fromList(TransportState.SPAM), + TransportState.fromList(TransportState.TRASH) + ) + .setBayesianDictionary(new Dictionary()) + .setAutoBayesian(false) + ); + + spam.markCreate(); + systemFolders.addFolder(spam); + + fs = cache.linkFolder( + KnownFolderIds.TrashId, + Type.FolderFilter, + new FolderDefinition(Constants.TRASH) + .setState( + TransportState.fromList(TransportState.TRASH), + null + ) + ); + fs.markCreate(); + systemFolders.addFolder(fs); + + master.getEventPropagator().signal(Events.Initialize_FolderLoadComplete, (Object[])null); + } + + public void start () + { + log.debug("indexer starting.."); + + CacheManager cache = master.getCacheManager(); + + systemFolders = (FolderMaster)cache.getFolder( + Type.FolderMaster, + KnownFolderIds.SystemFoldersId + ); + + systemFolders.apply( + new CallbackDefault() { + public void onSuccess(Object... arguments) throws Exception { + onMainFolderSucceeded((Folder)arguments[0]); + } + + public void onFailure(Exception e) { + onMainFolderFailed(e); + } + } + ); + } + + public void onMainFolderSucceeded (Folder f) + { + log.debug("onMainFolderSucceeded"); + systemFolders = (FolderMaster)f; + CacheManager cache = master.getCacheManager(); + repository = (FolderRepository)cache.getFolder(Type.FolderRepository, KnownFolderIds.RepositoryId); + spam = (FolderFilter)cache.getFolder(Type.FolderFilter, KnownFolderIds.SpamId); + userFolders = (FolderSet)cache.getFolder(Type.FolderFilterSet, KnownFolderIds.UserFoldersId); + + // cause subfolders to instantiate right away + List folders = systemFolders.getFolders(); + Callback countDown = + new CountDown( + folders.size(), + getMaster().getEventPropagator().signal_(Events.Initialize_FolderLoadComplete) + ); + + for (Folder folder : folders) + folder.apply(new Split(countDown)); + } + + public void onMainFolderFailed (Exception e) + { + log.debug("onMainFolderFailed"); + log.exception(e); + } + + public synchronized Conversation addMail (Mail mail) throws Exception + { + Header header = mail.getHeader(); + boolean isNewConversation; + Conversation conversation = repository.getMatchingConversation(header); + if (conversation != null) + { + log.debug("founding matching conversation"); + conversation.addItem(mail); + isNewConversation = false; + } + else + { + log.debug("new conversation"); + conversation = master.getCacheManager().newConversation(mail); + isNewConversation = true; + } + + // before we do anything we do the spam detection + conversation.getHeader().getTransportState().mark( + TransportState.SPAM, spam.getFolderDefinition().bayesianMatches(conversation) + ); + + if (isNewConversation) + addConversation(conversation); + else + conversationChanged (conversation); + + + // if the mail is new, no external key yet + if (mail.getHeader().getExternalKey()!=null) + { + log.debug("adding externalKey", mail.getHeader().getExternalKey()); + systemFolders.addExternalKey(mail.getHeader().getExternalKey(), mail.getHeader().getDate()); + } + + if (mail.getHeader().getUIDL() != null) + { + log.debug("adding UIDL", mail.getHeader().getUIDL()); + systemFolders.addUIDL(mail.getHeader().getUIDL(), mail.getHeader().getDate()); + } + + return conversation; + } + + protected void addFailure (Mail mail) throws Exception + { + addMail (mail); + } + + protected void addFailure (String externalKey, Date date) + { + systemFolders.addExternalKey(externalKey, date); + } + + public void addFailure (Direction direction, String path, Date date, Exception e) + { + try + { + e.printStackTrace(); + + Header header = new Header(); + header.setSubject("Mail failed parsing. Look at original for file."); + header.setExternalKey(path); + header.setDate(date); + if (direction == Direction.IN) + header.setTransportState(TransportState.fromList(TransportState.RECEIVED)); + else + header.setTransportState(TransportState.fromList(TransportState.SENT)); + + Body body = new Body(); + body.setText("Failed to load: " + e); + + Mail mail = master.getCacheManager().newMail(header, body, null); + addFailure(mail); + } + catch (Exception em) + { + em.printStackTrace(); + addFailure(path, date); + } + } + + public void addDuplicate (String externalKey, Date date) + { + systemFolders.addExternalKey(externalKey, date); + } + + public boolean containsExternalKey (String externalKey) + { + return systemFolders.containsExternalKey(externalKey); + } + + public boolean containsUIDL(String uidl) + { + return systemFolders.containsUIDL(uidl); + } + + public synchronized void addConversation (Conversation conversation) + { + for (Folder e : systemFolders.getFolders()) + e.conversationAdded(conversation); + + for (Folder e : userFolders.getFolders()) + e.conversationAdded(conversation); + } + + public synchronized void removeConversation (Conversation conversation) + { + for (Folder e : systemFolders.getFolders()) + e.conversationDeleted(conversation); + + for (Folder e : userFolders.getFolders()) + e.conversationDeleted(conversation); + } + + public synchronized void conversationChanged (Conversation conversation) + { + if (conversation != null) + { + for (Folder f : systemFolders.getFolders()) + f.conversationChanged(conversation); + + for (Folder e : userFolders.getFolders()) + e.conversationChanged(conversation); + } + + master.getEventPropagator().signalOnce(Events.ChangedConversation); + } + + public void replyMail (Conversation conversation, Mail mail) + { + systemFolders.addUIDL(mail.getHeader().getUIDL(), mail.getHeader().getDate()); + + conversationChanged(conversation); + } + + public Conversation newMail (Mail mail) throws Exception + { + return addMail(mail); + } + + public Folder getSystemFolder (String folderName) + { + for (Folder e : systemFolders.getFolders()) + if (e.isLoaded()) + if (e.getFolderDefinition().getName().equals(folderName)) + return e; + + return null; + } + + public FolderFilter getUserFolder (String folderName) + { + for (Folder e: userFolders.getFolders()) + if (e.isLoaded()) + if (e.getFolderDefinition().getName().equals(folderName)) + return (FolderFilter)e; + + return null; + } + + public List getSystemFolders () + { + return systemFolders.getFolders(); + } + + public List getUserFolders () + { + return userFolders.getFolders(); + } + + public Folder getRepository () + { + return repository; + } + + public void newUserFolder(String name) + { + userFolders.addFolder( + getMaster().getCacheManager().newFolder( + Type.FolderFilter, + new FolderDefinition(name) + .setBayesianDictionary(new Dictionary()) + .setAutoBayesian(false) + .setState(null, TransportState.fromList(TransportState.SPAM, TransportState.TRASH)) + ) + ); + } + + public void deleteUserFolder(Folder userFolder) + { + userFolders.removeFolder(userFolder); + } + + public void addToUserFolder(Folder userFolder, Conversation conversation) + { + FolderFilter folder = (FolderFilter)userFolder; + folder.manuallyAdd (conversation); + } + + public void removeFromUserFolder(Folder userFolder, Conversation conversation) + { + FolderFilter folder = (FolderFilter)userFolder; + folder.manuallyRemove (conversation); + } + + public FolderSet getInbox() + { + return (FolderSet)getMaster().getCacheManager().getFolder(Type.FolderFilter, KnownFolderIds.InboxId); + } +} diff --git a/java/core/src/core/mail/client/Initializer.java b/java/core/src/core/mail/client/Initializer.java new file mode 100644 index 0000000..978bec2 --- /dev/null +++ b/java/core/src/core/mail/client/Initializer.java @@ -0,0 +1,118 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import core.callback.Callback; +import core.callback.CallbackEmpty; +import core.constants.ConstantsStorage; +import core.util.LogNull; +import core.util.LogOut; + +public class Initializer extends Servent +{ + static LogNull log = new LogNull(Initializer.class); + int numRemainingIndexCaches = 3; + + public Initializer () + { + } + + public void start () + { + EventDispatcher e = master.getEventPropagator(); + + e.add(Events.Initialize_Start, this, new Callback() { + + @Override + public void invoke(Object... arguments) + { + onInitializeStart(); + } + }); + + e.add(Events.Initialize_IndexedCacheAcquired, this, new Callback() { + + @Override + public void invoke(Object... arguments) + { + onIndexedCacheAcquired(); + } + }); + + + e.add(Events.Initialize_IndexedCacheLoadFailed, this, new Callback() { + + @Override + public void invoke(Object... arguments) + { + onIndexedCacheLoadFailed(); + } + }); + + e.add(Events.Initialize_IndexedCacheLoadComplete, this, new Callback() { + + @Override + public void invoke(Object... arguments) + { + onIndexedCacheLoadComplete(); + } + }); + + e.add(Events.Initialize_FolderLoadComplete, this, new Callback() { + + @Override + public void invoke(Object... arguments) + { + onFolderLoadComplete(); + } + }); + + e.signal(Events.Initialize_Start, (Object[])null); + } + + public void onInitializeStart() + { + log.debug("onInitializeStart"); + + master.getStore().getConnector().ensureDirectories_( + new String[] { + ConstantsStorage.CACHE, + ConstantsStorage.NEW_IN_JSON, + ConstantsStorage.NEW_OUT_JSON + } + ).invoke(); + + master.getCacheManager().start(); + } + + public void onIndexedCacheLoadFailed () + { + log.debug("onIndexedCacheLoadFailed"); + + master.getCacheManager().firstRunInitialization(); + master.getIndexer().firstRunInitialization(); + + master.getEventPropagator().signal(Events.FirstRunInitialization, (Object[])null); + } + + public void onIndexedCacheAcquired () + { + log.debug("onIndexedCacheLoadAcquire"); + master.getCacheManager().deserializeIndexCaches(); + } + + public void onIndexedCacheLoadComplete () + { + log.debug("onIndexedCacheLoadComplete"); + master.getIndexer().start(); + } + + public void onFolderLoadComplete() + { + log.debug("onFolderLoadComplete"); + + master.getEventPropagator().signal(Events.Initialized, (Object[])null); + } +} diff --git a/java/core/src/core/mail/client/Mailer.java b/java/core/src/core/mail/client/Mailer.java new file mode 100644 index 0000000..2353ade --- /dev/null +++ b/java/core/src/core/mail/client/Mailer.java @@ -0,0 +1,154 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import core.util.SecureRandom; +import java.util.HashMap; +import java.util.Map; + +import mail.client.model.Conversation; +import mail.client.model.Identity; +import mail.client.model.Mail; +import mail.client.model.Settings; +import mail.client.model.TransportState; +import mail.client.model.UnregisteredIdentity; +import core.constants.ConstantsClient; +import core.crypt.Cryptor; +import core.crypt.CryptorRSA; +import core.crypt.CryptorRSAAES; +import core.crypt.CryptorRSAFactory; +import core.callback.Callback; +import core.callback.CallbackDefault; +import core.callbacks.JSONSerialize; +import core.util.Base64; +import core.util.FastRandom; +import core.util.HttpDelegate; +import core.util.InternalResource; +import core.util.LogNull; +import core.util.LogOut; +import core.util.Pair; +import core.util.Strings; + +public class Mailer extends Servent +{ + static LogNull log = new LogNull(Mailer.class); + HttpDelegate httpDelegate; + FastRandom fastRandom = new FastRandom(); + + public Mailer (HttpDelegate httpDelegate) + { + this.httpDelegate = httpDelegate; + } + + public void sendMail (String password, Conversation conversation, Mail mail) + { + try + { + log.debug("Mailer.sendMail"); + mail.getHeader().unmarkState(TransportState.DRAFT); + mail.getHeader().markState(TransportState.SENDING); + mail.getHeader().getRecipients().registerRecipients(master.getAddressBook()); + + conversation.itemChanged(mail); + master.getIndexer().conversationChanged(conversation); + + doSend(password, conversation, mail); + } + catch (Exception e) + { + onSendFailed(conversation, mail, e); + } + } + + protected void doSend(String password, Conversation conversation, Mail mail) throws Exception + { + log.debug("Mailer.doSend"); + Map sendMap = new HashMap(); + + Identity identity = getMaster().getIdentity(); + sendMap.put("user", identity.getEmail()); + sendMap.put("password", password); + + sendMap.put("from", identity.getFull()); + sendMap.put("to", Strings.concat(mail.getHeader().getRecipients().getTo(), ",")); + sendMap.put("cc", Strings.concat(mail.getHeader().getRecipients().getCc(), ",")); + sendMap.put("bcc", Strings.concat(mail.getHeader().getRecipients().getBcc(), ",")); + sendMap.put("replyTo", Strings.concat(mail.getHeader().getRecipients().getReplyTo(), ",")); + sendMap.put("publicKey", Base64.encode(((CryptorRSA)getMaster().getCryptor()).getPublicKey())); + + /* + if (mail.isPresendEncryptable()) + { + Pair presendEncrypted = mail.presendEncrypt(); + + sendMap.put("cryptors", presendEncrypted.first); + sendMap.put("block", presendEncrypted.second); + } + else + { + */ + sendMap.put("subject", mail.getHeader().getSubject()); + sendMap.put("text", mail.getBody().getText()); + sendMap.put("html", mail.getBody().getHTML()); + /* + } + */ + + sendMap.put("messageId", mail.getHeader().getUIDL()); + log.debug("sendMap ", sendMap); + + Cryptor cryptor = new CryptorRSAAES(CryptorRSAFactory.fromResources(null, InternalResource.getResourceAsStream(getClass(), "send-truststore.jks"))); + + new JSONSerialize() + .addCallback(cryptor.encrypt_()) + .addCallback(Base64.encodeBytes_()) + .addCallback(httpDelegate.execute_( + HttpDelegate.PUT, ConstantsClient.WEB_SERVER_TOMCAT + "Send?random="+ fastRandom.nextLong(), null, false, false + )) + .addCallback(onFinish_(conversation, mail)) + .invoke(sendMap); + } + + public Callback onFinish_ (Conversation conversation, Mail mail) + { + return + new CallbackDefault(conversation, mail) { + public void onSuccess(Object...arguments) + { + onSendSucceeded((Conversation)V(0), (Mail)V(1)); + } + + public void onFailure(Exception e) + { + onSendFailed((Conversation)V(0), (Mail)V(1), e); + } + }; + } + + protected void onSendSucceeded (Conversation conversation, Mail mail) + { + log.debug("Mailer.onSendSucceeded"); + + mail.getHeader().unmarkState(TransportState.SENDING); + mail.getHeader().markState(TransportState.SENT); + + conversation.itemChanged(mail); + master.getIndexer().conversationChanged(conversation); + master.getEventPropagator().signal(Events.SendSucceeded, mail); + } + + protected void onSendFailed (Conversation conversation, Mail mail, Exception e) + { + log.debug("Mailer.onSendFailed"); + log.exception(e); + + mail.getHeader().unmarkState(TransportState.SENDING); + mail.getHeader().markState(TransportState.DRAFT); + + conversation.itemChanged(mail); + master.getIndexer().conversationChanged(conversation); + master.getEventPropagator().signal(Events.SendFailed, mail); + } +} diff --git a/java/core/src/core/mail/client/Master.java b/java/core/src/core/mail/client/Master.java new file mode 100644 index 0000000..7c46154 --- /dev/null +++ b/java/core/src/core/mail/client/Master.java @@ -0,0 +1,151 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import mail.client.cache.JSON; +import mail.client.model.AddressBook; +import mail.client.model.Identity; +import core.crypt.Cryptor; +import core.util.Environment; + +public class Master +{ + protected Initializer initializer; + protected Store store; + protected Identity identity; + protected Environment environment; + protected AddressBook addressBook; + protected Indexer indexer; + protected ArrivalsProcessor arrivalsProcessor; + protected ArrivalsMonitor arrivalsMonitor; + protected EventDispatcher eventDispatcher; + protected Actions actions; + protected Mailer mailer; + protected Cryptor cryptor; + protected CacheManager cacheManager; + protected JSON json; + + public Master ( + Store store, + Identity identity, + Environment environment, + Indexer indexer, + AddressBook addressBook, + ArrivalsProcessor arrivalsProcessor, + ArrivalsMonitor arrivalsMonitor, + EventDispatcher eventDispatcher, + Actions actions, + Mailer mailer, + Cryptor cryptor, + CacheManager cacheManager, + JSON json + ) + { + this.identity = identity; + this.environment = environment; + this.addressBook = addressBook; + + this.store = store; + store.setMaster(this); + + this.initializer = new Initializer(); + initializer.setMaster(this); + + this.indexer = indexer; + indexer.setMaster(this); + + this.arrivalsProcessor = arrivalsProcessor; + arrivalsProcessor.setMaster(this); + + this.arrivalsMonitor = arrivalsMonitor; + arrivalsMonitor.setMaster(this); + + this.eventDispatcher = eventDispatcher; + + this.actions = actions; + actions.setMaster(this); + + this.mailer = mailer; + mailer.setMaster(this); + + this.cryptor = cryptor; + + this.cacheManager = cacheManager; + cacheManager.setMaster(this); + + this.json = json; + json.setMaster(this); + } + + public void start() throws Exception + { + initializer.start(); + } + + public Store getStore () + { + return store; + } + + public Identity getIdentity () + { + return identity; + } + + public Environment getEnvironment () + { + return environment; + } + + public AddressBook getAddressBook () + { + return addressBook; + } + + public Indexer getIndexer() + { + return indexer; + } + + public CacheManager getCacheManager() + { + return cacheManager; + } + + public ArrivalsProcessor getArrivalsProcessor() + { + return arrivalsProcessor; + } + + public ArrivalsMonitor getArrivalsMonitor() + { + return arrivalsMonitor; + } + + public EventDispatcher getEventPropagator () + { + return eventDispatcher; + } + + public Actions getActions () + { + return actions; + } + + public Mailer getMailer () + { + return mailer; + } + + public Cryptor getCryptor () + { + return cryptor; + } + + public JSON getJSON () + { + return json; + } +} diff --git a/java/core/src/core/mail/client/MasterCacheSerializer.java b/java/core/src/core/mail/client/MasterCacheSerializer.java new file mode 100644 index 0000000..38a9e9d --- /dev/null +++ b/java/core/src/core/mail/client/MasterCacheSerializer.java @@ -0,0 +1,44 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import core.callback.Callback; +import mail.client.cache.IndexedCacheSerializer; +import mail.client.cache.Item; +import mail.client.cache.ItemSerializer; +import mail.client.cache.JSON; +import mail.client.model.ModelSerializer; +import mail.client.model.Settings; + +public class MasterCacheSerializer implements ItemSerializer +{ + IndexedCacheSerializer indexedCacheSerializer; + ModelSerializer settingsSerializer; + + public MasterCacheSerializer (JSON json) + { + indexedCacheSerializer = new IndexedCacheSerializer(); + settingsSerializer = new ModelSerializer(json); + } + + @Override + public Callback serialize_(Item item) + { + if (item instanceof Settings) + return settingsSerializer.serialize_(item); + + return indexedCacheSerializer.serialize_(item); + } + @Override + public Callback deserialize_(Item item) + { + if (item instanceof Settings) + return settingsSerializer.deserialize_(item); + + return indexedCacheSerializer.deserialize_(item); + } + + +} diff --git a/java/core/src/core/mail/client/Servent.java b/java/core/src/core/mail/client/Servent.java new file mode 100644 index 0000000..60eaa9f --- /dev/null +++ b/java/core/src/core/mail/client/Servent.java @@ -0,0 +1,30 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +public class Servent +{ + protected T master; + + public Servent (T master) + { + this.master = master; + } + + public Servent () + { + this.master = null; + } + + public void setMaster (T master) + { + this.master = master; + } + + public T getMaster () + { + return master; + } +} diff --git a/java/core/src/core/mail/client/Store.java b/java/core/src/core/mail/client/Store.java new file mode 100644 index 0000000..6df5238 --- /dev/null +++ b/java/core/src/core/mail/client/Store.java @@ -0,0 +1,86 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import core.callback.CallbackChain; +import core.callback.CallbackDefault; +import core.callback.CallbackWithVariables; +import core.connector.async.AsyncStoreConnector; +import core.connector.async.AsyncStoreConnectorEncrypted; +import core.crypt.Cryptor; +import mail.client.model.Mail; +import mail.client.model.Original; + +public class Store extends Servent +{ + AsyncStoreConnector connector; + + public Store(Cryptor cryptor, AsyncStoreConnector connector) + { + this.connector = new AsyncStoreConnectorEncrypted(cryptor, connector); + } + + public Original getOriginal(String path) + { + Original original = new Original(path); + connector.get_().addCallback( + new CallbackWithVariables(original){ + @Override + public void invoke(Object... arguments) + { + Original original = V(0); + if (arguments[0] instanceof Exception) + original.setException((Exception)arguments[0]); + else + original.setData((byte[])arguments[0]); + + master.getEventPropagator().signal(Events.OriginalLoaded, original); + } + } + ).invoke(path); + + return original; + } + + public AsyncStoreConnector getConnector() + { + return connector; + } + + public void loadAttachments(Mail mail) + { + connector.get_().addCallback( + new CallbackDefault(mail) + { + public void onSuccess(Object... arguments) throws Exception + { + Mail mail = V(0); + mail.getAttachments().loadFrom((byte[])arguments[0]); + master.getEventPropagator().signal(Events.LoadAttachments, mail); + } + + public void onFailure(Exception e) + { + Mail mail = V(0); + master.getEventPropagator().signal(Events.LoadAttachmentsFailed, mail); + } + } + ).invoke(mail.getHeader().getExternalKey()); + + } + + public void deleteMail (Mail mail) + { + CallbackChain chain = new CallbackChain(); + + if (mail.getHeader().getExternalKey() != null) + chain.addCallback(connector.delete_(mail.getHeader().getExternalKey())); + + if (mail.getHeader().getOriginalKey() != null) + chain.addCallback(connector.delete_(mail.getHeader().getOriginalKey())); + + chain.invoke(); + } +} diff --git a/java/core/src/core/mail/client/TrackingConnector.java b/java/core/src/core/mail/client/TrackingConnector.java new file mode 100644 index 0000000..18b9d40 --- /dev/null +++ b/java/core/src/core/mail/client/TrackingConnector.java @@ -0,0 +1,158 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client; + +import core.callback.Callback; +import core.callback.CallbackDefault; +import core.callback.CallbackWithVariables; +import core.connector.async.AsyncStoreConnector; +import core.util.LogNull; +import core.util.LogOut; + +public class TrackingConnector extends Servent implements AsyncStoreConnector +{ + AsyncStoreConnector connector; + LogNull log = new LogNull(TrackingConnector.class); + + public volatile int uploading = 0; + public volatile int downloading = 0; + + public TrackingConnector (AsyncStoreConnector connector) + { + this.connector = connector; + } + + protected synchronized void onUploadBegin() + { + uploading++; + log.debug("onUploadBegin", uploading); + + if (uploading == 1) + getMaster().getEventPropagator().signal(Events.UploadBegin, (Object[])null); + } + + protected Callback onUploadBegin_() + { + return new Callback() { + public void invoke (Object... arguments) { + onUploadBegin(); + next(arguments); + } + }; + } + + protected synchronized void onUploadEnd() + { + log.debug("onUploadEnd", uploading); + uploading--; + + if (uploading == 0) + getMaster().getEventPropagator().signal(Events.UploadEnd, (Object[])null); + } + + protected Callback onUploadEnd_() + { + return new Callback() { + public void invoke (Object... arguments) { + onUploadEnd(); + next(arguments); + } + }; + } + + protected synchronized void onDownloadBegin() + { + downloading++; + log.debug("onDownloadBegin", downloading); + + if (downloading == 1) + getMaster().getEventPropagator().signal(Events.DownloadBegin, (Object[])null); + } + + protected Callback onDownloadBegin_() + { + return new Callback() { + public void invoke (Object... arguments) { + onDownloadBegin(); + next(arguments); + } + }; + } + + protected synchronized void onDownloadEnd () + { + log.debug("onDownloadEnd", downloading); + downloading--; + + if (downloading == 0) + getMaster().getEventPropagator().signal(Events.DownloadEnd, (Object[])null); + } + + protected Callback onDownloadEnd_() + { + return new Callback() { + public void invoke (Object... arguments) { + onDownloadEnd(); + next(arguments); + } + }; + } + + //-------------------------------------------------------------- + + @Override + public Callback list_(String path) + { + return onDownloadBegin_().addCallback(connector.list_(path)).addCallback(onDownloadEnd_()).setSlowFail(); + } + + @Override + public Callback createDirectory_(String path) + { + return onUploadBegin_().addCallback(connector.createDirectory_(path)).addCallback(onUploadEnd_()).setSlowFail(); + } + + @Override + public Callback ensureDirectories_(String[] directories) + { + return onUploadBegin_().addCallback(connector.ensureDirectories_(directories)).addCallback(onUploadEnd_()).setSlowFail(); + } + + @Override + public Callback get_() + { + return onDownloadBegin_().addCallback(connector.get_()).addCallback(onDownloadEnd_()).setSlowFail(); + } + + @Override + public Callback get_(String path) + { + return onDownloadBegin_().addCallback(connector.get_(path)).addCallback(onDownloadEnd_()).setSlowFail(); + } + + @Override + public Callback put_(String path, byte[] bytes) + { + return onUploadBegin_().addCallback(connector.put_(path, bytes)).addCallback(onUploadEnd_()).setSlowFail(); + } + + @Override + public Callback put_(String path) + { + return onUploadBegin_().addCallback(connector.put_(path)).addCallback(onUploadEnd_()).setSlowFail(); + } + + @Override + public Callback move_(String from, String to) + { + return onUploadBegin_().addCallback(connector.move_(from, to)).addCallback(onUploadEnd_()).setSlowFail(); + } + + @Override + public Callback delete_(String path) + { + return onUploadBegin_().addCallback(connector.delete_(path)).addCallback(onUploadEnd_()).setSlowFail(); + } +} diff --git a/java/core/src/core/mail/client/cache/Cache.java b/java/core/src/core/mail/client/cache/Cache.java new file mode 100644 index 0000000..018c347 --- /dev/null +++ b/java/core/src/core/mail/client/cache/Cache.java @@ -0,0 +1,288 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import core.callback.Callback; +import core.callback.CallbackChain; +import core.callback.CallbackDefault; +import core.util.LogNull; +import core.util.LogOut; + +public class Cache extends ItemCollection +{ + static LogNull log = new LogNull(Cache.class); + static LogNull logState = new LogNull(""); + + Store store; + Version storeVersion; + + ItemFactory factory; + ItemSerializer serializer; + + Map items = new HashMap(); + + public Cache(ItemFactory factory, ItemSerializer serializer, Store store) + { + this.factory = factory; + this.serializer = serializer; + this.store = store; + + if (store != null) + { + storeVersion = store.getLocalVersion(); + store.getLoadCallbacks().addCallback(update_()); + } + } + + public void onCreate () + { + store.markCreate(); + } + + public void update () + { + log.debug(this, "update"); + if (store!=null && !store.getLocalVersion().equals(storeVersion)) + for (Item item : items.values()) + populate(item); + + markLoad(store.getLocalVersion()); + } + + public Callback update_() + { + return new CallbackDefault() { + public void onSuccess(Object... arguments) throws Exception { + update(); + next(arguments); + } + }; + } + + public boolean has (ID id) + { + log.trace(this, "has", id); + return items.containsKey(id); + } + + public Item getItem (Type type, ID id) + { + log.trace(this, "getItem", type, id); + return getAndAcquire(type, id); + } + + public Item getAndAcquire(Type type, ID id) + { + log.trace(this, "getAndAcquire", type, id); + Item item = get(type, id); + acquire(item); + + return item; + } + + public Item get (Type type, ID id) + { + log.trace(this, "get", type, id); + Item item = items.get(id); + if (item != null) + return item; + + item = factory.instantiate(type); + item.setId(id); + item.onExisting(); + + link(item); + + return item; + } + + public void acquire (Item item) + { + log.trace(this, "acquire", item); + if (!item.isLoaded()) + populate(item); + } + + public void populate (Item item) + { + log.debug(this, "possibly populate", item); + if (store != null) + { + log.debug(this, "store is not null"); + if (store.has(item.getId())) + { + log.debug(this, "store has", item); + Version version = store.version(item.getId()); + if (!version.equals(item.getCacheVersion())) + { + log.debug(this, "version is different", item); + if (!item.isDirty()) + { + log.debug(this, "item is not dirty", item); + if (serializer != null) + { + log.debug(this, "serializer is not null", item); + if (!storeVersion.equals(Version.DELETED)) + { + log.debug(this, "version is not deleted", item); + log.debug(this, "populating", item); + + store.get_(item.getId()) + .addCallback(serializer.deserialize_(item)) + .addCallback(item.markLoad_(version)) + .invoke(); + } + } + } + else + { + log.debug(this, "item is dirty, marking no load", item); + item.markNoLoad(version); + } + } + } + } + } + + public void put (Item item) + { + log.debug(this, "put", item); + + link (item); + item.markCreate(); + } + + public void unlink (Item item) + { + log.debug(this, "unlink", item); + + items.remove(item.getId()); + itemRemoved(item); + } + + public Callback unlink_ (Item item) + { + return new CallbackDefault(item) { + public void onSuccess(Object... arguments) throws Exception { + Item item = V(0); + unlink(item); + next(arguments); + } + }; + } + + public void link (Item item) + { + log.debug(this, "link", item); + + items.put(item.getId(), item); + itemAdded(item); + } + + public Callback flush_ () + { + log.debug(this, "flush_"); + CallbackChain chain = new CallbackChain(); + + for (Item item : items.values()) + { + if (item.isDirty()) + { + log.debug(this, "flush_ isDirty",item); + if (store.isWritable()) + { + log.debug(this, "flush store isWritable", item); + if (item.isDeleted()) + { + CallbackChain itemChain = new CallbackChain() + .addCallback(log.debug_(this, "actually removing", item)) + .addCallback(store.remove_(item.getId())) + .addCallback(unlink_(item)); + + chain.addCallback(itemChain); + } + else + { + CallbackChain itemChain = new CallbackChain() + .addCallback(log.debug_(this, "actually flushing", item)) + .addCallback(item.flush_()) + .addCallback(serializer.serialize_(item)) + .addCallback(store.put_(item.getId(), item.getLocalVersion())) + .addCallback(item.markStore_(item.getLocalVersion())); + + chain.addCallback(itemChain); + } + } + } + else + if (item.hasDirtyChildren()) + { + log.debug(this, "flush_ hasDirtyChildren",item); + + CallbackChain itemChain = new CallbackChain() + .addCallback(log.debug_(this, "actually flushing", item)) + .addCallback(item.flush_()); + + chain.addCallback(itemChain); + } + } + + chain.addCallback(onFlush_()); + chain.addCallback(this.markStore_(this.getLocalVersion())); + + return chain; + } + + public Callback checkClean_() { + return new CallbackDefault() { + public void onSuccess(Object... arguments) throws Exception { + if (isDirty() || hasDirtyChildren()) + throw new Exception("Still dirty after flush"); + + next(arguments); + } + }; + } + + + public Map getItemMap () + { + return items; + } + + public boolean isFull () + { + log.debug(this, "isFull"); + return store.isFull(); + } + + public void debug (LogOut log, String prefix) + { + final String PREFIX = " |--"; + log.debug (prefix,this); + store.debug(log, prefix+PREFIX); + for (Entry entry : items.entrySet()) + entry.getValue().debug(log, prefix+PREFIX); + } + + public void debug (LogNull log, String prefix) + { + + } + public Callback debug_ () + { + return new CallbackDefault() { + public void onSuccess(Object... arguments) throws Exception { + debug(logState, ""); + next(arguments); + } + }; + } + +} ; \ No newline at end of file diff --git a/java/core/src/core/mail/client/cache/CacheFlush.java b/java/core/src/core/mail/client/cache/CacheFlush.java new file mode 100644 index 0000000..7551aa7 --- /dev/null +++ b/java/core/src/core/mail/client/cache/CacheFlush.java @@ -0,0 +1,30 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +import core.callback.Callback; +import core.callback.CallbackChain; +import core.callback.CallbackWithVariables; +import core.connector.async.AsyncStoreConnector; +import core.util.LogNull; + +public class CacheFlush extends CallbackChain +{ + static LogNull log = new LogNull(CacheFlush.class); + + Cache cache; + String file; + AsyncStoreConnector connector; + + CacheFlush (Cache cache, AsyncStoreConnector connector, String file, CallbackChain writeByteArray) + { + this.cache = cache; + this.file = file; + this.connector = connector; + + addCallback(writeByteArray); + addCallback(connector.put_(file)); + } +} diff --git a/java/core/src/core/mail/client/cache/CacheState.java b/java/core/src/core/mail/client/cache/CacheState.java new file mode 100644 index 0000000..8f89044 --- /dev/null +++ b/java/core/src/core/mail/client/cache/CacheState.java @@ -0,0 +1,12 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +public enum CacheState +{ + NONE, + NOTCACHED, + CACHED +} diff --git a/java/core/src/core/mail/client/cache/ID.java b/java/core/src/core/mail/client/cache/ID.java new file mode 100644 index 0000000..6a3ce90 --- /dev/null +++ b/java/core/src/core/mail/client/cache/ID.java @@ -0,0 +1,126 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +import java.math.BigInteger; + +import core.util.Arrays; +import core.util.Base16; +import core.util.SecureRandom; + +public class ID +{ + static public final int PartSize=6; + static public ID None = ID.fromLong(-1); + static SecureRandom random = new SecureRandom(); + + public static final byte VERSION = 1; + final String value; + + private ID(byte[] value) + { + this.value = Base16.encode(value); + } + + private ID(String value) + { + this.value = value; + } + + public static ID combine(ID left, ID right) + { + return new ID(left.value + "00" + right.value); + } + + public int indexOfDoubleNull(String s) + { + for (int i=0; i caches = new HashMap(); + + public IndexedCache (Factory factory) + { + this.factory = factory; + } + + public Item getAndAcquire (Type type, ID iid) + { + log.trace(this, "getAndAcquire", type, iid); + Item t = get(type, iid); + acquire(t); + return t; + } + + public Callback flush_ () + { + log.debug(this, "flush_"); + + CallbackChain chain = new CallbackChain(); + for (Cache cache : caches.values()) + { + if (cache.isDirty() || cache.hasDirtyChildren()) + chain.addCallback(cache.flush_()); + } + + chain.addCallback(onFlush_()); + return chain; + } + + public void acquire(Item t) + { + log.trace(this, "acquire", t.getId()); + getKnownCache(t.getId().left()).acquire(t); + } + + Cache getKnownCache(ID cid) + { + log.trace(this, "getKnownCache",cid); + if (!caches.containsKey(cid)) + { + log.debug("getKnownCache creating"); + Cache cache = factory.getCache(cid, false); + cache.setId(cid); + caches.put(cid, cache); + itemAdded(cache); + } + + return caches.get(cid); + } + + public void newCache(ID cid) + { + log.debug(this, "newCache", cid); + + Cache cache = factory.getCache(cid, true); + caches.put(cid, cache); + itemAdded(cache); + } + + ID getWorkCacheID () + { + return workCacheID; + } + + void setWorkCacheID (ID id) + { + log.debug(this, "setWorkCacheID"); + workCacheID = id; + markDirty(); + } + + public Item get(Type type, ID iid) + { + log.trace(this, "get", type, iid); + return getKnownCache(iid.left()).get(type, iid); + } + + public void link(ID id, Item t) + { + log.debug(this, "put", id, t); + + t.setId(id); + getKnownCache(t.getId().left()).link(t); + } + + public void put(Item t) + { + log.debug(this, "put", t); + + t.setId(instantiateID(ID.random())); + getKnownCache(t.getId().left()).put(t); + } + + private ID instantiateID(ID id) + { + ID workCache = getWorkCacheID(); + if (workCache.equals(ID.None) || getKnownCache(workCache).isFull()) + { + workCache = ID.random(); + setWorkCacheID(workCache); + newCache(workCache); + } + + return ID.combine(workCache, id); + } + + public Map getItemMap () + { + return caches; + } + + public void debug (LogOut log, String prefix) + { + log.debug (prefix,this, "W", workCacheID); + for (Entry entry : caches.entrySet()) + entry.getValue().debug(log, prefix+" |--"); + } +} diff --git a/java/core/src/core/mail/client/cache/IndexedCacheSerializer.java b/java/core/src/core/mail/client/cache/IndexedCacheSerializer.java new file mode 100644 index 0000000..219c3b3 --- /dev/null +++ b/java/core/src/core/mail/client/cache/IndexedCacheSerializer.java @@ -0,0 +1,41 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import core.util.LogNull; +import core.util.LogOut; +import core.util.Streams; + +public class IndexedCacheSerializer extends ItemSerializerSync +{ + protected final static byte VERSION = 1; + + static LogNull log = new LogNull(IndexedCacheSerializer.class); + public byte[] serialize(Item item) throws Exception + { + log.debug("serialize", item); + IndexedCache indexedCache = (IndexedCache)item; + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(VERSION); + Streams.writeBoundedArray(bos, indexedCache.workCacheID.serialize()); + + return bos.toByteArray(); + } + + public void deserialize(Item item, byte[] bytes) throws Exception + { + log.debug("deserialize", item); + IndexedCache indexedCache = (IndexedCache)item; + + ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + int version = bis.read(); + indexedCache.workCacheID.deserialize(Streams.readBoundedArray(bis)); + } + +}; \ No newline at end of file diff --git a/java/core/src/core/mail/client/cache/Info.java b/java/core/src/core/mail/client/cache/Info.java new file mode 100644 index 0000000..dc2b4e9 --- /dev/null +++ b/java/core/src/core/mail/client/cache/Info.java @@ -0,0 +1,287 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +import core.callback.Callback; +import core.callback.CallbackDefault; +import core.util.LogNull; + +public class Info +{ + static LogNull log = new LogNull (Info.class); + + protected ItemOwner owner; + private Version cacheVersion; + private Version localVersion; + private ID id; + + protected Info () + { + cacheVersion = Version.NONE; + localVersion = Version.NONE; + } + + public ID getId () + { + return id; + } + + public void setId (ID id) + { + this.id = id; + } + + public void onExisting () + { + + } + + public void setOwner (ItemOwner owner) + { + this.owner = owner; + } + + public void setLocalVersion (Version localVersion) + { + this.localVersion = localVersion; + } + + public Version getLocalVersion () + { + return localVersion; + } + + public Version getCacheVersion () + { + return cacheVersion; + } + + public void setCacheVersion (Version cacheVersion) + { + this.cacheVersion = cacheVersion; + } + + public final void markNoLoad (Version cacheVersion) + { + this.cacheVersion = cacheVersion; + } + + public final void markLoad (Version cacheVersion) + { + this.cacheVersion = this.localVersion = cacheVersion; + onLoaded(); + onModified(); + } + + public void markDeleted () + { + onDeleting(); + this.localVersion = Version.DELETED; + + onDirty(); + onModified(); + } + + public boolean isDeleted() + { + return localVersion.equals(Version.DELETED); + } + + protected void nextVersion () + { + localVersion = Version.random(); + } + + private final void markDirtyNoCheck () + { + nextVersion(); + onDirty(); + onModified(); + } + + public final void markDirty () + { + log.debug("info.markDirty", this); + if (!isWritable()) + throw new RuntimeException("markDirty can't be called on an unwritable item"); + + markDirtyNoCheck(); + } + + public final boolean isDirty () + { + return !localVersion.equals(cacheVersion); + } + + public boolean hasDirtyChildren () + { + return false; + } + + public boolean isLoaded () + { + return !localVersion.equals(Version.NONE); + } + + public boolean isWritable () + { + return isLoaded(); + } + + final public void markStore (Version version) + { + log.debug(this, "markStore", version); + cacheVersion = version; + + onStored(); + } + + public void markCreate () + { + log.debug(this, "onCreate"); + markDirtyNoCheck(); + + onCreate(); + } + + public void markPreLoad () + { + onPreLoad(); + } + + public void markPreStore () + { + onPreStore(); + } + + protected void onCreate () + { + } + + protected void onPreLoad () + { + } + + protected void onPreStore () + { + } + + protected void onLoaded() + { + + } + + protected void onStored() + { + + } + + protected void onModified () + { + + } + + protected void onDirty () + { + + } + + protected void onDeleting () + { + + } + + protected String classNameLastPart () + { + String s = getClass().getName(); + int lastPeriod = s.lastIndexOf('.'); + return s.substring(lastPeriod == -1 ? 0 : lastPeriod); + } + + public String getStringRepOfLoadState () + { + String str = ""; + if (!isLoaded()) + str += "NotLoaded:"; + if (isDirty()) + str += "DIRTY!:" + getLocalVersion() + ":" + getCacheVersion(); + + return str; + } + + public String toString() + { +// return classNameLastPart() + "(" + getId() + ":" + getLocalVersion() + ":" + getCacheVersion() + ")"; + return classNameLastPart() + "(" + getId() + ":" + getStringRepOfLoadState() + ")"; + } + + public Callback markPreLoad_ () + { + return + new CallbackDefault() { + public void onSuccess(Object... arguments) throws Exception { + onPreLoad(); + next(arguments); + } + }; + } + + public Callback markLoad_ (Version version) + { + return + new CallbackDefault(version) { + public void onSuccess(Object... arguments) throws Exception { + Version version = (Version)V(0); + markLoad(version); + next(arguments); + } + }; + } + + /* + public Callback markPartialLoad_ (Version version) + { + return + new CallbackDefault(version) { + public void onSuccess(Object... arguments) throws Exception { + Version version = (Version)V(0); + setCacheVersion(version); + next(arguments); + } + }; + } + */ + public Callback markStore_ (Version localVersion) + { + return + new CallbackDefault(localVersion) { + public void onSuccess(Object... arguments) throws Exception { + Version version = (Version)V(0); + markStore(version); + next(arguments); + } + }; + } + + public Callback markPreStore_() + { + return new CallbackDefault() { + public void onSuccess(Object... arguments) throws Exception { + onPreStore(); + next(arguments); + } + }; + } + + public Callback markDeleted_() + { + return new CallbackDefault() { + public void onSuccess(Object... arguments) throws Exception { + markDeleted(); + next(arguments); + } + }; + } +} diff --git a/java/core/src/core/mail/client/cache/Item.java b/java/core/src/core/mail/client/cache/Item.java new file mode 100644 index 0000000..54eb250 --- /dev/null +++ b/java/core/src/core/mail/client/cache/Item.java @@ -0,0 +1,89 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +import core.callback.Callback; +import core.callback.CallbackChain; +import core.callback.CallbackDefault; +import core.callback.CallbackEmpty; +import core.util.LogNull; +import core.util.LogOut; + +public abstract class Item extends Info +{ + static LogNull log = new LogNull(Item.class); + + CallbackChain onLoadCallbacks; + CallbackChain onLoadOnceCallbacks; + + public Item () + { + + } + + protected void onDirty() + { + super.onDirty(); + + if (owner != null) + owner.onDirty(this); + } + + protected void onLoaded() + { + log.debug("onLoaded", this); + + super.onLoaded(); + + if (onLoadCallbacks != null) + onLoadCallbacks.invoke(this); + + if (onLoadOnceCallbacks != null) + { + CallbackChain chain = onLoadOnceCallbacks; + onLoadOnceCallbacks = null; + chain.invoke(this); + } + }; + + public CallbackChain getLoadCallbacks() + { + if (onLoadCallbacks == null) + { + onLoadCallbacks = new CallbackChain(); + onLoadCallbacks.setPropagateOriginalArguments().setSlowFail(); + } + return onLoadCallbacks; + } + + public CallbackChain getLoadOnceCallbacks() + { + if (onLoadOnceCallbacks == null) + { + onLoadOnceCallbacks = new CallbackChain(); + onLoadOnceCallbacks.setPropagateOriginalArguments().setSlowFail(); + } + + return onLoadOnceCallbacks; + } + + public void apply (Callback callback) + { + if (isLoaded()) + callback.invoke(this); + else + getLoadOnceCallbacks().addCallback(callback); + } + + public Callback flush_ () + { + return new CallbackEmpty(); + } + + public void debug(LogOut log, String prefix) + { + log.debug(prefix,this); + } +} diff --git a/java/core/src/core/mail/client/cache/ItemCacheFactory.java b/java/core/src/core/mail/client/cache/ItemCacheFactory.java new file mode 100644 index 0000000..d91cde6 --- /dev/null +++ b/java/core/src/core/mail/client/cache/ItemCacheFactory.java @@ -0,0 +1,32 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +public class ItemCacheFactory extends IndexedCache.Factory +{ + String prefix; + StoreLibrary library; + ItemFactory factory; + ItemSerializer serializer; + + public ItemCacheFactory(String prefix, StoreLibrary library, ItemFactory factory, ItemSerializer serializer) + { + this.library = library; + this.prefix = prefix; + this.factory = factory; + this.serializer = serializer; + } + + @Override + public Cache getCache(ID id, boolean isNew) + { + return new Cache( + factory, + serializer, + library.instantiate(prefix, id, isNew) + ); + } + +} diff --git a/java/core/src/core/mail/client/cache/ItemCollection.java b/java/core/src/core/mail/client/cache/ItemCollection.java new file mode 100644 index 0000000..92bdd41 --- /dev/null +++ b/java/core/src/core/mail/client/cache/ItemCollection.java @@ -0,0 +1,72 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; + +import core.callback.Callback; +import core.callback.CallbackDefault; + +public abstract class ItemCollection extends Item implements ItemOwner +{ + @Override + public boolean hasDirtyChildren () + { + return areAnyChildrenDirty(); + } + + public void onDirty (Item item) + { + onDirty(); + } + + public abstract Map getItemMap (); + + public final boolean areAnyChildrenDirty () + { + for (Entry i : getItemMap().entrySet()) + { + Item item = i.getValue(); + if (item.isDirty() || item.hasDirtyChildren()) + return true; + } + + return false; + } + + public void onFlush () + { + } + + public Callback onFlush_ () + { + return new CallbackDefault() { + public void onSuccess(Object... arguments) throws Exception { + onFlush(); + next(arguments); + } + }; + } + + public void itemAdded (Item item) + { + item.setOwner(this); + + if (item.isDirty() || item.hasDirtyChildren()) + onDirty(item); + } + + public void itemRemoved (Item item) + { + item.setOwner(null); + } + + public String toString () + { + return super.toString() + (areAnyChildrenDirty() ? " ChildrenDirty" : ""); + } +} diff --git a/java/core/src/core/mail/client/cache/ItemFactory.java b/java/core/src/core/mail/client/cache/ItemFactory.java new file mode 100644 index 0000000..22466c9 --- /dev/null +++ b/java/core/src/core/mail/client/cache/ItemFactory.java @@ -0,0 +1,10 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +public interface ItemFactory +{ + public Item instantiate (Type type); +} diff --git a/java/core/src/core/mail/client/cache/ItemOwner.java b/java/core/src/core/mail/client/cache/ItemOwner.java new file mode 100644 index 0000000..d52b202 --- /dev/null +++ b/java/core/src/core/mail/client/cache/ItemOwner.java @@ -0,0 +1,10 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +public interface ItemOwner +{ + public void onDirty (Item item); +} diff --git a/java/core/src/core/mail/client/cache/ItemSerializer.java b/java/core/src/core/mail/client/cache/ItemSerializer.java new file mode 100644 index 0000000..a8131f8 --- /dev/null +++ b/java/core/src/core/mail/client/cache/ItemSerializer.java @@ -0,0 +1,13 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +import core.callback.Callback; + +public interface ItemSerializer +{ + Callback serialize_ (Item item); + Callback deserialize_ (Item item); +} diff --git a/java/core/src/core/mail/client/cache/ItemSerializerSync.java b/java/core/src/core/mail/client/cache/ItemSerializerSync.java new file mode 100644 index 0000000..8d9db22 --- /dev/null +++ b/java/core/src/core/mail/client/cache/ItemSerializerSync.java @@ -0,0 +1,44 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +import java.io.IOException; + +import core.callback.Callback; +import core.callback.CallbackDefault; + +public abstract class ItemSerializerSync implements ItemSerializer +{ + public abstract byte[] serialize(Item item) throws Exception; + public abstract void deserialize(Item item, byte[] bytes) throws Exception; + + @Override + public Callback serialize_(Item item) { + return new CallbackDefault(item) { + + @Override + public void onSuccess(Object... arguments) throws Exception { + Item item = V(0); + item.onPreStore(); + next(serialize(item)); + } + }; + } + + @Override + public Callback deserialize_(Item item) { + return new CallbackDefault(item) { + + @Override + public void onSuccess(Object... arguments) throws Exception { + Item item = V(0); + item.onPreLoad(); + deserialize(item, (byte[])arguments[0]); + next((Item)V(0)); + } + }; + } + +} diff --git a/java/core/src/core/mail/client/cache/JSON.java b/java/core/src/core/mail/client/cache/JSON.java new file mode 100644 index 0000000..af8ece3 --- /dev/null +++ b/java/core/src/core/mail/client/cache/JSON.java @@ -0,0 +1,576 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import mail.client.Master; +import mail.client.Servent; +import mail.client.model.Attachment; +import mail.client.model.Attachments; +import mail.client.model.Body; +import mail.client.model.Conversation; +import mail.client.model.Dictionary; +import mail.client.model.Folder; +import mail.client.model.FolderDefinition; +import mail.client.model.FolderMaster; +import mail.client.model.FolderSet; +import mail.client.model.Header; +import mail.client.model.Identity; +import mail.client.model.Mail; +import mail.client.model.Recipients; +import mail.client.model.Settings; +import mail.client.model.TransportState; +import mail.client.model.UnregisteredIdentity; + +import core.util.Base64; + +import core.util.JSON_; +import core.util.JSON_.JSONException; +import core.util.LogNull; +import core.util.LogOut; +import core.util.Pair; +import core.util.Strings; + +public class JSON extends Servent +{ + LogNull log = new LogNull(JSON.class); + + public Object toJSON (ID id) + { + return toJSON(id.serialize()); + } + + public ID toID (String json) + { + return ID.deserialize(toBytes(json)); + } + + public Identity toIdentity(String s) + { + return + master.getAddressBook().getIdentity( + new UnregisteredIdentity(s) + ); + } + + public Object toJSON (String s) + { + return JSON_.newString(s); + } + + public Object toJSON (boolean b) + { + return JSON_.newBoolean(b); + } + + public Object toJSON (int v) + { + return JSON_.newNumber(v); + } + + public Object toJSON (Identity i) + { + return toJSON(i.toString()); + } + + public List toIdentityList (Object a) throws JSONException + { + List l = new ArrayList(); + + for (int i=0; i l) throws JSONException + { + Object a = JSON_.newArray(); + for (Identity i: l) + JSON_.add(a, toJSON(i)); + + return a; + } + + public Recipients toRecipients(Object o) throws JSONException + { + Recipients r = new Recipients(); + r.setTo(toIdentityList(JSON_.getArray(o, "to"))); + r.setCc(toIdentityList(JSON_.getArray(o, "cc"))); + r.setBcc(toIdentityList(JSON_.getArray(o, "bcc"))); + r.setReplyTo(toIdentityList(JSON_.getArray(o, "replyTo"))); + + return r; + } + + + public Object toJSON (Recipients r) throws JSONException + { + Object o = JSON_.newObject(); + JSON_.put(o, "to", toJSON(r.getTo())); + JSON_.put(o, "cc", toJSON(r.getCc())); + JSON_.put(o, "bcc", toJSON(r.getBcc())); + JSON_.put(o, "replyTo", toJSON(r.getReplyTo())); + + return o; + } + + public Dictionary toDictionary(String s) + { + Dictionary d = new Dictionary(); + d.fromSerializableString(s); + return d; + } + + public Object toJSON (Dictionary d) + { + return toJSON(d.toSerializableString()); + } + + public Date toDate (long d) + { + return new Date(d); + } + + public Object toJSON (Date d) + { + return JSON_.newNumber(d.getTime()); + } + + public Header toHeader (Object h) throws JSONException + { + Header header = new Header(); + + if (JSON_.has(h, "externalKey")) + header.setExternalKey(JSON_.getString(h, "externalKey")); + + if (JSON_.has(h, "originalKey")) + header.setOriginalKey(JSON_.getString(h, "originalKey")); + + if (JSON_.has(h, "uidl")) + header.setUIDL(JSON_.getString(h, "uidl")); + + if (JSON_.has(h, "author")) + header.setAuthor(toIdentity (JSON_.getString(h, "author"))); + + if (JSON_.has(h, "authors")) + header.setAuthors(toIdentityList(JSON_.getArray(h, "authors"))); + + if (JSON_.has(h, "recipients")) + header.setRecipients(toRecipients(JSON_.getObject(h, "recipients"))); + + if (JSON_.has(h, "subject")) + header.setSubject(JSON_.getString(h, "subject")); + + if (JSON_.has(h, "date")) + header.setDate(toDate(JSON_.getLong(h, "date"))); + + if (JSON_.has(h, "transportState")) + header.setTransportState(TransportState.fromString(JSON_.getString(h, "transportState"))); + + if (JSON_.has(h, "brief")) + header.setBrief(JSON_.getString(h, "brief")); + + if (JSON_.has(h, "dictionary")) + header.setDictionary(toDictionary(JSON_.getString(h, "dictionary"))); + + return header; + } + + public Object toJSON (Header h) throws JSONException + { + Object o = JSON_.newObject(); + + if (h.getExternalKey()!=null) + JSON_.put(o, "externalKey", toJSON(h.getExternalKey())); + + if (h.getOriginalKey()!=null) + JSON_.put(o, "originalKey", toJSON(h.getOriginalKey())); + + if (h.getUIDL()!=null) + JSON_.put(o, "uidl", toJSON(h.getUIDL())); + + if (h.getAuthor() != null) + JSON_.put(o, "author", toJSON(h.getAuthor())); + + if (h.getAuthors() != null) + JSON_.put(o, "authors", toJSON(h.getAuthors())); + + if (h.getRecipients() != null) + JSON_.put(o, "recipients", toJSON(h.getRecipients())); + + if (h.getSubject() != null) + JSON_.put(o, "subject", toJSON(h.getSubject())); + + if (h.getDate() != null) + JSON_.put(o, "date", toJSON(h.getDate())); + + if (h.getTransportState() != null) + JSON_.put(o, "transportState", toJSON(h.getTransportState().toString())); + + if (h.getBrief() != null) + JSON_.put(o, "brief", toJSON(h.getBrief())); + + if (h.getDictionary() != null) + JSON_.put(o, "dictionary", toJSON(h.getDictionary())); + + return o; + } + + public Body toBody (Object o) throws JSONException + { + Body body = new Body(); + + if (JSON_.has(o, "text")) + body.setText(JSON_.getString(o, "text")); + + if (JSON_.has(o, "html")) + body.setHTML(JSON_.getString(o, "html")); + + return body; + } + + public Object toJSON (Body b) throws JSONException + { + Object o = JSON_.newObject(); + if (b.hasText()) + JSON_.put(o, "text", toJSON(b.getText())); + + if (b.hasHTML()) + JSON_.put(o, "html", toJSON(b.getHTML())); + + return o; + } + + public FolderDefinition toFolderDefinition(Object o) throws JSONException + { + FolderDefinition f = new FolderDefinition(JSON_.getString(o, "name")); + + if (JSON_.has(o, "subject")) + f.setSubject(JSON_.getString(o, "subject")); + + if (JSON_.has(o, "author")) + f.setAuthor(toIdentity(JSON_.getString(o, "author"))); + + if (JSON_.has(o, "recipient")) + f.setRecipient(toIdentity(JSON_.getString(o, "recipient"))); + + if (JSON_.has(o, "stateDiffers") || JSON_.has(o, "stateEquals")) + { + TransportState d=null,e=null; + if (JSON_.has(o, "stateDiffers")) + d = TransportState.fromString(JSON_.getString(o, "stateDiffers")); + + if (JSON_.has(o, "stateEquals")) + e = TransportState.fromString(JSON_.getString(o, "stateEquals")); + + f.setState(e, d); + } + + if (JSON_.has(o, "bayesianDictionary")) + f.setBayesianDictionary(toDictionary(JSON_.getString(o, "bayesianDictionary"))); + + if (JSON_.has(o, "autoBayesian")) + f.setAutoBayesian(JSON_.getBoolean(o, "autoBayesian")); + + return f; + } + + public Object toJSON(FolderDefinition d) throws JSONException + { + Object o = JSON_.newObject(); + JSON_.put(o, "name", toJSON(d.getName())); + + if (d.getAuthor()!=null) + JSON_.put(o, "author", toJSON(d.getAuthor())); + + if (d.getSubject()!=null) + JSON_.put(o, "subject", toJSON(d.getSubject())); + + if (d.getRecipient()!=null) + JSON_.put(o, "recipient", toJSON(d.getRecipient())); + + if (d.getStateDiffers()!=null) + JSON_.put(o, "stateDiffers", toJSON(d.getStateDiffers().toString())); + + if (d.getStateEquals()!=null) + JSON_.put(o, "stateEquals", toJSON(d.getStateEquals().toString())); + + if (d.getBayesianDictionary()!=null) + JSON_.put(o, "bayesianDictionary", toJSON(d.getBayesianDictionary())); + + if (d.getAutoBayesian()) + JSON_.put(o, "autoBayesian", toJSON(d.getAutoBayesian())); + + return o; + } + + public Object toJSON(byte[] id) + { + return toJSON(Base64.encode(id)); + } + + public byte[] toBytes(String string) + { + return Base64.decode(string); + } + + public Attachments toAttachments(Object json) throws JSONException + { + Attachments attachments = new Attachments (); + for (int i=0; i p : c.getItemIds()) + { + Object iNd = JSON_.newArray(); + JSON_.add(iNd, toJSON(p.first)); + JSON_.add(iNd, toJSON(p.second)); + JSON_.add(m, iNd); + } + + JSON_.put(v, "mail", m); + + return v; + } + + public void fromJSON(Folder f, Object v) throws Exception + { + String version = JSON_.getString(v, "version"); + + if (JSON_.has(v, "definition")) + f.setFolderDefinition(toFolderDefinition(JSON_.getObject(v, "definition"))); + + if (f instanceof FolderSet) + { + if (f instanceof FolderMaster) + { + FolderMaster fm = (FolderMaster)f; + + { + Object a = JSON_.getArray(v, "uidl"); + for (int i=0; i=0; --i) // reverse it + { + fs.addFolderId(toID(JSON_.getString(a, i))); + } + + fs.setNumConversations(JSON_.getInt(v, "numConversations")); + + return; + } + + Object a = JSON_.getArray(v, "conversations"); + for (int i=JSON_.size(a)-1; i>=0; --i) // reverse it + { + Object iNd = JSON_.getArray(a, i); + f.addConversationId(toID(JSON_.getString(iNd,0)), toDate(JSON_.getLong(iNd,1))); + } + } + + public Object toJSON(Folder f) throws Exception + { + Object v = JSON_.newObject(); + JSON_.put(v, "version", toJSON("1.0")); + + if (f.getFolderDefinition() != null) + JSON_.put(v, "definition", toJSON(f.getFolderDefinition())); + + if (f instanceof FolderSet) + { + if (f instanceof FolderMaster) + { + FolderMaster fm = (FolderMaster)f; + + { + Object a = JSON_.newArray(); + for (Map.Entry id : fm.getUIDLHashes().entrySet()) + { + Object iNd = JSON_.newArray(); + JSON_.add(iNd, toJSON(id.getKey())); + JSON_.add(iNd, toJSON(id.getValue())); + JSON_.add(a, iNd); + } + JSON_.put(v, "uidl", a); + } + + { + Object a = JSON_.newArray(); + for (Map.Entry id : fm.getExternalKeyHashes().entrySet()) + { + Object iNd = JSON_.newArray(); + JSON_.add(iNd, toJSON(id.getKey())); + JSON_.add(iNd, toJSON(id.getValue())); + JSON_.add(a, iNd); + } + JSON_.put(v, "externalKey", a); + } + } + + FolderSet fs = (FolderSet)f; + Object a = JSON_.newArray(); + for (ID id : fs.getFolderIds()) + JSON_.add(a, toJSON(id)); + + JSON_.put(v, "parts", a); + JSON_.put(v, "numConversations", toJSON(fs.getNumConversations())); + + return v; + } + + Object a = JSON_.newArray(); + for (Pair p : f.getConversationIds()) + { + Object iNd = JSON_.newArray(); + JSON_.add(iNd, toJSON(p.first)); + JSON_.add(iNd, toJSON(p.second)); + JSON_.add(a, iNd); + } + JSON_.put(v, "conversations", a); + + return v; + } + + public void fromJSON(Settings item, Object o) throws JSONException + { + String[] keys = JSON_.keys(o); + + Map kv = new HashMap(); + + for (String key : keys) + kv.put(key, JSON_.getString(o, key)); + + item.setKV(kv); + } + + public Object toJSON(Settings item) throws JSONException + { + Object o = JSON_.newObject(); + + for (Entry key : item.getKV().entrySet()) + { + JSON_.put(o, key.getKey(), toJSON(key.getValue())); + } + + return o; + } +} diff --git a/java/core/src/core/mail/client/cache/LoadState.java b/java/core/src/core/mail/client/cache/LoadState.java new file mode 100644 index 0000000..0da2c79 --- /dev/null +++ b/java/core/src/core/mail/client/cache/LoadState.java @@ -0,0 +1,13 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +public enum LoadState +{ + NONE, + LOADING, + LOADED, + FAILED +} diff --git a/java/core/src/core/mail/client/cache/Operation.java b/java/core/src/core/mail/client/cache/Operation.java new file mode 100644 index 0000000..1cd0ca6 --- /dev/null +++ b/java/core/src/core/mail/client/cache/Operation.java @@ -0,0 +1,25 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +public class Operation { + enum Type + { + GET, + PUT, + REMOVE + } + + public Type type; + public ID id; + public Item t; + + public Operation(Type type, ID id, Item t) + { + this.type = type; + this.id = id; + this.t = t; + } +} diff --git a/java/core/src/core/mail/client/cache/Store.java b/java/core/src/core/mail/client/cache/Store.java new file mode 100644 index 0000000..3979964 --- /dev/null +++ b/java/core/src/core/mail/client/cache/Store.java @@ -0,0 +1,239 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +import java.util.Map; +import java.util.HashMap; +import java.util.Map.Entry; + +import core.callback.Callback; +import core.callback.CallbackDefault; +import core.util.LogNull; +import core.util.LogOut; +import core.util.Pair; +import core.util.Triple; + +/** + * Store sequence: + * Store exists + * #0 Store.markDirty (new local version A) + * Item is added -> takes on storeVersionWhenModified=Store.getLocalVersion() A + * #1 Store.flush_ + * #1 Store.serialize_ + * #1 Store.markDirty (new local version B) + * Item is added -> takes on NEW storeVersionWhenModified B + * #1 Store.markStore (local version A) + * + * + * @author tprepscius + * + */ +public class Store extends ItemCollection +{ + public static class Data extends Item + { + byte[] bytes; + + public Data (ID id) + { + setId(id); + } + + public void markLoad (byte[] bytes, Version version) + { + this.bytes = bytes; + markLoad(version); + } + + public void markDirty (byte[] bytes, Version version) + { + this.bytes = bytes; + setLocalVersion(version); + } + + public void markStore () + { + markStore(getLocalVersion()); + } + + public String toString() + { + return super.toString(); + } + }; + + LogNull log = new LogNull(Store.class); + protected Map map = new HashMap(); + int requestedMaxSize; + boolean locked = false; + + public Store (int requestedMaxSize) + { + this.requestedMaxSize = requestedMaxSize; + } + + byte[] get(ID id) + { + return map.get(id).bytes; + } + + public CallbackDefault get_(ID id) + { + return new CallbackDefault(id) { + public void onSuccess(Object... arguments) throws Exception { + next(get((ID)V(0))); + } + }; + } + + public CallbackDefault put_(ID id, Version version) + { + return new CallbackDefault(id, version) { + public void onSuccess(Object... arguments) throws Exception { + put((ID)V(0), (Version)V(1), (byte[])arguments[0]); + next(); + } + }; + } + + public CallbackDefault remove_(ID id) + { + return new CallbackDefault(id) { + public void onSuccess(Object... arguments) throws Exception { + remove((ID)V(0)); + next(); + } + }; + } + + Version version(ID id) + { + return map.get(id).getLocalVersion(); + } + + boolean has(ID id) + { + if (map.containsKey(id)) + { + log.debug(this, "has", id); + } + else + { + log.debug(this, "does not have", id); + log.debug(map.keySet().toArray()); + } + + return map.containsKey(id); + } + + void remove(ID id) + { + log.debug(this, "remove", id); + put(id, Version.DELETED, new byte[0]); + } + + void update(ID id, Version version, byte[] bytes) + { + log.debug(this, "update", id, version); + + Data item = map.get(id); + + if (item == null) + { + item = new Data(id); + map.put(id, item); + } + + if (!item.isDirty()) + { + log.debug("store updating data", id); + item.markLoad(bytes,version); + } + else + { + log.debug("store not updating data because of conflict", id); + } + } + + void put(ID id, Version version, byte[] bytes) + { + assert(isWritable()); + + log.debug(this, "put", id, version); + + Data item = map.get(id); + + if (item == null) + { + item = new Data(id); + map.put(id, item); + } + + item.markDirty(bytes, version); + } + + int getSize () + { + int size = 0; + for (Data item : map.values()) + size += item.bytes.length; + + return size; + } + + public boolean isWritable () + { + return super.isWritable() && !locked; + } + + boolean isFull () + { + if (requestedMaxSize < 0) + return false; + + return getSize() > requestedMaxSize; + } + + public String toString () + { + return super.toString() + " " + (getSize()/1000) + "k " + (isFull() ? "Full" :""); + } + + public void lock () + { + locked = true; + } + + @Override + public void onStored () + { + super.onStored(); + + for (Data item : map.values()) + { + if (item.isDirty()) + { + item.markStore(); + } + } + + locked = false; + } + + @Override + public Map getItemMap() + { + return map; + } + + public void debug (LogOut log, String prefix) + { + final String PREFIX = " |--"; + log.debug (prefix+" store:",this); + for (Entry entry : map.entrySet()) + entry.getValue().debug(log, prefix+PREFIX); + } + +} diff --git a/java/core/src/core/mail/client/cache/StoreFactory.java b/java/core/src/core/mail/client/cache/StoreFactory.java new file mode 100644 index 0000000..2e359e2 --- /dev/null +++ b/java/core/src/core/mail/client/cache/StoreFactory.java @@ -0,0 +1,19 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +public class StoreFactory implements ItemFactory +{ + int requestedMaxSize; + + public StoreFactory (int requestedMaxSize) + { + this.requestedMaxSize = requestedMaxSize; + } + + public Item instantiate(Type type) { + return new Store(requestedMaxSize); + } +} diff --git a/java/core/src/core/mail/client/cache/StoreLibrary.java b/java/core/src/core/mail/client/cache/StoreLibrary.java new file mode 100644 index 0000000..5a7ac95 --- /dev/null +++ b/java/core/src/core/mail/client/cache/StoreLibrary.java @@ -0,0 +1,354 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +import core.callback.Callback; +import core.callback.CallbackChain; +import core.callback.CallbackDefault; +import core.callback.CallbackEmpty; +import core.callbacks.SaveArguments; +import core.connector.FileInfo; +import core.connector.async.AsyncStoreConnector; +import core.connector.async.Lock; +import core.constants.ConstantsStorage; +import core.crypt.Cryptor; +import core.crypt.CryptorAES; +import core.crypt.CryptorSeed; +import core.crypt.HashSha256; +import core.util.Arrays; +import core.util.Comparators; +import core.util.LogNull; +import core.util.LogOut; +import core.util.Maps; +import core.util.Pair; +import core.util.Strings; + +public class StoreLibrary +{ + static LogNull log = new LogNull(StoreLibrary.class); + + protected Map versions = new HashMap(); + protected Map stores = new HashMap(); + protected Map cryptors = new HashMap(); + + ItemSerializer storeSerializer; + ItemFactory storeFactory; + + HashSha256 derivedKeyGenerator = new HashSha256(); + CryptorSeed cryptorSeed; + + Lock flushLock; + AsyncStoreConnector storeConnector; + + public StoreLibrary (CryptorSeed cryptorSeed, StoreFactory storeFactory, AsyncStoreConnector storeConnector) + { + this.cryptorSeed = cryptorSeed; + this.storeSerializer = new StoreSerializer(); + this.storeFactory = storeFactory; + this.storeConnector = storeConnector; + flushLock = + new Lock( + storeConnector, + ConstantsStorage.CACHE_PREFIX + "flush.lock", + ConstantsStorage.FLUSH_LOCK_TIME_SECONDS, + ConstantsStorage.FLUSH_LOCK_TIME_ALLOWED_BEFORE_RELOCK_SECONDS + ); + } + + public String getFullPathFor (String relative) + { + return ConstantsStorage.CACHE_PREFIX + relative; + } + + public Store instantiate (String prefix, ID id, boolean isNew) + { + log.debug("instantiating", prefix, id, isNew); + + Store store = (Store)storeFactory.instantiate(null); + String relative = id != null ? (prefix + "_" + id.toFileSystemSafe()) : prefix; + stores.put(relative, store); + + if (isNew) + { + store.markCreate(); + } + else + { + loadSingleStore_(relative).invoke(); + } + + return store; + } + + public byte[] createCryptorKeyFor (String key) + { + byte[] data = Arrays.concat(Strings.toBytes(key), cryptorSeed.seed); + return derivedKeyGenerator.hash(data); + } + + public Cryptor getOrCreateCryptorForKey (String key) + { + Cryptor cryptor = cryptors.get(key); + if (cryptor == null) + { + cryptor = new CryptorAES(createCryptorKeyFor(key)); + cryptors.put(key, cryptor); + } + + return cryptor; + } + + public Callback load_ (String key) + { + log.debug("load_", key); + + Store store = stores.get(key); + SaveArguments saveArgs = new SaveArguments(); + Cryptor cryptor = getOrCreateCryptorForKey(key); + + return + log.debug_("loading", key) + .addCallback(saveArgs) + .addCallback(cryptor.decrypt_()) + .addCallback(storeSerializer.deserialize_(store)) + .addCallback(store.markLoad_(Version.random())) + .addCallback(saveArgs.restore_(101)) + .addCallback(Maps.put_(versions, key)); + } + + public Callback store_ (String key, Version version) + { + log.debug("store_", key); + + Store store = stores.get(key); + Cryptor cryptor = getOrCreateCryptorForKey(key); + + return + log.debug_("storing", key) + .addCallback(flushLock.relock_()) + .addCallback(storeSerializer.serialize_(store)) + .addCallback(cryptor.encrypt_()) + .addCallback(storeConnector.put_(getFullPathFor(key))) + .addCallback(Maps.put_(versions, key)) + .addCallback(store.markStore_(version)); + } + + static class SortByCacheType implements Comparator + { + static String priority = "MCFI"; + + public int getPriority(String s) + { + String file = s.substring(s.lastIndexOf("/")+1); + return priority.indexOf(file.charAt(0)); + } + + public int compare(String lhs, String rhs) { + return getPriority((String)rhs) - getPriority((String)lhs); + } + } + + public Callback checkIncludesMainIndex_() + { + return new CallbackDefault() { + public void onSuccess(Object...arguments) throws Exception + { + log.debug("found files"); + List fileInfos = (List)arguments[0]; + + boolean hasMainIndex = false; + for (FileInfo info : fileInfos) + { + log.trace(info.path); + + if (info.path.endsWith("/I")) + hasMainIndex = true; + } + + if (!hasMainIndex) + throw new Exception("Main index not found!"); + + next(fileInfos); + } + } ; + } + + public Callback getFilesToLoad_() + { + return new CallbackDefault() { + public void onSuccess(Object...arguments) + { + log.debug("getFilesToLoad_"); + + List fileInfos = (List)arguments[0]; + List> filesToLoad = new ArrayList>(); + + for (FileInfo info : fileInfos) + { + boolean storeForFileIsInstantiated = + stores.containsKey(info.relativePath); + + if (storeForFileIsInstantiated) + { + log.trace("storeForFileIsInstantiated", info.relativePath); + String remoteVersion = info.version; + String localVersion = versions.get(info.relativePath); + + if (!remoteVersion.equals(localVersion)) + { + log.debug("will load"); + filesToLoad.add(new Pair(info.relativePath, remoteVersion)); + } + } + } + + Collections.sort(filesToLoad, new Comparators.SortByFirst(new SortByCacheType())); + next(filesToLoad); + } + }; + } + + public Callback handleFileInfos_ () { + return + new CallbackDefault() { + @Override + public void onSuccess(Object... arguments) throws Exception { + log.debug("handleFileInfos_"); + + @SuppressWarnings("unchecked") + List> filesToLoad = (List>)arguments[0]; + + CallbackChain chain = new CallbackChain(); + for (Pair i : filesToLoad) + { + log.trace("building load sequence", i.first); + + Callback callback = + storeConnector.get_(getFullPathFor(i.first)) + .addCallback(load_(i.first)); + + chain.addCallback(callback); + } + + log.debug("starting load sequence"); + chain.setReturn(callback); + chain.invoke(); + } + + }; + } + + public Callback loadSingleStore_(String fileName) + { + return + storeConnector.get_(getFullPathFor(fileName)) + .addCallback(load_(fileName)); + + } + + public Callback update_ (boolean shouldTestLock) + { + return + storeConnector.list_(ConstantsStorage.CACHE_PREFIX) + .addCallback(shouldTestLock ? flushLock.testLock_() : new CallbackEmpty()) + .addCallback(shouldTestLock ? new CallbackEmpty() : checkIncludesMainIndex_()) + .addCallback(getFilesToLoad_()) + .addCallback(handleFileInfos_()); + } + + public Callback flushDirty_() + { + log.debug("flushDirty_"); + List> filesToStore = new ArrayList>(); + + for (Map.Entry p : stores.entrySet()) + { + log.trace("flushDirty_ iterating over", p.getKey(), p.getValue()); + Store store = p.getValue(); + if (store.hasDirtyChildren()) + { + log.debug("will store",p.getKey(),p.getValue()); + store.lock(); + filesToStore.add(new Pair(p.getKey(),p.getValue().getLocalVersion())); + } + } + + Collections.sort( + filesToStore, + new Comparators.SortByFirstReverse(new SortByCacheType()) + ); + + CallbackChain chain = new CallbackChain(); + for (Pair file : filesToStore) + { + log.debug("will store (ordered):", file.first); + chain.addCallback(store_(file.first, file.second)); + } + + return chain; + } + + public Callback flushDirty__ () + { + return + new CallbackDefault() { + public void onSuccess(Object... arguments) throws Exception { + flushDirty_().setReturn(callback).invoke(arguments); + } + }; + } + + public boolean hasDirtyChildren () + { + for (Map.Entry p : stores.entrySet()) + { + Store store = p.getValue(); + if (store.hasDirtyChildren()) + { + log.debug("found dirty store"); + return true; + } + } + + return false; + } + + public Callback flush_ () + { + return new CallbackDefault() { + public void onSuccess(Object... arguments) throws Exception { + if (hasDirtyChildren()) + { + call ( + flushLock.lock_() + .addCallback (update_(true)) + .addCallback (flushDirty__()) + .addCallback(flushLock.unlock_()) + ); + } + else + { + next(); + } + } + }; + } + + public void start(Callback callback) + { + log.debug("start!"); + + update_(false).addCallback(callback).invoke(); + } + +} diff --git a/java/core/src/core/mail/client/cache/StoreSerializer.java b/java/core/src/core/mail/client/cache/StoreSerializer.java new file mode 100644 index 0000000..4f80271 --- /dev/null +++ b/java/core/src/core/mail/client/cache/StoreSerializer.java @@ -0,0 +1,72 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.cache; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.Map.Entry; + +import mail.client.cache.Store.Data; + +import core.util.LogNull; +import core.util.LogOut; +import core.util.Pair; +import core.util.Streams; + +public class StoreSerializer extends ItemSerializerSync +{ + protected final static byte VERSION = 1; + + static LogNull log = new LogNull(StoreSerializer.class); + + @Override + public byte[] serialize(Item item) throws IOException + { + log.debug(this, "serialize", item); + + Store store = (Store)item; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(VERSION); + + Streams.writeInt(bos, store.map.size()); + for (Entry value : store.map.entrySet()) + { + Streams.writeBoundedArray(bos, value.getKey().serialize()); + Streams.writeBoundedArray(bos, value.getValue().bytes); + Streams.writeBoundedArray(bos, value.getValue().getLocalVersion().toBytes()); + } + + return bos.toByteArray(); + } + + @Override + public void deserialize(Item item, byte[] bytes) throws IOException + { + log.debug(this, "deserialize", item); + + Store store = (Store)item; + ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + int version = bis.read(); + + // NEVER clear, because, we may overwrite + // store.map.clear(); + // deletes happen through writing a zero byte array, not by removing + + int size = Streams.readInt(bis); + for (int i=0; i people; + Map indexedByEmail; + + public AddressBook () + { + people = new ArrayList(); + indexedByEmail = new HashMap(); + } + + public boolean hasIdentity (UnregisteredIdentity identity) + { + return indexedByEmail.containsKey(identity.getEmail()); + } + + public Identity getIdentity (UnregisteredIdentity identity) + { + Identity stored = indexedByEmail.get(identity.getEmail()); + + if (stored != null) + { + log.debug("getIdentity",identity,"exists"); + clearIndices (stored); + stored.copyFrom (identity); + } + else + { + log.debug("getIdentity",identity,"new"); + stored = new Identity(); + stored.copyFrom(identity); + people.add(stored); + } + + index(stored); + + return stored; + } + + public List parseAddressString (String s) + { + List list = new ArrayList(); + String[] split = s.split(","); + for (String address : split) + { + address = address.trim(); + if (!address.isEmpty()) + list.add(getIdentity (new UnregisteredIdentity(address))); + } + + return list; + } + + public List parseUnfinishedAddressString (String s) + { + List list = new ArrayList(); + String[] split = s.split(","); + for (String address : split) + { + address = address.trim(); + if (!address.isEmpty()) + { + UnregisteredIdentity uri = new UnregisteredIdentity (address); + if (hasIdentity(uri)) + list.add(getIdentity(uri)); + else + list.add(uri); + } + } + + return list; + } + + public void removeIdentity (Identity identity) + { + Identity stored = indexedByEmail.get(identity.getEmail()); + + if (stored != null) + { + clearIndices (stored); + people.remove(stored); + } + } + + protected void clearIndices (Identity identity) + { + indexedByEmail.values().remove(identity); + } + + protected void index (Identity identity) + { + if (identity.email != null) + { + indexedByEmail.put(identity.email, identity); + } + } + + public List getAddressList () + { + return people; + } +} diff --git a/java/core/src/core/mail/client/model/Attachment.java b/java/core/src/core/mail/client/model/Attachment.java new file mode 100644 index 0000000..2d65a85 --- /dev/null +++ b/java/core/src/core/mail/client/model/Attachment.java @@ -0,0 +1,119 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import java.util.Date; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; + +import core.util.Base64; + +import core.crypt.HashSha256; +import core.util.LogNull; +import core.util.Strings; + +@Export() +public class Attachment implements Exportable +{ + static LogNull log = new LogNull(Attachment.class); + + String id; + String disposition; + String mimeType; + byte[] data = null; + + boolean loaded = false; + + public Attachment (String id, String disposition, String mimeType) + { + log.debug("Attachment", id,disposition,mimeType); + + this.disposition = disposition; + this.id = id; + this.mimeType = mimeType; + } + + static public String getAttachmentId (String disposition, String id) throws Exception + { + boolean hasDisposition = disposition != null; + boolean hasId = id!=null; + boolean hasFileName = false; + if (hasDisposition) + { + String oneLineDisposition = Strings.concat(Strings.splitLines(disposition)," "); + hasFileName = oneLineDisposition.toLowerCase().matches(".*filename=.*"); + log.debug(oneLineDisposition, "hasFileName", hasFileName); + } + + if (hasDisposition || hasId) + { + if (hasId) + return id; + + if (hasFileName) + return calculateId(disposition); + } + + return null; + } + + static protected String calculateId (String disposition) throws Exception + { + HashSha256 hash = new HashSha256(); + return Base64.encode(hash.hash(Strings.toBytes(disposition))); + } + + public String getDataBase64 () + { + try + { + log.debug("getDataBase64 a ", data.length, " ", new Date()); + + return Base64.encode(data); + } + finally + { + log.debug("getDataBase64 b", new Date()); + } + } + + public String getId () + { + return id; + } + + public byte[] getData () + { + return data; + } + + public void setData (byte[] data) + { + this.data = data; + loaded = true; + } + + public void clearData () + { + this.data = null; + loaded = false; + } + + public String getDisposition () + { + return disposition; + } + + public String getMimeType () + { + return mimeType; + } + + public boolean isLoaded () + { + return loaded; + } +} diff --git a/java/core/src/core/mail/client/model/Attachments.java b/java/core/src/core/mail/client/model/Attachments.java new file mode 100644 index 0000000..197ac07 --- /dev/null +++ b/java/core/src/core/mail/client/model/Attachments.java @@ -0,0 +1,126 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import java.util.ArrayList; +import java.util.List; + +import mail.client.ArrivalsProcessor; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; + +import core.util.Base64; +import core.constants.ConstantsMailJson; +import core.util.JSON_; +import core.util.JSON_.JSONException; +import core.util.LogNull; +import core.util.LogOut; +import core.util.Strings; + +@Export +public class Attachments implements Exportable +{ + static LogNull log = new LogNull(Attachments.class); + + List attachments; + boolean loaded = false; + + public Attachments () + { + attachments = new ArrayList(); + } + + public void addAttachment (Attachment attachment) + { + attachments.add(attachment); + } + + public void removeAttachmentId (Attachment attachment) + { + attachments.remove(attachment); + } + + public List getList () + { + return attachments; + } + + public Attachment getAttachment (String id) + { + log.debug("getAttachment", id); + + if (id == null) + return null; + + for (Attachment a : attachments) + { + log.debug("comparing",a.getId(), id); + if (a.getId().equals(id)) + return a; + } + + return null; + } + + public void setLoaded (boolean loaded) + { + this.loaded = loaded; + } + + public boolean isLoaded () + { + return loaded; + } + + public void loadFrom(byte[] bs) throws Exception + { + String text = Strings.toString(bs); + log.debug("loading json", text); + Object json = JSON_.parse(text); + Object content = JSON_.getObject(json, ConstantsMailJson.Content); + + List contents = new ArrayList(); + contents.add(content); + while (contents.size() > 0) + { + Object c = contents.get(0); + contents.remove(0); + + String clazz = JSON_.getString(c, ConstantsMailJson.Class); + Object value = JSON_.has(c, ConstantsMailJson.Value) ? + JSON_.get(c,ConstantsMailJson.Value) : null; + + if (clazz.equals(ConstantsMailJson.MultiPart)) + { + for (int i=0; i", Pattern.CASE_INSENSITIVE | Pattern.DOTALL), + removeHeadBlock = Pattern.compile(".*<\\s*+\\/head.*?>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL), + removeScripting = Pattern.compile("<\\s*+script[\\s>]++.*?<\\s*+\\/script.*?>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL), + removeBeforeBody1 = Pattern.compile(".*\\", Pattern.CASE_INSENSITIVE | Pattern.DOTALL), + removeBeforeBody2 = Pattern.compile(".*\\]++.*", Pattern.CASE_INSENSITIVE | Pattern.DOTALL), + removeEndHtml = Pattern.compile("<\\s*+\\/html[\\s>]++.*", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + }; + */ + public String getStrippedHTML () + { + return stripHTML(html); + } + + public String getStrippedText () + { + return stripHTML(text); + } + + /* too slow + public String stripHTML (String html) + { + if (html == null) + return null; + + log.debug("stripHTML"); + + // remove the tag + html = Patterns.removeStartHtml.matcher(html).replaceFirst(""); + log.debug("after removeStartHtml"); + + // try to remove the head block + html = Patterns.removeHeadBlock.matcher(html).replaceFirst(""); + log.debug("after removeHeadBlock"); + + // remove scripting if possible + html = Patterns.removeScripting.matcher(html).replaceAll(""); + log.debug("after removeScripting"); + + // remove before the body + html = Patterns.removeBeforeBody1.matcher(html).replaceFirst("
"); + log.debug("after removeBeforeBody1"); + + html = Patterns.removeBeforeBody2.matcher(html).replaceFirst("
"); + log.debug("after removeAfterBody"); + + // remove the html tag ender and everyhting aftewards + html = Patterns.removeEndHtml.matcher(html).replaceAll(""); + log.debug("after removeEndHtml"); + + return html; + } + */ + + /* maybe for another day + public String stripHTML (String html) throws Exception + { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setValidating(false); + dbf.setNamespaceAware(true); + dbf.setIgnoringComments(false); + dbf.setIgnoringElementContentWhitespace(false); + dbf.setExpandEntityReferences(false); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document doc = db.parse(new StringInputStream(html)); + } + */ + + protected static Pair findTag(boolean last, String html, String tag, Pair search) + { + // adkjfkasdflk asdlkasd < html > ojwefijoweiofjoiwjfwe + // find: "html" + + Pair range = Pair.create(search.first, search.second); + int pos = -1; + + while (true) + { + + if (last) + { + if (pos != -1) + range.second = pos-1; + + pos = html.lastIndexOf(tag, range.second); + if (pos == -1 || pos < range.first) + return null; + } + else + { + if (pos != -1) + range.first = pos + tag.length(); + + if (html.length() <= range.first + tag.length()) + return null; + + pos = html.indexOf(tag, range.first); + if (pos == -1 || pos > range.second) + return null; + } + + // search backwards for < + boolean restart = false; + int i; + for (i=pos-1; i>=0; i--) + { + char c = html.charAt(i); + if (c=='<') + break; + else + // wan't our tag signal for restart search + if (!Characters.isWhitespace(c)) + { + restart = true; + break; + } + } + + // restart the search if signaled + if (restart) + continue; + + if (i == -1) + return null; + + int j; + for (j=pos+tag.length(); j') + break; + } + + // restart the search + if (j == html.length()) + continue; + + log.debug("found",tag,i,j); + + return Pair.create(i,j+1); + } + } + + protected static void andLeftRange (Pair range, Pair tag) + { + if (tag == null) + return; + + if (tag.second > range.first) + range.first = tag.second; + } + + protected static void andRightRange (Pair range, Pair tag) + { + if (tag == null) + return; + + if (tag.first < range.second) + range.second = tag.first; + } + + public static String stripHTML (String html) + { + if (html == null) + return null; + + String lower = html.toLowerCase(); + Pair range = Pair.create(0, html.length()); + Pair tag; + + //----------------- + + tag = findTag(true, lower, "body", range); + andLeftRange(range,tag); + + tag = findTag(true, lower, "html", range); + andLeftRange(range,tag); + + tag = findTag(true, lower, "/head", range); + andLeftRange(range,tag); + + //---------------- + + tag = findTag(false, lower, "/body", range); + andRightRange(range,tag); + + tag = findTag(false, lower, "/html", range); + andRightRange(range,tag); + + //---------------- + String result = html.substring(range.first, range.second); + log.debug("stripHTML", result); + return result; + } + + /* + public static void main (String[] args) + { + String html = "kjhasdfkjakjd < html > < body > woeifjweijf "; + System.out.println(stripHTML(html)); + } + */ + + public void setHTML(String html) + { + this.html = html; + } + + public String calculateBrief () + { + if (text == null) + return null; + + String content = calculateTextWithoutReply(); + int length = Math.min(content.length(), 256); + String brief = content.substring(0, length).replace("\n", " "); + + return brief; + } + + public String calculateReply () + { + if (text == null) + return ""; + + try + { + ArrayList lines = new ArrayList(); + BufferedReader r = new BufferedReader(new StringReader(text)); + String line; + while ((line = r.readLine()) != null) + { + lines.add("> " + line); + } + + return Strings.concat(lines.iterator(), "\n").trim(); + } + catch (Exception e) + { + return "Failed to calculate reply"; + } + } + + public boolean isProbablyReplyHeader (String s) + { + return (s.endsWith(":") && s.contains("On")); + } + + public boolean isPossiblyQuoteBeginning (String s) + { + return s.startsWith("--") || s.startsWith("=="); + } + + public String calculateTextWithoutReply () + { + if (text == null) + return ""; + + try + { + ArrayList lines = new ArrayList(); + BufferedReader r = new BufferedReader(new StringReader(text)); + String line; + + boolean alreadyFoundReply = false; + int possiblyFoundQuote = -1; + while ((line = r.readLine()) != null) + { + if (line.startsWith(">")) + { + if (!alreadyFoundReply) + { + alreadyFoundReply = true; + + for (int i=0; i<5; ++i) + { + int index = (lines.size()-i)-1; + if (index < 0) + break; + + if (isProbablyReplyHeader(lines.get(index))) + { + assert(index >=0 ); + while (lines.size() > index) + lines.remove(lines.size()-1); + + break; + } + } + } + } + else + { + lines.add(line); + + if (isPossiblyQuoteBeginning(line)) + possiblyFoundQuote = lines.size()-1; + } + } + + while (!lines.isEmpty() && lines.get(lines.size()-1).trim().isEmpty()) + lines.remove(lines.size()-1); + + if (possiblyFoundQuote >= 0 && possiblyFoundQuote > (lines.size()-6)) + { + while (lines.size() > possiblyFoundQuote) + lines.remove(lines.size()-1); + } + + return Strings.concat(lines.iterator(), "\n").trim(); + } + catch (Exception e) + { + return e.toString(); + } + } +} diff --git a/java/core/src/core/mail/client/model/ConstantsMisc.java b/java/core/src/core/mail/client/model/ConstantsMisc.java new file mode 100644 index 0000000..6e41d47 --- /dev/null +++ b/java/core/src/core/mail/client/model/ConstantsMisc.java @@ -0,0 +1,12 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +public class ConstantsMisc { + + public static String ALL = "All"; + public static final String REPLY_PREFIX = "Re:"; + +} diff --git a/java/core/src/core/mail/client/model/Conversation.java b/java/core/src/core/mail/client/model/Conversation.java new file mode 100644 index 0000000..e372209 --- /dev/null +++ b/java/core/src/core/mail/client/model/Conversation.java @@ -0,0 +1,190 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; +import org.timepedia.exporter.client.NoExport; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import core.util.Collectionz; +import core.util.Comparators; +import core.util.LogNull; +import core.util.Pair; +import mail.client.CacheManager; +import mail.client.Events; +import mail.client.cache.ID; + +@Export +public class Conversation extends Model implements Exportable +{ + static LogNull log = new LogNull(Conversation.class); + + static class SortByDateLatestFirst implements Comparator { + + @Override + public int compare(Conversation l, Conversation r) + { + Date ld = l.getHeader().getDate(), rd = r.getHeader().getDate(); + return rd.compareTo(ld); + } + } + + protected Header header; + protected List> items; + protected Set itemIds = new HashSet(); + + public Conversation (CacheManager manager) + { + super(manager); + reset(); + + getLoadCallbacks() + .addCallback(manager.getMaster().getEventPropagator().signal_(Events.LoadConversation, this)); + } + + public void reset () + { + items = new ArrayList>(); + recomputeHeader(); + } + + protected void recomputeHeader () + { + header = new Header(); + header.setDictionary(new Dictionary()); + header.setAuthors(new ArrayList()); + header.setRecipients(new Recipients()); + header.setTransportState(TransportState.NONE()); + + for (Pair p : items) + { + Mail m = getManager().getMail(p.first); + if (m.isLoaded()) + accumulate(m); + } + } + + protected void accumulate(Mail m) + { + Header h = m.getHeader(); + + if (h.getAuthor() != null) + header.getAuthors().add(h.getAuthor()); + + if (header.getDate() == null || h.getDate().after(header.getDate())) + { + header.setDate(h.getDate()); + header.setBrief(h.getBrief()); + header.setSubject(h.getSubjectExcludingReplyPrefix()); + } + + if (h.getRecipients() != null) + header.getRecipients().add(h.getRecipients()); + + header.getDictionary().add(m); + header.getTransportState().mark(h.getTransportState()); + header.unmarkState(TransportState.READ); + } + + public List getItems () + { + List result = new ArrayList(items.size()); + for (Pair p : items) + { + Mail m = getManager().getMail(p.first); + result.add(m); + } + + return result; + } + + public List> getItemIds () + { + return items; + } + + public void addItemId (ID id, Date date) + { + items.add(new Pair(id,date)); + Collections.sort(items, new Comparators.SortBySecondNatural()); + } + + public void removeItemId (ID id) + { + Collectionz.removeByFirst(items, id); + } + + public void addItem (Mail mail) + { + addItemId (mail.getId(), mail.getHeader().getDate()); + accumulate (mail); + + markDirty(); + } + + public void removeItem (Mail mail) + { + removeItemId (mail.getId()); + recomputeHeader(); + + markDirty(); + } + + public void itemChanged (Mail mail) + { + for (Pair p : items) + { + if (p.first == mail.getId()) + p.second = mail.getHeader().getDate(); + } + + Collections.sort(items, new Comparators.SortBySecondNatural()); + recomputeHeader(); + + markDirty(); + } + + public Header getHeader () + { + return header; + } + + public void setHeader (Header header) + { + this.header = header; + } + + public int getNumItems () + { + return items.size(); + } + + public void markState (String state) + { + if (!getHeader().hasState(state)) + { + getHeader().markState(state); + markDirty(); + } + } + + public void unmarkState (String state) + { + if (getHeader().hasState(state)) + { + getHeader().unmarkState(state); + markDirty(); + } + } +} diff --git a/java/core/src/core/mail/client/model/Dictionary.java b/java/core/src/core/mail/client/model/Dictionary.java new file mode 100644 index 0000000..89d7f28 --- /dev/null +++ b/java/core/src/core/mail/client/model/Dictionary.java @@ -0,0 +1,282 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.StringTokenizer; + +import core.util.Comparators; +import core.util.FastRandom; +import core.util.LogNull; +import core.util.LogOut; +import core.util.Pair; +import core.util.Strings; + +public class Dictionary implements Serializable +{ + private static final long serialVersionUID = 1L; + static LogNull log = new LogNull(Dictionary.class); + static FastRandom random = new FastRandom(); + + public Map vocabulary; + int bayesianSize=0; + + public Dictionary () + { + vocabulary = new HashMap(); + } + + public Dictionary (String filter) + { + this(); + + add(filter); + } + + public Dictionary (Mail mail) + { + this(); + + add (mail); + } + + public Map getVocabulary () + { + return vocabulary; + } + + final String TOKENS = " \t\r\n!@#$%^&*()_+-=`~{}[]\\|;:'\",./<>?"; + + public Dictionary add(String text) + { + if (text != null) + { + StringTokenizer st = new StringTokenizer(text,TOKENS); + while (st.hasMoreTokens()) + { + String token = st.nextToken().toLowerCase(); + + int occurences = 0; + + if (vocabulary.containsKey(token)) + occurences = vocabulary.get(token); + + bayesianSize ++; + vocabulary.put(token, new Integer(occurences+1)); + } + } + + return this; + } + + public Dictionary add (Mail mail) + { + if (mail.getHeader().getAuthor()!=null) + add (mail.getHeader().getAuthor().toString()); + + if (mail.getHeader().getRecipients()!=null) + for (Identity i : mail.getHeader().getRecipients().getAll()) + add(i.toString()); + + add (mail.getBody().getText()); + add (mail.getHeader().getSubject()); + + log.debug(this, "after add", toSerializableString()); + + return this; + } + + public boolean matches (Dictionary filter) + { + final String Q = "\""; + + for (Entry i : filter.vocabulary.entrySet()) + { + String match = i.getKey(); + + boolean exact = (match.startsWith(Q) && match.endsWith(Q)); + + if (exact) + { + match = match.substring(1, match.length()-1); + log.debug("match is surrounded by quotes, using exact match:",match); + } + + if (!vocabulary.containsKey(match)) + { + if (exact) + return false; + + boolean found = false; + for (Entry j : vocabulary.entrySet()) + { + if (j.getKey().startsWith(match)) + { + found = true; + break; + } + } + if (!found) + return false; + } + } + + return true; + } + + public String toSerializableString() + { + String[] strings = new String[vocabulary.size()]; + + int j=0; + for (Entry i : vocabulary.entrySet()) + { + strings[j++] = i.getKey() + ":" + i.getValue(); + } + + return Strings.concat(strings, ","); + } + + public void fromSerializableString(String string) + { + String[] strings = string.split(","); + + for (String i : strings) + { + if (i.isEmpty()) + continue; + + try + { + String[] split = i.split(":"); + int occurences = Integer.parseInt(split[1]); + + bayesianSize += occurences; + vocabulary.put(split[0], occurences); + } + catch (Exception e) + { + log.exception(e); + continue; + } + } + } + + public void add (Dictionary dictionary) + { + for (Entry i : dictionary.vocabulary.entrySet()) + { + int occurences = 0; + String token = i.getKey(); + + if (vocabulary.containsKey(i)) + occurences = vocabulary.get(token); + + bayesianSize += i.getValue(); + vocabulary.put(token, new Integer(occurences+i.getValue())); + } + + bayesianPrune(); + } + + public void subtract(Dictionary dictionary) + { + for (Entry i : dictionary.vocabulary.entrySet()) + { + int occurences = 0; + String token = i.getKey(); + + if (vocabulary.containsKey(i)) + occurences = vocabulary.get(token); + + bayesianSize -= i.getValue(); + vocabulary.put(token, new Integer(occurences-i.getValue())); + } + + bayesianPrune(); + } + + protected float bayesianProbabilityOfTerm (String term) + { + Integer v = vocabulary.get(term); + if (v == null) + return 0.0f; + + log.trace(this, "bayesianProbabilityOfTerm", term, v, "+1 /", bayesianSize); + + return (float)(v + 1)/(float)bayesianSize; + } + + public float bayesianProbability (Dictionary match) + { + float probability = 0.0f; + for (Entry i : match.vocabulary.entrySet()) + { +// probability += (float)i.getValue() * bayesianProbabilityOfTerm(i.getKey()); + probability += bayesianProbabilityOfTerm(i.getKey()); + } + + return probability; + } + + void bayesianPrune () + { + List> remove = new ArrayList>(); + + // remove all negative and zero values + for (Entry i : vocabulary.entrySet()) + { + if (i.getValue() <= 0) + remove.add(new Pair(i.getKey(), i.getValue())); + } + + for (Pair i : remove) + { + bayesianSize -= i.second; + vocabulary.remove(i.first); + } + + remove.clear(); + + // remove some parts of the remaining + for (Entry i : vocabulary.entrySet()) + remove.add(new Pair(i.getKey(), i.getValue())); + + Collections.sort(remove, new Comparators.SortBySecondNatural()); + + float numToPossiblyRemove = remove.size() - 100; + float numOkAfterThreshold = 100; + if (numToPossiblyRemove < numOkAfterThreshold) + return; + + float probabilityOfRemoval = numToPossiblyRemove/(numToPossiblyRemove + numOkAfterThreshold); + log.trace(this, "bayesianPrune", probabilityOfRemoval, numToPossiblyRemove, numOkAfterThreshold); + + for (int i=0; i term = remove.get(i); + bayesianSize -= term.second; + vocabulary.remove(term.first); + } + } + } + + public boolean bayesianMatches(Dictionary dictionary) + { + float result = bayesianProbability(dictionary); + log.debug(this, "bayesianProbability", result, this.toSerializableString(), dictionary.toSerializableString()); + + return result > 0.5f; + } + +} diff --git a/java/core/src/core/mail/client/model/Direction.java b/java/core/src/core/mail/client/model/Direction.java new file mode 100644 index 0000000..8eb1dcd --- /dev/null +++ b/java/core/src/core/mail/client/model/Direction.java @@ -0,0 +1,11 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +public enum Direction +{ + IN, + OUT +}; \ No newline at end of file diff --git a/java/core/src/core/mail/client/model/Folder.java b/java/core/src/core/mail/client/model/Folder.java new file mode 100644 index 0000000..0704d47 --- /dev/null +++ b/java/core/src/core/mail/client/model/Folder.java @@ -0,0 +1,66 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import java.util.Date; +import java.util.List; + +import core.callback.Callback; +import core.util.Pair; +import mail.client.CacheManager; +import mail.client.cache.ID; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; +import org.timepedia.exporter.client.NoExport; + +@Export() +public abstract class Folder extends Model implements Exportable +{ + FolderDefinition folderDefinition; + + @NoExport() + public Folder (CacheManager manager) + { + super(manager); + } + + public FolderDefinition getFolderDefinition() + { + return folderDefinition; + } + + public void setFolderDefinition(FolderDefinition folderDefinition) + { + this.folderDefinition = folderDefinition; + } + + public String getName() + { + return folderDefinition.getName(); + } + + public void setName(String name) + { + folderDefinition.setName(name); + markDirty(); + } + + public abstract List> getConversationIds (); + public abstract void addConversationId (ID id, Date date); + public abstract boolean isFull (); + + public abstract List getConversations (int from, int length, String filter); + public abstract boolean hasConversation (Conversation conversation); + public abstract void conversationAdded (Conversation conversation); + public abstract void conversationDeleted (Conversation conversation); + public abstract Conversation getMatchingConversation (Header header); + + public final void conversationChanged (Conversation conversation) + { + conversationDeleted(conversation); + conversationAdded(conversation); + } +} diff --git a/java/core/src/core/mail/client/model/FolderDefinition.java b/java/core/src/core/mail/client/model/FolderDefinition.java new file mode 100644 index 0000000..6a80611 --- /dev/null +++ b/java/core/src/core/mail/client/model/FolderDefinition.java @@ -0,0 +1,167 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; + +import core.util.LogNull; +import core.util.LogNull; + +@Export +public class FolderDefinition implements Exportable +{ + static LogNull log = new LogNull(FolderDefinition.class); + + String name; + Identity author; + Identity recipient; + String subject; + TransportState stateEquals, stateDiffers; + + boolean autoBayesian = false; + Dictionary bayesianDictionary; + + public FolderDefinition (String name) + { + this.name = name; + } + + public String getName () + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public FolderDefinition setAuthor(Identity author) + { + this.author = author; + return this; + } + + public Identity getAuthor () + { + return author; + } + + public FolderDefinition setRecipient(Identity recipient) + { + this.recipient = recipient; + return this; + } + + public Identity getRecipient () + { + return recipient; + } + + public FolderDefinition setSubject(String subject) + { + this.subject = subject; + return this; + } + + public String getSubject () + { + return subject; + } + + public FolderDefinition setState(TransportState stateEquals, TransportState stateDiffers) + { + this.stateEquals = stateEquals; + this.stateDiffers = stateDiffers; + return this; + } + + public TransportState getStateEquals () + { + return stateEquals; + } + + public TransportState getStateDiffers () + { + return stateDiffers; + } + + public boolean matchesFilter (Conversation conversation) + { + Header h = conversation.getHeader(); + + boolean matches = true; + if (matches && author != null) + { + matches = h.getAuthors().contains(author); + } + if (matches && recipient != null) + { + matches = h.getRecipients().contains(recipient); + } + if (matches && subject != null) + { + matches = h.getSubject().equals(subject); + } + if (matches && (stateEquals != null || stateDiffers != null)) + { + boolean equalMatch = stateEquals != null ? (h.getTransportState().hasOne(stateEquals)) : true; + boolean differMatch = stateDiffers != null ? (h.getTransportState().hasNone(stateDiffers)) : true; + + matches = equalMatch && differMatch; + log.debug("matches filter ", stateEquals, ":!", stateDiffers, " : ", h.getTransportState(), " = ("+ equalMatch, "&", differMatch, ") = ", matches); + } + if (matches && bayesianDictionary != null && autoBayesian) + { + matches = bayesianMatches(conversation); + } + + return matches; + } + + public Dictionary getBayesianDictionary () + { + return bayesianDictionary; + } + + public FolderDefinition setBayesianDictionary(Dictionary bayesianDictionary) + { + this.bayesianDictionary = bayesianDictionary; + return this; + } + + public FolderDefinition setAutoBayesian (boolean autoBayesian) + { + this.autoBayesian = autoBayesian; + return this; + } + + public boolean bayesianMatches (Conversation conversation) + { + return bayesianDictionary.bayesianMatches(conversation.getHeader().getDictionary()); + } + + public boolean getAutoBayesian () + { + return autoBayesian; + } + + public void conversationAdded(Conversation conversation) + { + if (bayesianDictionary != null) + { + bayesianDictionary.add(conversation.getHeader().getDictionary()); + } + } + + public void conversationDeleted(Conversation conversation) + { + if (bayesianDictionary != null) + { + bayesianDictionary.subtract(conversation.getHeader().getDictionary()); + } + } +} diff --git a/java/core/src/core/mail/client/model/FolderFilter.java b/java/core/src/core/mail/client/model/FolderFilter.java new file mode 100644 index 0000000..aa9f336 --- /dev/null +++ b/java/core/src/core/mail/client/model/FolderFilter.java @@ -0,0 +1,67 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import org.timepedia.exporter.client.NoExport; + +import mail.client.CacheManager; +import mail.client.cache.Type; + +public class FolderFilter extends FolderSet +{ + @NoExport + public FolderFilter (CacheManager manager) + { + super(manager, Type.FolderPart); + } + + @Override + protected void onLoaded() + { + super.onLoaded(); + preCacheMostRecentFolder(); + } + + public boolean matchesFilter (Conversation conversation) + { + return true; + } + + @Override + public synchronized void conversationAdded (Conversation conversation) + { + if (matchesFilter(conversation)) + { + super.conversationAdded(conversation); + } + } + + @Override + public synchronized void conversationDeleted (Conversation conversation) + { + if (hasConversation(conversation)) + { + super.conversationDeleted(conversation); + } + } + + public synchronized void manuallyAdd (Conversation conversation) + { + if (!super.hasConversation(conversation)) + { + folderDefinition.conversationAdded(conversation); + super.conversationAdded(conversation); + } + } + + public synchronized void manuallyRemove (Conversation conversation) + { + if (super.hasConversation(conversation)) + { + folderDefinition.conversationDeleted(conversation); + super.conversationDeleted(conversation); + } + } +} diff --git a/java/core/src/core/mail/client/model/FolderFilterSet.java b/java/core/src/core/mail/client/model/FolderFilterSet.java new file mode 100644 index 0000000..896b48b --- /dev/null +++ b/java/core/src/core/mail/client/model/FolderFilterSet.java @@ -0,0 +1,18 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import mail.client.CacheManager; +import mail.client.cache.Type; + +public class FolderFilterSet extends FolderSet +{ + + public FolderFilterSet(CacheManager manager) + { + super(manager, Type.FolderFilter); + } + +} diff --git a/java/core/src/core/mail/client/model/FolderFilterSimple.java b/java/core/src/core/mail/client/model/FolderFilterSimple.java new file mode 100644 index 0000000..9a4ca70 --- /dev/null +++ b/java/core/src/core/mail/client/model/FolderFilterSimple.java @@ -0,0 +1,27 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; +import org.timepedia.exporter.client.NoExport; + +import mail.client.CacheManager; + +@Export() +public class FolderFilterSimple extends FolderFilter +{ + @NoExport + public FolderFilterSimple(CacheManager manager) + { + super(manager); + } + + @Override + public boolean matchesFilter (Conversation conversation) + { + return folderDefinition.matchesFilter(conversation); + } +} diff --git a/java/core/src/core/mail/client/model/FolderMaster.java b/java/core/src/core/mail/client/model/FolderMaster.java new file mode 100644 index 0000000..0081300 --- /dev/null +++ b/java/core/src/core/mail/client/model/FolderMaster.java @@ -0,0 +1,88 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.NoExport; + + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import core.util.Base64; +import core.util.Strings; + +import core.crypt.HashSha256; + +import mail.client.CacheManager; +import mail.client.cache.Type; + +@Export() +public class FolderMaster extends FolderFilterSet +{ + HashSha256 hasher = new HashSha256(); + Map externalKeys = new HashMap(); + Map uidls = new HashMap(); + + @NoExport + public FolderMaster(CacheManager manager) + { + super(manager); + } + + protected String hash (String key) + { + try + { + return Base64.encode(hasher.hash(Strings.toBytes(key))); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + public void addExternalKey (String id, Date date) + { + addExternalKeyHash(hash(id), date); + markDirty(); + } + + public void addExternalKeyHash (String hash, Date date) + { + externalKeys.put(hash, date); + } + + public boolean containsExternalKey (String id) + { + return externalKeys.containsKey(hash(id)); + } + + public void addUIDL (String uidl, Date date) + { + addUIDLHash(hash(uidl), date); + markDirty(); + } + + public void addUIDLHash (String hash, Date date) + { + uidls.put(hash, date); + } + + public boolean containsUIDL (String uidl) + { + return uidls.containsKey(hash(uidl)); + } + + public Map getUIDLHashes() + { + return uidls; + } + + public Map getExternalKeyHashes () + { + return externalKeys; + } +} diff --git a/java/core/src/core/mail/client/model/FolderPart.java b/java/core/src/core/mail/client/model/FolderPart.java new file mode 100644 index 0000000..28bdd99 --- /dev/null +++ b/java/core/src/core/mail/client/model/FolderPart.java @@ -0,0 +1,155 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import org.timepedia.exporter.client.NoExport; + +import core.util.Collectionz; +import core.util.Comparators; +import core.util.LogNull; +import core.util.LogOut; +import core.util.Pair; + +import mail.client.CacheManager; +import mail.client.Events; +import mail.client.cache.ID; + +public class FolderPart extends Folder +{ + static LogNull log = new LogNull(FolderPart.class); + static final int MAX_FOLDER_CONVERSATIONS = 1000; + + List> conversations; + + @NoExport + public FolderPart (CacheManager manager) + { + super(manager); + reset(); + + getLoadCallbacks() + .addCallback(manager.getMaster().getEventPropagator().signal_(Events.LoadFolderPart, this)); + } + + public void reset () + { + conversations = new ArrayList>(); + } + + @Override + public List getConversations (int from, int length, String filter) + { + CacheManager cache = getManager(); + + log.debug("getConversations ", from, ":", length); + + Dictionary filterDictionary = null; + if (filter != null) + filterDictionary = new Dictionary(filter); + + log.debug("getConversations using filter dictionary", filterDictionary); + + int min = Math.min(from+length, conversations.size()); + List result = new ArrayList(min); + + for (int i=from; i(id, date)); + Collections.sort(conversations, new Comparators.SortBySecondNaturalOpposite()); + } + + protected synchronized void removeConversationId (Object id) + { + Collectionz.removeByFirst(conversations, id); + } + + @Override + public List> getConversationIds() + { + return conversations; + } + + @Override + public boolean isFull () + { + return conversations.size() > MAX_FOLDER_CONVERSATIONS; + } + + @Override + public synchronized void conversationAdded (Conversation conversation) + { + addConversationId(conversation.getId(), conversation.getHeader().getDate()); + markDirty(); + } + + @Override + public synchronized void conversationDeleted (Conversation conversation) + { + removeConversationId(conversation.getId()); + markDirty(); + } + + @Override + public Conversation getMatchingConversation (Header header) + { + if (header.getSubject() == null) + return null; + + String headerSubject = header.getSubjectExcludingReplyPrefix (); + + for (Pair pair : conversations) + { + ID id = pair.first; + Conversation conversation = getManager().getConversation(id); + + if (conversation.isLoaded()) + { + Header compare = conversation.getHeader(); + String compareSubject = compare.getSubjectExcludingReplyPrefix(); + + if (compareSubject.toLowerCase().equals(headerSubject.toLowerCase())) + { + return conversation; + } + } + } + + return null; + } +} diff --git a/java/core/src/core/mail/client/model/FolderRepository.java b/java/core/src/core/mail/client/model/FolderRepository.java new file mode 100644 index 0000000..b775bf7 --- /dev/null +++ b/java/core/src/core/mail/client/model/FolderRepository.java @@ -0,0 +1,29 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; +import org.timepedia.exporter.client.NoExport; + +import mail.client.CacheManager; +import mail.client.cache.Type; + +@Export() +public class FolderRepository extends FolderSet +{ + @NoExport + public FolderRepository(CacheManager manager) + { + super(manager, Type.FolderPart); + } + + @Override + protected void onLoaded() + { + super.onLoaded(); + preCacheMostRecentFolder(); + } +} diff --git a/java/core/src/core/mail/client/model/FolderSet.java b/java/core/src/core/mail/client/model/FolderSet.java new file mode 100644 index 0000000..eca8b51 --- /dev/null +++ b/java/core/src/core/mail/client/model/FolderSet.java @@ -0,0 +1,246 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.NoExport; + + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import core.callbacks.Single; +import core.util.LogNull; +import core.util.Pair; + +import mail.client.CacheManager; +import mail.client.Events; +import mail.client.cache.ID; +import mail.client.cache.Type; + +@Export() +public class FolderSet extends Folder +{ + static LogNull log = new LogNull(FolderSet.class); + List parts; + int numConversations; + Type childType; + + @NoExport + public FolderSet(CacheManager manager, Type childType) + { + super(manager); + this.childType = childType; + reset(); + + getLoadCallbacks() + .addCallback(manager.getMaster().getEventPropagator().signal_(Events.LoadFolder, this)); + } + + public void preCacheMostRecentFolder () + { + if (!parts.isEmpty()) + { + CacheManager cache = getManager(); + cache.getFolder(getChildType(), parts.get(0)); + } + } + + public void reset () + { + parts = new ArrayList(); + this.numConversations = 0; + } + + public void addFolderId (ID id) + { + parts.add(0,id); + } + + protected void addFolderIdEnd (ID id) + { + parts.add(id); + } + + public List getFolderIds() + { + return parts; + } + + public void addFolder (Folder folder) + { + addFolderIdEnd(folder.getId()); + markDirty(); + } + + public void removeFolder(Folder folder) + { + if (!parts.contains(folder.getId())) + return; + + parts.remove(folder.getId()); + markDirty(); + + if (folder.isLoaded()) + folder.markDeleted(); + else + folder.getLoadCallbacks().addCallback(new Single(markDeleted_())); + } + + protected void onDeleting () + { + List parts = getFolders(); + for(Folder part : parts) + removeFolder(part); + } + + public List getFolders () + { + List f = new ArrayList(parts.size()); + for (ID id : parts) + f.add(getManager().getFolder(getChildType(), id)); + + return f; + } + + @Override + public List getConversations(int from, int length, String filter) + { + log.debug("filter: ", filter); + + int totalLength = from+length; + List result = new ArrayList(totalLength); + + for (ID id : parts) + { + Folder f = getManager().getFolder(getChildType(), id); + + if (f.isLoaded()) + { + result.addAll(f.getConversations(0, totalLength - result.size(), filter)); + + if (result.size() >= totalLength) + break; + } + else + break; + } + + totalLength = Math.min(totalLength, result.size()); + return result.subList(from, totalLength); + } + + @Override + public boolean hasConversation(Conversation conversation) + { + for (ID id : parts) + { + Folder f = getManager().getFolder(getChildType(), id); + + if (f.isLoaded()) + if (f.hasConversation(conversation)) + return true; + } + + return false; + } + + public Type getChildType () + { + return childType; + } + + @Override + public void conversationAdded(Conversation conversation) + { + CacheManager cache = getManager(); + + Folder first = !parts.isEmpty() ? cache.getFolder(getChildType(), parts.get(0)) : null; + + log.debug ("FolderSet.conversationAdded ", first); + + if (parts.isEmpty() || !first.isLoaded() || first.isFull()) + { + first = cache.newFolder(getChildType(), new FolderDefinition(getId().toFileSystemSafe() + ":part")); + parts.add(0, first.getId()); + } + + first.conversationAdded(conversation); + + numConversations++; + markDirty(); + } + + @Override + public void conversationDeleted(Conversation conversation) + { + for (ID id : parts) + { + Folder f = getManager().getFolder(getChildType(), id); + + if (f.isLoaded()) + { + if (f.hasConversation(conversation)) + { + f.conversationDeleted(conversation); + numConversations--; + markDirty(); + + break; + } + } + } + + } + + public int getNumConversations () + { + return numConversations; + } + + public void setNumConversations (int numConversations) + { + this.numConversations = numConversations; + } + + @Override + public List> getConversationIds() + { + assert(false); + return null; + } + + @Override + public void addConversationId(ID id, Date date) + { + assert(false); + } + + @Override + public boolean isFull() + { + assert(false); + return false; + } + + @Override + public Conversation getMatchingConversation (Header header) + { + for (ID id : parts) + { + Folder f = getManager().getFolder(getChildType(), id); + + if (f.isLoaded()) + { + Conversation c = f.getMatchingConversation(header); + if (c != null) + return c; + } + } + + return null; + } +} diff --git a/java/core/src/core/mail/client/model/Header.java b/java/core/src/core/mail/client/model/Header.java new file mode 100644 index 0000000..3da40bf --- /dev/null +++ b/java/core/src/core/mail/client/model/Header.java @@ -0,0 +1,325 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; + + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import mail.client.Master; + +import core.util.DateFormat; +import core.util.LogNull; +import core.util.LogOut; +import core.util.Strings; + +@Export() +public class Header implements Exportable +{ + static LogNull log = new LogNull (Header.class); + + protected String externalKey; + protected String originalKey; + protected Identity author; + protected Set authors; + protected String subject; + protected Date date; + protected Recipients recipients; + protected Dictionary dictionary; + protected TransportState state; + protected String brief; + protected String uidl; + + public Header (String externalKey, String originalKey, String uidl, Identity author, Recipients recipients, String subject, Date date, TransportState state, String brief) + { + this.externalKey = externalKey; + this.originalKey = originalKey; + this.uidl = uidl; + this.author = author; + this.subject = subject; + this.date = date; + this.recipients = recipients; + this.state = state; + this.brief = brief; + } + + public Header () + { + + } + + public String getExternalKey () + { + return externalKey; + } + + public void setOriginalKey (String originalKey) + { + this.originalKey = originalKey; + } + + public String getOriginalKey () + { + return originalKey; + } + + public void setExternalKey (String externalKey) + { + this.externalKey = externalKey; + } + + public Identity getAuthor () + { + return author; + } + + public Identity[] filterMe (List identities, Identity me) + { + if (identities == null) + return null; + + List filtered = new ArrayList(); + for (Identity identity : identities) + { + if (identity != me) + { + log.debug("filterMe adding", identity.debug(), "is not",me.debug()); + filtered.add(identity); + } + } + + if (filtered.isEmpty()) + return null; + + return filtered.toArray(new Identity[0]); + } + + public Identity[] calculateReplyTo (Master master) + { + log.debug("calculate replyTo"); + Identity me = master.getIdentity(); + + Identity[] results = null; + if (getRecipients()!=null) + { + log.debug("recipients not null"); + + results = filterMe(getRecipients().getReplyTo(), me); + if (results != null) + return results; + + if (getAuthor() == me) + { + results = filterMe(getRecipients().getAll(), me); + if (results != null) + return results; + } + } + + if (getAuthor() != null) + return new Identity[] { getAuthor() }; + + return new Identity[0]; + } + + public Identity[] calculateReplyAll (Master master) + { + Identity me = master.getIdentity(); + ArrayList results = new ArrayList(); + + if (getRecipients()!=null) + { + Identity[] filtered = filterMe(getRecipients().getAll(), me); + if (filtered != null) + for (Identity identity : filtered) + results.add(identity); + } + + Identity author = getAuthor(); + if (author!=null && author != me && !results.contains(author)) + results.add(0, author); + + return results.toArray(new Identity[0]); + } + + public void setAuthor (Identity author) + { + this.author = author; + } + + public Set getAuthors () + { + return authors; + } + + public void setAuthors (List authors) + { + this.authors = new HashSet(); + this.authors.addAll(authors); + } + + public String getAuthorsShortList () + { + String[] shorts = new String[authors.size()]; + int j=0; + for (Identity i : authors) + shorts[j++] = i.getShortName(); + + return Strings.concat(shorts, ", "); + } + + public String getSubject () + { + return subject; + } + + public String getSubjectExcludingReplyPrefix () + { + String subject = this.subject; + + if (subject == null) + subject = ""; + + while (subject.toLowerCase().startsWith(ConstantsMisc.REPLY_PREFIX.toLowerCase())) + { + subject = subject.substring(ConstantsMisc.REPLY_PREFIX.length()).trim(); + } + + return subject; + } + + public Dictionary getDictionary () + { + return dictionary; + } + + public void setDictionary (Dictionary dictionary) + { + this.dictionary = dictionary; + } + + public Date getDate () + { + return date; + } + + public void setDate (Date date) + { + this.date = date; + } + + public Recipients getRecipients () + { + return recipients; + } + + public void setRecipients (Recipients recipients) + { + this.recipients = recipients; + } + + public void setSubject(String subject) + { + this.subject = subject; + } + + public void setTransportState (TransportState state) + { + this.state = state; + } + + public TransportState getTransportState () + { + return state; + } + + public boolean hasState(String flag) + { + return state.has(flag); + } + + public void markState (String flag) + { + state.mark(flag); + } + + public void unmarkState(String flag) + { + state.unmark(flag); + } + + public void markModification () + { + this.date = new Date(); + } + + public String getBrief() + { + return brief; + } + + public void setBrief(String brief) + { + this.brief = brief; + } + + public String getRelativeDate () + { + String result = "Unknown"; + + if (date == null) + result = "Infinity + 1"; + else + { + DateFormat std = new DateFormat("yyyyMMddhhmmss"); + String now = std.format(new Date()); + String then = std.format(date); + + if (then.startsWith(now.substring(0,10))) + { + long diff = (new Date().getTime()/(60 * 1000)) - (date.getTime()/(60 * 1000)); + result = diff + " min"; + } + else + if (then.startsWith(now.substring(0,8))) + { + DateFormat sdf = new DateFormat("h:mm a"); + result = sdf.format(date).toLowerCase(); + } + else + { + if (then.startsWith(now.substring(0,4))) + { + DateFormat sdf = new DateFormat("MMM d"); + result = sdf.format(date); + } + else + { + DateFormat sdf = new DateFormat("MM/dd/yy"); + result = sdf.format(date); + } + } + } + + log.debug("Header.getRelativeDate ", result); + return result; + } + + public String getUIDL() + { + return uidl; + } + + public void setUIDL (String uidl) + { + this.uidl = uidl; + } +} diff --git a/java/core/src/core/mail/client/model/Identity.java b/java/core/src/core/mail/client/model/Identity.java new file mode 100644 index 0000000..8f46a21 --- /dev/null +++ b/java/core/src/core/mail/client/model/Identity.java @@ -0,0 +1,187 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; + + +import java.io.Serializable; + +import core.util.LogNull; +import core.util.LogOut; +import core.util.Pair; +import core.util.Strings; + +@Export() +public class Identity implements Serializable, Exportable +{ + private static final long serialVersionUID = 1L; + static LogNull log = new LogNull(Identity.class); + + String name; + String email; + boolean isPrimary = false; + String publicKey; + + protected Identity () + { + } + + protected Identity (String name, String email, boolean isPrimary) + { + this.name = name; + this.email = email; + this.isPrimary = isPrimary; + } + + /** + * Rewrite with regular expressions + * @param full + */ + public Identity (String full) + { + Pair parsed = parseFull(full); + this.name = parsed.first; + this.email = parsed.second; + } + + public void setPrimary (boolean primary) + { + this.isPrimary = primary; + } + + public static Pair parseFull (String full) + { + String name = null; + String email = null; + + int indexOfLessThan = full.lastIndexOf('<'); + int indexOfGreaterThan = full.indexOf('>', indexOfLessThan); + if (indexOfLessThan != -1 && indexOfGreaterThan != -1) + { + name = full.substring(0, indexOfLessThan); + name = name.trim(); + if (name.isEmpty()) + name = null; + + String t = full.substring(indexOfLessThan+1); + email = t.substring(0, indexOfGreaterThan - indexOfLessThan - 1); + } + else + { + email = full; + } + + return new Pair(name, email); + } + + public String getFull() + { + if (name != null && !name.isEmpty()) + return name + " " + getEnclosedEmail(); + + return getEnclosedEmail(); + } + + public String toString() + { + return getFull(); + } + + public String debug () + { + return super.toString() + getFull(); + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getEmail() + { + return email; + } + + public String getEnclosedEmail () + { + if (email != null) + return "<" + email + ">"; + + return null; + } + + public void setEmail(String email) + { + this.email = email; + } + + public String getShortName () + { + if (isPrimary) + return "me"; + + if (name != null) + if (name.contains(" ")) + if (name.contains(",")) + return Strings.trimQuotes(name.substring(name.indexOf(' ')+1)); + else + return Strings.trimQuotes(name.substring(0, name.indexOf(' '))); + else + return name; + + return email; + } + + public String getLongName () + { + return name; + } + + /** + * This should check which information is better, + * possibly based on amount of information in the information? + * @param identity + */ + public void copyFrom (Identity identity) + { + if (!this.isPrimary) + { + log.debug("copying from ", identity.isPrimary, identity.name, identity.email, this.isPrimary, this.name, this.email); + + if ( + this.name == null || + ( (identity.name != null) && (this.name.length() < identity.name.length()) ) + ) + { + this.name = identity.name; + } + + if (this.email == null) + this.email = identity.email; + } + } + + public void setPublicKey (String publicKey) + { + this.publicKey = publicKey; + } + + public boolean hasPublicKey() + { + return publicKey != null; + } + + public String getPublicKey() + { + return publicKey; + } +} diff --git a/java/core/src/core/mail/client/model/Mail.java b/java/core/src/core/mail/client/model/Mail.java new file mode 100644 index 0000000..007688c --- /dev/null +++ b/java/core/src/core/mail/client/model/Mail.java @@ -0,0 +1,197 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +import com.sun.org.apache.xml.internal.security.utils.Base64; + +import sun.reflect.ReflectionFactory.GetReflectionFactoryAction; + +import mail.client.CacheManager; +import mail.client.Events; + +import core.constants.ConstantsMailJson; +import core.crypt.CryptorAES; +import core.crypt.CryptorRSAAES; +import core.crypt.CryptorRSAFactory; +import core.util.JSON_; +import core.util.LogNull; +import core.util.Pair; +import core.util.Strings; + +public class Mail extends Model +{ + static LogNull log = new LogNull(Mail.class); + + static class SortByDateLatestFirst implements Comparator { + + @Override + public int compare(Mail l, Mail r) + { + Date ld = l.getHeader().getDate(), rd = r.getHeader().getDate(); + return rd.compareTo(ld); + } + } + + static class SortByDateLatestLast implements Comparator { + + @Override + public int compare(Mail l, Mail r) + { + Date ld = l.getHeader().getDate(), rd = r.getHeader().getDate(); + return ld.compareTo(rd); + } + } + + Header header; + Body body; + Attachments attachments; + + public Mail (CacheManager manager) + { + super(manager); + + getLoadCallbacks() + .addCallback(manager.getMaster().getEventPropagator().signal_(Events.LoadMail, this)); + } + + public void reset () + { + + } + + public Header getHeader () + { + return header; + } + + public void setHeader (Header header) + { + this.header = header; + } + + public Body getBody () + { + return body; + } + + public void setBody (Body body) + { + this.body = body; + } + + public void setBody (String text, String html) + { + body.setText(text); + body.setHTML(html); + header.setBrief(body.calculateBrief()); + } + + public Identity[] calculateReplyTo () + { + return header.calculateReplyTo(getManager().getMaster()); + } + + public Body calculateReply (String signature) + { + Body reply = new Body(); + + String author = "" + getHeader().getAuthor(); + String date = "" + getHeader().getDate(); + + String prefix = "On " + date + ", " + author + " wrote:"; + String html = + "

" + signature + "

" + prefix + "
" + + "
" + + (body.hasHTML() ? body.getStrippedHTML() : ("
" + body.getStrippedText() + "
")) + "
"; + + reply.setText("\r\n\r\n" + signature + "\r\n\r\n" + prefix + "\r\n" + body.calculateReply()); + reply.setHTML(html); + + return reply; + } + + public Attachments getAttachments () + { + return attachments; + } + + public void setAttachments(Attachments attachments) + { + this.attachments = attachments; + } + + public boolean isPresendEncryptable() + { + for (Identity i : getHeader().getRecipients().getAll()) + { + if (!i.hasPublicKey()) + return false; + } + + return true; + } + + public Pair presendEncrypt () throws Exception + { + byte[] aesKey = CryptorAES.newKey(); + CryptorAES aes = new CryptorAES(aesKey); + + List cryptors = new ArrayList(); + for (Identity i : getHeader().getRecipients().getAll()) + { + CryptorRSAAES rsaaes = new CryptorRSAAES(CryptorRSAFactory.fromString(i.getPublicKey(), null)); + cryptors.add(Base64.encode(rsaaes.encrypt(aesKey))); + } + + Object json = JSON_.newObject(); + JSON_.put(json, ConstantsMailJson.Class, JSON_.newString(ConstantsMailJson.MultiPart)); + Object multiPart = JSON_.newArray(); + + + if (getBody().hasText()) + { + Object part = JSON_.newObject(); + Object headers = JSON_.newArray(); + Object mimeType = JSON_.newArray(); + JSON_.add(mimeType, JSON_.newString("Content-Type")); + JSON_.add(mimeType, JSON_.newString("text/plain")); + JSON_.add(headers, mimeType); + JSON_.put(part, ConstantsMailJson.Class, ConstantsMailJson.String); + JSON_.put(part, ConstantsMailJson.Value, getBody().getText()); + JSON_.put(part, ConstantsMailJson.Headers, headers); + + JSON_.add(multiPart, part); + } + + { + Object part = JSON_.newObject(); + Object headers = JSON_.newArray(); + Object mimeType = JSON_.newArray(); + JSON_.add(mimeType, JSON_.newString("Content-Type")); + JSON_.add(mimeType, JSON_.newString("text/html")); + JSON_.add(headers, mimeType); + JSON_.put(part, ConstantsMailJson.Class, ConstantsMailJson.String); + JSON_.put(part, ConstantsMailJson.Value, getBody().getHTML()); + JSON_.put(part, ConstantsMailJson.Headers, headers); + + JSON_.add(multiPart, part); + } + + Object container = JSON_.newObject(); + JSON_.put(container, "subject", getHeader().getSubject()); + JSON_.put(container, "content", multiPart); + + return new Pair( + Strings.concat(cryptors,","), + Base64.encode(aes.encrypt(Strings.toBytes(JSON_.asString(container)))) + ); + } +} diff --git a/java/core/src/core/mail/client/model/Model.java b/java/core/src/core/mail/client/model/Model.java new file mode 100644 index 0000000..6df9cb3 --- /dev/null +++ b/java/core/src/core/mail/client/model/Model.java @@ -0,0 +1,38 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import mail.client.CacheManager; + +public class Model extends mail.client.cache.Item +{ + CacheManager manager; + + public Model(CacheManager manager) + { + this.manager = manager; + } + + public CacheManager getManager () + { + return manager; + } + + protected void onPreLoad () + { + reset(); + } + + protected void onDirty () + { + super.onDirty(); + manager.onModelDirty(); + } + + public void reset () + { + + } +} diff --git a/java/core/src/core/mail/client/model/ModelFactory.java b/java/core/src/core/mail/client/model/ModelFactory.java new file mode 100644 index 0000000..200fb94 --- /dev/null +++ b/java/core/src/core/mail/client/model/ModelFactory.java @@ -0,0 +1,44 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import mail.client.CacheManager; +import mail.client.cache.Item; +import mail.client.cache.ItemFactory; +import mail.client.cache.Type; + +public class ModelFactory implements ItemFactory +{ + CacheManager manager; + + public ModelFactory (CacheManager manager) + { + this.manager = manager; + } + + @Override + public Item instantiate (Type type) + { + switch (type) + { + case Mail: + return new Mail(manager); + case Conversation: + return new Conversation(manager); + case FolderPart: + return new FolderPart(manager); + case FolderFilterSet: + return new FolderFilterSet(manager); + case FolderMaster: + return new FolderMaster(manager); + case FolderFilter: + return new FolderFilterSimple(manager); + case FolderRepository: + return new FolderRepository(manager); + } + + return null; + } +} diff --git a/java/core/src/core/mail/client/model/ModelSerializer.java b/java/core/src/core/mail/client/model/ModelSerializer.java new file mode 100644 index 0000000..15060a5 --- /dev/null +++ b/java/core/src/core/mail/client/model/ModelSerializer.java @@ -0,0 +1,86 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import core.callback.Callback; +import core.callback.CallbackDefault; +import core.util.JSON_; +import core.util.LogNull; +import core.util.LogOut; +import core.util.Strings; +import core.util.Zip; +import mail.client.cache.Item; +import mail.client.cache.ItemSerializer; +import mail.client.cache.JSON; + +public class ModelSerializer implements ItemSerializer +{ + static LogNull log = new LogNull(ModelSerializer.class); + + JSON json; + + public ModelSerializer(JSON json) + { + this.json = json; + } + + public byte[] serialize (Item item) throws Exception + { + log.debug("serialize", item); + + item.markPreStore(); + + if (item instanceof Mail) + return Strings.toBytes(json.toJSON((Mail)item).toString()); + if (item instanceof Conversation) + return Strings.toBytes(json.toJSON((Conversation)item).toString()); + if (item instanceof Folder) + return Strings.toBytes(json.toJSON((Folder)item).toString()); + if (item instanceof Settings) + return Strings.toBytes(json.toJSON((Settings)item).toString()); + + return null; + } + + public void deserialize (Item item, byte[] bytes) throws Exception + { + log.debug("deserialize", item); + + item.markPreLoad(); + + if (item instanceof Mail) + json.fromJSON((Mail)item, JSON_.parse(Strings.toString(bytes))); + if (item instanceof Conversation) + json.fromJSON((Conversation)item, JSON_.parse(Strings.toString(bytes))); + if (item instanceof Folder) + json.fromJSON((Folder)item, JSON_.parse(Strings.toString(bytes))); + if (item instanceof Settings) + json.fromJSON((Settings)item, JSON_.parse(Strings.toString(bytes))); + } + + @Override + public Callback serialize_(Item item) + { + return new CallbackDefault(item) { + public void onSuccess(Object... arguments) throws Exception { + next(serialize((Item)V(0))); + } + }.addCallback(Zip.deflate_()); + } + + @Override + public Callback deserialize_(Item item) + { + return Zip.inflate_().addCallback( + new CallbackDefault(item) { + public void onSuccess(Object... arguments) throws Exception { + deserialize((Item)V(0), (byte[])arguments[0]); + next(); + } + } + ); + } + +} diff --git a/java/core/src/core/mail/client/model/Original.java b/java/core/src/core/mail/client/model/Original.java new file mode 100644 index 0000000..b5ed4a6 --- /dev/null +++ b/java/core/src/core/mail/client/model/Original.java @@ -0,0 +1,78 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import java.io.UnsupportedEncodingException; +import java.util.Date; +import java.util.List; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; + +import core.util.LogNull; + +@Export() +public class Original implements Exportable +{ + static LogNull log = new LogNull(Original.class); + + String path; + boolean loaded; + + Exception exception; + byte[] data; + + public Original (String path) + { + this.path = path; + } + + public String getPath () + { + return path; + } + + public void setData (byte[] data) + { + this.data = data; + this.loaded = true; + } + + public boolean hasData () + { + return data!=null; + } + + public String getDataAsString () throws UnsupportedEncodingException + { + return new String(data); + } + + public boolean isLoaded () + { + return loaded; + } + + public boolean hasException() + { + return exception!=null; + } + + public void setException(Exception exception) + { + this.exception = exception; + this.loaded = true; + } + + public Exception getException() + { + return exception; + } + + public boolean equals (Original rhs) + { + return super.equals(rhs); + } +} diff --git a/java/core/src/core/mail/client/model/Original.java.no b/java/core/src/core/mail/client/model/Original.java.no new file mode 100644 index 0000000..5091e20 --- /dev/null +++ b/java/core/src/core/mail/client/model/Original.java.no @@ -0,0 +1,149 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.core; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; + +import javax.mail.BodyPart; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import com.sun.mail.util.BASE64DecoderStream; + +import core.util.Arrays; +import core.util.LogNull; +import core.util.LogNull; +import core.util.Pair; +import core.util.Streams; +import core.util.Triple; + +public class Original +{ + static LogNull log = new LogNull(Original.class); + + String path; + boolean loaded; + + Exception exception; + byte[] data; + + public Original (String path) + { + this.path = path; + } + + public String getPath () + { + return path; + } + + public void setData (byte[] data) + { + this.data = data; + this.loaded = true; + } + + public boolean hasData () + { + return data!=null; + } + + public String getDataAsString () throws UnsupportedEncodingException + { + return new String(data); + } + + public boolean isLoaded () + { + return loaded; + } + + public boolean hasException() + { + return exception!=null; + } + + public void setException(Exception exception) + { + this.exception = exception; + this.loaded = true; + } + + public Exception getException() + { + return exception; + } + + List attachments; + + public void loadAttachments (Attachments attachments) throws Exception + { + log.debug("loadAttachments a ", data.length, " "+ new Date()); + + Session s = Session.getDefaultInstance(new Properties()); + MimeMessage message = new MimeMessage(s, new ByteArrayInputStream(data)); + + log.debug("loadAttachments b ", new Date()); + List> contents = new ArrayList>(); + contents.add(new Triple(message.getDisposition(), message.getContentID(), message.getContent())); + while (contents.size() > 0) + { + log.debug("loadAttachments c ", new Date()); + + Triple p = contents.get(0); + contents.remove(0); + + String disposition = p.first; + String id = p.second; + Object content = p.third; + + if (content instanceof InputStream) + { + String attachmentId = Attachment.getAttachmentId(disposition, id); + + Attachment attachment = attachments.getAttachment(attachmentId); + if (attachment != null) + { + attachment.setData (Streams.readFullyBytes((InputStream)content)); + } + } + else + if (content instanceof MimeMultipart) + { + MimeMultipart m = (MimeMultipart)content; + for (int i=0; i( + Arrays.firstOrNull(b.getHeader("Content-Disposition")), + Arrays.firstOrNull(b.getHeader("Content-Id")), + b.getContent() + ) + ); + } + } + } + + log.debug("loadAttachments d ", new Date()); + attachments.setLoaded(true); + } + + public List getAttachments () + { + log.debug("getAttachments", new Date()); + return attachments; + } +} diff --git a/java/core/src/core/mail/client/model/Recipients.java b/java/core/src/core/mail/client/model/Recipients.java new file mode 100644 index 0000000..6ac440a --- /dev/null +++ b/java/core/src/core/mail/client/model/Recipients.java @@ -0,0 +1,207 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import java.util.ArrayList; +import java.util.List; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; + +import mail.client.Master; + +import core.util.Strings; + +@Export() +public class Recipients implements Exportable +{ + public static final String To = "to", Cc = "cc", Bcc = "bcc", ReplyTo = "reply-to"; + + protected List to, cc, bcc, replyTo; + + List toIdentityList (Master master, String is) + { + AddressBook addressBook = master.getAddressBook(); + + String[] parts = is.split(","); + List identities = new ArrayList(parts.length); + + for (String part : parts) + { + String trimmed = part.trim(); + if (trimmed.isEmpty()) + continue; + + Identity identity = addressBook.getIdentity(new UnregisteredIdentity(trimmed)); + if (!identities.contains(identity)) + identities.add(identity); + } + + return identities; + } + + public Recipients () + { + this.to = new ArrayList(); + this.cc = new ArrayList(); + this.bcc = new ArrayList(); + this.replyTo = new ArrayList(); + } + + public Recipients (Identity[] to, Identity[] cc, Identity[] bcc, Identity[] replyTo) + { + this.to = new ArrayList(); + this.cc = new ArrayList(); + this.bcc = new ArrayList(); + this.replyTo = new ArrayList(); + + if (to != null) + for (Identity i : to) + this.to.add(i); + + if (cc != null) + for (Identity i : cc) + this.cc.add(i); + + if (bcc != null) + for (Identity i : bcc) + this.bcc.add(i); + + if (replyTo != null) + for (Identity i : replyTo) + this.replyTo.add(i); + + } + + public void add (List from, List to) + { + for (Identity i : from) + if (!to.contains(i)) + to.add(i); + } + + public void add (Recipients r) + { + add (r.to, to); + add (r.cc, cc); + add (r.bcc, bcc); + add (r.replyTo, replyTo); + } + + public List get(String key) + { + if (key.equals(To)) + return to; + else + if (key.equals(Cc)) + return cc; + else + if (key.equals(Bcc)) + return bcc; + else + if (key.equals(ReplyTo)) + return replyTo; + + return null; + } + + public List getTo() + { + return to; + } + + public void setTo (List to) + { + this.to = to; + } + + public List getCc () + { + return cc; + } + + public void setCc (List cc) + { + this.cc = cc; + } + + public List getBcc () + { + return bcc; + } + + public void setBcc (List bcc) + { + this.bcc = bcc; + } + + public List getReplyTo () + { + return replyTo; + } + + public void setReplyTo (List replyTo) + { + this.replyTo = replyTo; + } + + public List getAll () + { + ArrayList all = new ArrayList(); + all.addAll(to); + all.addAll(cc); + all.addAll(bcc); + all.addAll(replyTo); + + ArrayList once = new ArrayList(); + for (Identity identity : all) + { + if (!once.contains(all)) + once.add(identity); + } + + return once; + } + + protected void registerRecipients (AddressBook addressBook, List to) + { + Identity[] save = to.toArray(new Identity[0]); + to.clear(); + + for (Identity i : save) + { + if (i instanceof UnregisteredIdentity) + to.add(addressBook.getIdentity((UnregisteredIdentity)i)); + else + to.add(i); + } + } + + public void registerRecipients (AddressBook addressBook) + { + registerRecipients (addressBook, to); + registerRecipients (addressBook, cc); + registerRecipients (addressBook, bcc); + registerRecipients (addressBook, replyTo); + } + + public boolean contains (Identity identity) + { + return + to.contains(identity) || + cc.contains(identity) || + bcc.contains(identity) || + replyTo.contains(identity); + } + + public String shortList () + { + List shorts = new ArrayList(); + for (Identity i : getAll()) + shorts.add(i.getShortName()); + + return Strings.concat(shorts.iterator(), ", "); + } +} diff --git a/java/core/src/core/mail/client/model/Settings.java b/java/core/src/core/mail/client/model/Settings.java new file mode 100644 index 0000000..fceec57 --- /dev/null +++ b/java/core/src/core/mail/client/model/Settings.java @@ -0,0 +1,77 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import java.util.HashMap; +import java.util.Map; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; + +import mail.client.CacheManager; +import mail.client.cache.Item; + +@Export() +public class Settings extends Model implements Exportable +{ + public static final String + VERSION = "version", + CURRENT_VERSION = "1.0"; + + protected Map settings = new HashMap(); + + public Settings(CacheManager manager) + { + super(manager); + } + + @Override + public void onLoaded () + { + super.onLoaded(); + manager.onSettingsChanged(this); + } + + @Override + public void onDirty () + { + super.onDirty(); + manager.onSettingsChanged(this); + } + + public String get (String key) + { + return settings.get(key); + } + + public String get(String key, String defaultz) + { + String value = settings.get(key); + if (value != null) + return value; + + return defaultz; + } + + public void set(String key, String value) + { + String existing = get(key); + if (existing != value) + { + settings.put(key, value); + markDirty(); + } + } + + public Map getKV() + { + return settings; + } + + public void setKV(Map kv) + { + settings = kv; + } +} diff --git a/java/core/src/core/mail/client/model/TransportState.java b/java/core/src/core/mail/client/model/TransportState.java new file mode 100644 index 0000000..5210cd4 --- /dev/null +++ b/java/core/src/core/mail/client/model/TransportState.java @@ -0,0 +1,142 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; + +import core.util.Strings; + +@Export() +public class TransportState implements Exportable +{ + public static final String + RECEIVED = "RECEIVED", + SENT = "SENT", + DRAFT = "DRAFT", + SENDING = "SENDING", + TRASH = "TRASH", + SPAM = "SPAM", + READ = "READ"; + + public static TransportState NONE() + { + return new TransportState(); + } + + List state = new ArrayList(); + + public TransportState () + { + } + + public void mark(String flag) + { + if (flag == null) + return; + + if (state.contains(flag)) + return; + + state.add(flag.toUpperCase()); + } + + public void unmark (String flag) + { + state.remove(flag.toUpperCase()); + } + + public void mark(String flag, boolean state) + { + if (state) + mark(flag); + else + unmark(flag); + } + + public void mark(TransportState flags) + { + if (flags == null) + return; + + for (String flag : flags.state) + { + mark(flag); + } + } + + public boolean has (String flag) + { + return state.contains(flag.toUpperCase()); + } + + public boolean hasOne (TransportState state) + { + for (String flag : state.state) + { + if (has(flag)) + return true; + } + + return false; + } + + public boolean hasAll (TransportState state) + { + for (String flag : state.state) + { + if (has(flag)) + return false; + } + + return true; + } + + public boolean hasNot (String flag) + { + return !has(flag); + } + + public boolean hasNone (TransportState state) + { + return !hasOne(state); + } + + public String toString () + { + return Strings.concat(state, ","); + } + + public static TransportState fromString (String flagString) + { + TransportState state = new TransportState(); + + if (!flagString.isEmpty()) + { + String[] flags = flagString.split(","); + for (String flag : flags) + { + state.mark(flag); + } + } + return state; + } + + public static TransportState fromList (String... flags) + { + TransportState state = new TransportState(); + + for (String flag : flags) + { + state.mark(flag); + } + + return state; + } +} diff --git a/java/core/src/core/mail/client/model/UnregisteredIdentity.java b/java/core/src/core/mail/client/model/UnregisteredIdentity.java new file mode 100644 index 0000000..709a5e3 --- /dev/null +++ b/java/core/src/core/mail/client/model/UnregisteredIdentity.java @@ -0,0 +1,25 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.client.model; + +import org.timepedia.exporter.client.Export; +import org.timepedia.exporter.client.Exportable; + +@Export() +public class UnregisteredIdentity extends Identity +{ + private static final long serialVersionUID = 1L; + + public UnregisteredIdentity (String name, String email) + { + super(name, email, false); + } + + public UnregisteredIdentity (String full) + { + super(full); + } + +} diff --git a/java/core/src/core/mail/core/MimeMessageRetainMessageId.java b/java/core/src/core/mail/core/MimeMessageRetainMessageId.java new file mode 100644 index 0000000..11a18d6 --- /dev/null +++ b/java/core/src/core/mail/core/MimeMessageRetainMessageId.java @@ -0,0 +1,47 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.core; + +import java.io.InputStream; + +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.internet.MimeMessage; + +public class MimeMessageRetainMessageId extends MimeMessage +{ + String savedMessageId; + + public MimeMessageRetainMessageId (Session session, InputStream inputStream) throws MessagingException + { + super(session, inputStream); + + savedMessageId = getMessageID(); + } + + public MimeMessageRetainMessageId (Session session) throws MessagingException + { + super(session); + } + + public void setMessageID (String savedMessageId) + { + this.savedMessageId = savedMessageId; + } + + @Override + protected void updateMessageID() + { + try + { + setHeader("Message-ID", savedMessageId); + } + catch (javax.mail.MessagingException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } +} diff --git a/java/core/src/core/mail/core/SMTPAuthenticator.java b/java/core/src/core/mail/core/SMTPAuthenticator.java new file mode 100644 index 0000000..c45f97f --- /dev/null +++ b/java/core/src/core/mail/core/SMTPAuthenticator.java @@ -0,0 +1,24 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.core; + +import javax.mail.PasswordAuthentication; + +public class SMTPAuthenticator extends javax.mail.Authenticator +{ + String email; + String password; + + public SMTPAuthenticator (String email, String password) + { + this.email = email; + this.password = password; + } + + public PasswordAuthentication getPasswordAuthentication() + { + return new PasswordAuthentication(email, password); + } +} diff --git a/java/core/src/core/mail/db/MailUserDb.java b/java/core/src/core/mail/db/MailUserDb.java new file mode 100644 index 0000000..46be798 --- /dev/null +++ b/java/core/src/core/mail/db/MailUserDb.java @@ -0,0 +1,58 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.server.db; + +import java.io.IOException; +import java.sql.SQLException; + +import core.exceptions.CryptoException; +import core.crypt.PBE; +import core.server.srp.db.UserDb; +import core.server.srp.db.sql.Catalog; +import core.util.Environment; +import core.util.JSONSerializer; +import core.util.Passwords; +import core.util.Zip; + + +public class MailUserDb extends UserDb +{ + PBE cryptor; + + public MailUserDb() throws CryptoException, IOException + { + super(new Catalog()); + + String password = Passwords.getPasswordFor("mail-pbe"); + cryptor = new PBE(password, PBE.DEFAULT_SALT_0, PBE.DEFAULT_ITERATIONS, 256); + } + + public byte[] getBlock (String userName) throws IOException, SQLException, CryptoException + { + return Zip.inflate(cryptor.decrypt(super.getMailBlock(userName))); + } + + public byte[] setBlock(String userName, byte[] block) throws IOException, SQLException, CryptoException + { + return super.setMailBlock(userName, cryptor.encrypt(Zip.deflate(block))); + } + + public Environment getUserEnvironment (String user) throws IOException, SQLException, CryptoException, ClassNotFoundException + { + byte[] block = getBlock(user); + return JSONSerializer.deserialize(block); + } + + public void putUserEnvironment (String user, Environment e) throws IOException, SQLException, CryptoException + { + setBlock(user, JSONSerializer.serialize(e)); + } + + public Environment getDeletedUserEnvironment (String userName) throws Exception + { + byte[] block = Zip.inflate(cryptor.decrypt(super.getDeletedMailBlock(userName))); + return JSONSerializer.deserialize(block); + } +} diff --git a/java/core/src/core/mail/deploy/AddNullIVsToExistingUserBlocks.java b/java/core/src/core/mail/deploy/AddNullIVsToExistingUserBlocks.java new file mode 100644 index 0000000..4199f57 --- /dev/null +++ b/java/core/src/core/mail/deploy/AddNullIVsToExistingUserBlocks.java @@ -0,0 +1,95 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.server.deploy; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +import key.server.sql.KeyUserDb; +import mail.server.db.MailUserDb; +import mail.server.deploy.sql.Catalog; + +import core.crypt.CryptorAES; +import core.crypt.CryptorAESIV; +import core.server.srp.db.UserDb; +import core.util.Arrays; +import core.util.Base64; +import core.util.LogOut; + +public class AddNullIVsToExistingUserBlocks +{ + static LogOut log = new LogOut (TransferDataToBase64.class); + + public static void main (String[] args) throws Exception + { + Class.forName("com.mysql.jdbc.Driver"); + + KeyUserDb _keyDb = new KeyUserDb(); + MailUserDb _mailDb = new MailUserDb(); + PreparedStatement ps; + + Catalog catalog = new Catalog(); + + Object[][] blockTables = { {"key_block", _keyDb}, {"mail_block", _mailDb} }; + for (Object[] pair : blockTables) + { + String tableName = (String)pair[0]; + UserDb db = (UserDb)pair[1]; + + Connection conn = db.openConnection(); + + String[] sqlKeyCreateColumn = catalog.getMulti("ANIV_prepare.sql"); + for (String sql : sqlKeyCreateColumn) + { + ps = conn.prepareStatement(String.format(sql, tableName)); + log.debug(ps); + ps.execute(); + ps.close(); + } + + while (true) + { + String sqlSelectNext = String.format(catalog.getSingle("ANIV_select_next.sql"), tableName); + ps = conn.prepareStatement(sqlSelectNext); + + log.debug(ps); + ResultSet rs = ps.executeQuery(); + + if (!rs.next()) + break; + + int id = rs.getInt("user_id"); + byte[] bytes = Base64.decode(rs.getString("block")); + bytes = Arrays.concat(CryptorAES.NullIV, bytes); + + rs.close(); + ps.close(); + + String sqlSetBlock = String.format(catalog.getSingle("ANIV_set.sql"), tableName); + ps = conn.prepareStatement(sqlSetBlock); + ps.setString(1, Base64.encode(bytes)); + ps.setInt(2, id); + + log.debug(ps); + ps.executeUpdate(); + ps.close(); + } + + String[] sqlKeyFinishBlocks = catalog.getMulti("ANIV_finish.sql"); + for (String sql : sqlKeyFinishBlocks) + { + ps = conn.prepareStatement(String.format(sql, tableName)); + + log.debug(ps); + ps.execute(); + ps.close(); + } + + conn.close(); + } + + } +} diff --git a/java/core/src/core/mail/deploy/TransferDataToBase64.java b/java/core/src/core/mail/deploy/TransferDataToBase64.java new file mode 100644 index 0000000..fe324d3 --- /dev/null +++ b/java/core/src/core/mail/deploy/TransferDataToBase64.java @@ -0,0 +1,181 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.server.deploy; + +import java.io.IOException; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; + +import java.sql.PreparedStatement; + +import core.crypt.PBE; +import core.exceptions.CryptoException; +import core.server.srp.db.UserDb; +import core.util.Base64; +import core.util.LogOut; +import core.util.Passwords; +import core.util.Strings; + +import key.server.sql.KeyUserDb; +import mail.server.db.MailUserDb; +import mail.server.deploy.sql.Catalog; + +public class TransferDataToBase64 +{ + static LogOut log = new LogOut (TransferDataToBase64.class); + static PBE mailPBEold, mailPBE; + + static public void createPBEs () throws CryptoException, IOException + { + char[] PASSWORD1 = "REMOVED".toCharArray(); + char[] PASSWORD2 = "1234567890".toCharArray(); + int LENGTH = PASSWORD1.length; + + char[] password = new char[LENGTH]; + for (int i=0; i inactiveDevices = + Push.feedback( + getCertificate(), + Passwords.getPasswordFor("push-certificate"), + !ConstantsServer.DEBUG + ); + + for (Device device : inactiveDevices) + { + log.debug("should remove device ", device.getDeviceId()); + PushDispatcher.getInstance().removeDevice(device.getDeviceId()); + } + } + catch (Exception e) + { + e.printStackTrace(); + log.exception(e); + } + } + + protected void sendMessageToDevices(List recipients, int badgeNumber, String messageKey, String[] arguments) + { + log.debug ("sendMessageToDevices"); + + for (String deviceToken_ : recipients) + { + try + { + String deviceToken = deviceToken_.replace("<", "").replace(">", "").replace(" ", ""); + + PushNotificationPayload payload = PushNotificationPayload.complex(); + payload.addCustomAlertLocKey(messageKey); + payload.addCustomAlertLocArgs(Collectionz.toMutableList(arguments)); + payload.addSound("default"); + + getQueue().add(payload, new BasicDevice(deviceToken)); + } + catch (Exception e) + { + e.printStackTrace(); + log.error("Could not connect to APNs to send the notification "); + } + } + + log.debug ("~sendMessageToDevices"); + } + + public void notifyUserOfEmail (String deviceId, String author, String subject, String body) + { + List devices = new ArrayList(); + devices.add(deviceId); + + List keys = new ArrayList(); + + if (author != null) + keys.add("%@"); + + String text = null; + if (body != null || subject != null) + { + keys.add("%@"); + + final int MAX_MESSAGE_LENGTH = 128; + if (subject != null) + subject = subject.substring(0, Math.min(MAX_MESSAGE_LENGTH, subject.length())); + + int subjectLength = subject != null ? subject.length() :0; + + if (body != null) + body = body.substring(0, Math.min(MAX_MESSAGE_LENGTH - subjectLength, body.length())); + + text = Strings.concat(Collectionz.filterNull(subject, body), " * "); + } + + sendMessageToDevices( + devices, 0, + Strings.concat(keys, "\n"), + Collectionz.filterNull(author, text).toArray(new String[0]) + ); + } + + + public void shutdown () + { + try + { + Thread.sleep(5000); + } + catch (Exception e) + { + e.printStackTrace(); + } + } +} diff --git a/java/core/src/core/mail/push/PushDb.java b/java/core/src/core/mail/push/PushDb.java new file mode 100644 index 0000000..bd54b70 --- /dev/null +++ b/java/core/src/core/mail/push/PushDb.java @@ -0,0 +1,51 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.server.push; + +import java.io.IOException; +import java.sql.SQLException; + +import org.json.JSONObject; + +import core.crypt.Cryptor; +import core.crypt.CryptorRSAAES; +import core.crypt.CryptorRSAJCE; +import core.server.mailextra.MailExtraDb; +import core.util.ExternalResource; +import core.util.LogOut; +import core.util.Strings; + +public class PushDb +{ + LogOut log = new LogOut(PushDb.class); + + Cryptor cryptor; + MailExtraDb mailExtra; + + public PushDb () throws Exception + { + try + { + cryptor = new CryptorRSAAES(new CryptorRSAJCE(ExternalResource.getResourceAsStream(getClass(), "keystore.jks"), null)); + mailExtra = new MailExtraDb(); + } + catch (Exception e) + { + log.exception(e); + throw e; + } + } + + public void ensureDb () throws SQLException, IOException + { + mailExtra.ensureTables(); + } + + public void handleEncryptedBlock (byte[] block) throws Exception + { + String json = Strings.toString(cryptor.decrypt(block)); + mailExtra.handlePushNotificationsJson(new JSONObject(json)); + } +} diff --git a/java/core/src/core/mail/push/PushDispatcher.java b/java/core/src/core/mail/push/PushDispatcher.java new file mode 100644 index 0000000..9e34c81 --- /dev/null +++ b/java/core/src/core/mail/push/PushDispatcher.java @@ -0,0 +1,78 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.server.push; + +import java.io.IOException; +import java.sql.SQLException; + +import core.server.mailextra.MailExtraDb; +import core.util.LogOut; +import core.util.Pair; +import core.util.Triple; + +public class PushDispatcher +{ + static PushDispatcher instance; + public static PushDispatcher getInstance () + { + if (instance == null) + instance = new PushDispatcher(); + + return instance; + } + + static LogOut log = new LogOut(PushDispatcher.class); + MailExtraDb mailExtraDb = new MailExtraDb(); + protected ApplePushService applePushService = new ApplePushService(); + + public void notifyUserOfEmail (String email, String author, String subject, String body) throws SQLException, IOException + { + // this should go elsewherez + applePushService.queryFeedBackService(); + + Triple device = mailExtraDb.getDeviceFor(email); + + log.debug (device, email, author, subject, body); + + if (device != null && device.first != null && device.second != null && device.third != null) + { + log.debug (device.first, device.second, device.third); + + if (device.first.equals("NONE")) + { + return; + } + + if (device.first.equals("SHORT")) + { + author = null; + subject = null; + body = null; + } + + if (device.second.equals("ios")) + { + applePushService.notifyUserOfEmail(device.third, author, subject, body); + } + else + if (device.second.equals("android")) + { + + } + } + } + + public void removeDevice(String deviceId) + { + log.debug("removeDevice", deviceId); + mailExtraDb.removeDevice("ios", deviceId); + } + + public void shutdown () + { + applePushService.shutdown(); + } + +} diff --git a/java/core/src/core/mail/relay/LocalRelay.java b/java/core/src/core/mail/relay/LocalRelay.java new file mode 100644 index 0000000..ffd52ae --- /dev/null +++ b/java/core/src/core/mail/relay/LocalRelay.java @@ -0,0 +1,128 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.server.relay; + +import java.util.Date; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; + +import javax.mail.Authenticator; +import javax.mail.Message.RecipientType; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMultipart; + +import mail.core.MimeMessageRetainMessageId; +import mail.core.SMTPAuthenticator; + +import core.constants.ConstantsServer; +import core.crypt.CryptorRSAAES; +import core.crypt.CryptorRSAJCE; +import core.util.ExternalResource; +import core.util.JSONSerializer; +import core.util.LogNull; +import core.util.LogOut; + +public class LocalRelay +{ + LogOut log = new LogOut(LocalRelay.class); + CryptorRSAAES cryptor; + + public LocalRelay () throws Exception + { + cryptor = new CryptorRSAAES(new CryptorRSAJCE(ExternalResource.getResourceAsStream(getClass(), "keystore.jks"), null)); + } + + public void onMail (byte[] bytes) throws Exception + { + log.debug("LocalRelay.onMail"); + + byte[] clear = cryptor.decrypt(bytes); + log.debug("after decrypt"); + + Map map = JSONSerializer.deserialize(clear); + for (Entry e : map.entrySet()) + log.trace(e.getKey(), e.getValue()); + + String user = (String)map.get("user"); + String password = (String)map.get("password"); + + Properties props = new Properties(); + props.put("mail.smtp.user", user); + props.put("mail.smtp.host", ConstantsServer.LOCAL_SMTP_HOST); + props.put("mail.smtp.port", ConstantsServer.LOCAL_SMTP_PORT); + props.put("mail.smtp.auth", "true"); + + // props.put("mail.smtp.starttls.enable","true"); + + props.put("mail.smtp.socketFactory.port", ConstantsServer.LOCAL_SMTP_PORT); + // props.put("mail.debug", "true"); + + Authenticator auth = + new SMTPAuthenticator( + user, + password + ); + + log.debug("setting properties"); + Session session = Session.getInstance(props, auth); + MimeMessageRetainMessageId message = new MimeMessageRetainMessageId(session); + + String version = map.get("version"); + String from = map.get("from"); + String to = map.get("to"); + String cc = map.get("cc"); + String bcc = map.get("bcc"); + String subject = map.get("subject"); + String text = map.get("text"); + String html = map.get("html"); + String messageId = map.get("messageId"); + + String[] tos = to.split(","); + String[] ccs = cc.split(","); + String[] bccs = bcc.split(","); + + for (String address : tos) + message.addRecipients(RecipientType.TO, address); + + for (String address : ccs) + message.addRecipients(RecipientType.CC, address); + + for (String address : bccs) + message.addRecipients(RecipientType.BCC, address); + + message.setFrom(new InternetAddress(from)); + if (subject != null) + message.setSubject(subject); + + message.setSentDate(new Date()); + + MimeMultipart multiPart = new MimeMultipart("alternative"); + + if (text != null) + { + MimeBodyPart textPart = new MimeBodyPart(); + textPart.setContent(text, "text/plain"); + multiPart.addBodyPart(textPart); + } + + if (html != null) + { + MimeBodyPart htmlPart = new MimeBodyPart(); + htmlPart.setContent(html, "text/html"); + multiPart.addBodyPart(htmlPart); + } + + message.setContent(multiPart); + message.setMessageID(messageId); + + log.debug("Sending mail"); + Transport.send(message); + log.debug("Done sending mail"); + } +} diff --git a/java/core/src/core/mail/storage/AWSStorageCommon.java b/java/core/src/core/mail/storage/AWSStorageCommon.java new file mode 100644 index 0000000..4a1649e --- /dev/null +++ b/java/core/src/core/mail/storage/AWSStorageCommon.java @@ -0,0 +1,27 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.server.storage; + +import com.amazonaws.services.identitymanagement.model.AccessKey; + +public class AWSStorageCommon +{ + String writeIdentity, readWriteIdentity; + String bucketName, groupName, policyWriteName, policyReadWriteName; + AccessKey writeAccessKey, readWriteAccessKey; + + public void deriveNames (String bucketName) throws Exception + { + this.bucketName = bucketName; + groupName = bucketName + ".Group"; + writeIdentity = bucketName + ".Write"; + readWriteIdentity = bucketName + ".ReadWrite"; + policyWriteName = bucketName + ".Write"; + policyReadWriteName = bucketName + ".ReadWrite"; + policyWriteName = policyWriteName.replace(".", ""); + policyReadWriteName = policyReadWriteName.replace(".", ""); + } + +} diff --git a/java/core/src/core/mail/storage/AWSStorageCreation.java b/java/core/src/core/mail/storage/AWSStorageCreation.java new file mode 100644 index 0000000..5191877 --- /dev/null +++ b/java/core/src/core/mail/storage/AWSStorageCreation.java @@ -0,0 +1,220 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.server.storage; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; + + +import com.amazonaws.services.identitymanagement.AmazonIdentityManagement; +import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient; +import com.amazonaws.services.identitymanagement.model.AccessKey; +import com.amazonaws.services.identitymanagement.model.AddUserToGroupRequest; +import com.amazonaws.services.identitymanagement.model.CreateAccessKeyRequest; +import com.amazonaws.services.identitymanagement.model.CreateGroupRequest; +import com.amazonaws.services.identitymanagement.model.CreateUserRequest; +import com.amazonaws.services.identitymanagement.model.PutUserPolicyRequest; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.BucketCrossOriginConfiguration; +import com.amazonaws.services.s3.model.BucketWebsiteConfiguration; +import com.amazonaws.services.s3.model.CORSRule; +import com.amazonaws.services.s3.model.Region; +import com.sun.org.apache.xml.internal.security.utils.Base64; + +import core.connector.s3.sync.SimpleAWSCredentials; +import core.util.LogOut; +import core.util.Maps; +import core.util.Pair; +import core.util.Passwords; +import core.util.Strings; + +public class AWSStorageCreation extends AWSStorageCommon +{ + SecureRandom random = new SecureRandom(); + String bucketNameRandomId; + String awsAccessKeyId, awsSecretKey; + + LogOut log = new LogOut(AWSStorageCreation.class); + + public AWSStorageCreation () throws IOException + { + awsAccessKeyId = Passwords.getPasswordFor("BucketCreate-AWS-AccessKey"); + awsSecretKey = Passwords.getPasswordFor("BucketCreate-AWS-SecretKey"); + } + + protected String generateBucketName (String email) + { + String name = email.substring(0, email.indexOf('@')); + String domain = email.substring(email.indexOf('@')+1); + + log.debug("generateBucketName:",email,name,domain); + + String[] domainParts = domain.split("\\."); + ArrayList parts = new ArrayList(); + + // reverse, for com.mailiverse + for (int i=domainParts.length-1; i>=0; i--) + parts.add(domainParts[i]); + parts.add(name); + + parts.add("" + new BigInteger("" + Math.abs(random.nextLong())).toString(16)); + + return Strings.concat(parts, ".").toLowerCase(); + } + + public Map create (String email, String region) throws Exception + { + log.debug ("I will now figure out what region to put things in", region); + Region awsRegion = Region.valueOf(region); + String awsRegionString = awsRegion.toString(); + if (awsRegionString == null) + awsRegionString = ""; + + String awsRegionStringEndPoint = + awsRegionString.isEmpty() ? "s3.amazonaws.com" : ("s3-" + awsRegionString + ".amazonaws.com"); + + log.debug ("I will now log in to S3 and the IdentityManagement to check these credentials."); + + SimpleAWSCredentials credentials = new SimpleAWSCredentials(awsAccessKeyId, awsSecretKey); + AmazonS3 s3 = new AmazonS3Client(credentials); + AmazonIdentityManagement im = new AmazonIdentityManagementClient(credentials); + + log.debug ("Successfully logged into S3"); + + log.debug ("I will now derive names for items"); + deriveNames(generateBucketName(email)); + + log.debug ( + "I will now try to:\n" + + " 1. Create the S3 Bucket with name ",bucketName,"\n" + + " 2. Create two IAM Identities for permissions -\n" + + " ",writeIdentity," to be sent to the mail server to be able to write to the mailbox.\n" + + " ",writeIdentity," to be stored in your configuration to enable the mail client to read and write mail.\n\n" + ); + + + s3.setEndpoint(awsRegionStringEndPoint); + s3.createBucket(bucketName, awsRegion); + + log.debug("Setting website configuration"); + + BucketWebsiteConfiguration bwc = new BucketWebsiteConfiguration("index.html"); + s3.setBucketWebsiteConfiguration(bucketName, bwc); + log.debug("Done"); + + log.debug("Enabling CORS"); + CORSRule rule1 = new CORSRule() + .withId("CORSRule1") + .withAllowedMethods(Arrays.asList(new CORSRule.AllowedMethods[] { + CORSRule.AllowedMethods.GET, CORSRule.AllowedMethods.PUT, CORSRule.AllowedMethods.DELETE})) + .withAllowedOrigins(Arrays.asList(new String[] {"*"})) + .withMaxAgeSeconds(3000) + .withAllowedHeaders(Arrays.asList(new String[] {"*"})) + .withExposedHeaders(Arrays.asList(new String[] {"ETag"})); + + BucketCrossOriginConfiguration cors = new BucketCrossOriginConfiguration(); + cors.setRules(Arrays.asList(new CORSRule[] {rule1})); + + s3.setBucketCrossOriginConfiguration(bucketName, cors); + log.debug("Done"); + + log.format("Creating group %s ... ", groupName); + im.createGroup(new CreateGroupRequest().withGroupName(groupName)); + log.debug("Done"); + + log.format("Creating user %s ... ", writeIdentity); + im.createUser(new CreateUserRequest().withUserName(writeIdentity)); + log.debug("Done"); + + log.format("Adding user %s to group %s ... ", writeIdentity, groupName); + im.addUserToGroup(new AddUserToGroupRequest().withGroupName(groupName).withUserName(writeIdentity)); + log.debug("Done"); + + log.format("Creating user %s ... ", readWriteIdentity); + im.createUser(new CreateUserRequest().withUserName(readWriteIdentity)); + log.debug("Done"); + + log.format("Adding user %s to group %s ... ", readWriteIdentity, groupName); + im.addUserToGroup(new AddUserToGroupRequest().withGroupName(groupName).withUserName(readWriteIdentity)); + log.debug("Done"); + + log.format("Creating permissions for %s to write to bucket %s ... \n", writeIdentity, bucketName); + + String writePolicyRaw = + "{ \n"+ + " #Statement#: [ \n"+ + " { \n"+ + " #Sid#: #SID#, \n"+ + " #Action#: [ \n"+ + " #s3:PutObject#, \n"+ + " #s3:PutObjectAcl# \n"+ + " ], \n"+ + " #Effect#: #Allow#, \n"+ + " #Resource#: [ \n"+ + " #arn:aws:s3:::BUCKET/*#\n"+ + " ] \n"+ + " } \n"+ + " ] \n"+ + "}\n"; + + String writePolicy = writePolicyRaw.replaceAll("#", "\"").replace("SID", policyWriteName).replace("BUCKET", bucketName); +// q.println ("Policy definition: " + writePolicy); + im.putUserPolicy(new PutUserPolicyRequest().withUserName(writeIdentity).withPolicyDocument(writePolicy).withPolicyName(policyWriteName)); + log.debug("Done"); + + log.format("Creating permissions for %s to read/write to bucket %s ... \n", writeIdentity, bucketName); + + String readWritePolicyRaw = + "{ \n"+ + " #Statement#: [ \n"+ + " { \n"+ + " #Sid#: #SID#, \n"+ + " #Action#: [ \n"+ + " #s3:PutObject#, \n"+ + " #s3:PutObjectAcl#, \n"+ + " #s3:DeleteObject#, \n"+ + " #s3:Get*#, \n"+ + " #s3:List*# \n"+ + " ], \n"+ + " #Effect#: #Allow#, \n"+ + " #Resource#: [ \n"+ + " #arn:aws:s3:::BUCKET/*#,\n"+ + " #arn:aws:s3:::BUCKET# \n"+ + " ] \n"+ + " } \n"+ + " ] \n"+ + "}\n"; + + String readWritePolicy = readWritePolicyRaw.replaceAll("#", "\"").replace("SID", policyReadWriteName).replace("BUCKET", bucketName); +// q.println ("Policy definition: " + readPolicy); + im.putUserPolicy(new PutUserPolicyRequest().withUserName(readWriteIdentity).withPolicyDocument(readWritePolicy).withPolicyName(policyReadWriteName)); + log.debug("Done"); + + log.format("Requesting access key for %s", writeIdentity); + writeAccessKey = im.createAccessKey(new CreateAccessKeyRequest().withUserName(writeIdentity)).getAccessKey(); + log.format("Received [%s] [%s] Done.\n", writeAccessKey.getAccessKeyId(), writeAccessKey.getSecretAccessKey()); + + log.format("Requesting access key for %s", readWriteIdentity); + readWriteAccessKey = im.createAccessKey(new CreateAccessKeyRequest().withUserName(readWriteIdentity)).getAccessKey(); + log.format("Received [%s] [%s] Done.\n", readWriteAccessKey.getAccessKeyId(), readWriteAccessKey.getSecretAccessKey()); + + log.debug(); + log.debug("I have finished the creating the S3 items.\n"); + + return Maps.toMap( + "bucketName", bucketName, + "bucketRegion", awsRegionString, + "writeAccessKey", writeAccessKey.getAccessKeyId(), + "writeSecretKey", writeAccessKey.getSecretAccessKey(), + "readWriteAccessKey", readWriteAccessKey.getAccessKeyId(), + "readWriteSecretKey", readWriteAccessKey.getSecretAccessKey() + ); + } +} diff --git a/java/core/src/core/mail/storage/AWSStorageDelete.java b/java/core/src/core/mail/storage/AWSStorageDelete.java new file mode 100644 index 0000000..db700f5 --- /dev/null +++ b/java/core/src/core/mail/storage/AWSStorageDelete.java @@ -0,0 +1,155 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.server.storage; + +import java.io.IOException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; + + +import com.amazonaws.services.identitymanagement.AmazonIdentityManagement; +import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient; +import com.amazonaws.services.identitymanagement.model.AccessKeyMetadata; +import com.amazonaws.services.identitymanagement.model.DeleteAccessKeyRequest; +import com.amazonaws.services.identitymanagement.model.DeleteGroupRequest; +import com.amazonaws.services.identitymanagement.model.DeleteUserPolicyRequest; +import com.amazonaws.services.identitymanagement.model.DeleteUserRequest; +import com.amazonaws.services.identitymanagement.model.ListAccessKeysRequest; +import com.amazonaws.services.identitymanagement.model.ListAccessKeysResult; +import com.amazonaws.services.identitymanagement.model.RemoveUserFromGroupRequest; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.DeleteBucketRequest; +import com.amazonaws.services.s3.model.DeleteObjectsRequest; +import com.amazonaws.services.s3.model.ObjectListing; +import com.amazonaws.services.s3.model.S3ObjectSummary; + +import core.connector.s3.sync.SimpleAWSCredentials; +import core.util.LogOut; +import core.util.Passwords; + +public class AWSStorageDelete extends AWSStorageCommon +{ + static LogOut log = new LogOut(AWSStorageDelete.class); + + public AWSStorageDelete () throws IOException + { + } + + protected void deleteUser(AmazonIdentityManagement im, String group, String user, String policy, boolean force) throws Exception + { + try + { + log.debug("deleting user policy", user, policy); + im.deleteUserPolicy(new DeleteUserPolicyRequest().withUserName(user).withPolicyName(policy)); + } + catch (Exception e) + { + if (!force) + throw e; + + log.exception(e); + } + + try + { + log.debug("deleting access keys", user); + ListAccessKeysResult accessKeys = im.listAccessKeys(new ListAccessKeysRequest().withUserName(user)); + for (AccessKeyMetadata i : accessKeys.getAccessKeyMetadata()) + { + log.debug("deleting access key", user, i.getAccessKeyId()); + im.deleteAccessKey(new DeleteAccessKeyRequest().withUserName(user).withAccessKeyId(i.getAccessKeyId())); + } + } + catch (Exception e) + { + if (!force) + throw e; + + log.exception(e); + } + + try + { + log.debug("removing user from group", group, user); + im.removeUserFromGroup(new RemoveUserFromGroupRequest().withGroupName(group).withUserName(user)); + } + catch (Exception e) + { + if (!force) + throw e; + + log.exception(e); + } + + try + { + log.debug("deleting user", user); + im.deleteUser(new DeleteUserRequest().withUserName(user)); + } + catch (Exception e) + { + if (!force) + throw e; + + log.exception(e); + } + } + + protected void deleteBucketContents(AmazonS3 s3, String bucketName) throws Exception + { + while (true) + { + List keys = new ArrayList(); + + log.debug("creating batch delete"); + + ObjectListing listing = s3.listObjects(bucketName); + for (S3ObjectSummary i : listing.getObjectSummaries()) + { + log.debug("key", i.getKey()); + keys.add(i.getKey()); + } + + if (keys.isEmpty()) + break; + + DeleteObjectsRequest req = new DeleteObjectsRequest(bucketName).withKeys(keys.toArray(new String[0])); + log.debug("deleting"); + s3.deleteObjects(req); + } + } + + public void delete(String bucketName) throws Exception + { + String awsAccessKeyId = Passwords.getPasswordFor("BucketCreate-AWS-AccessKey"); + String awsSecretKey = Passwords.getPasswordFor("BucketCreate-AWS-SecretKey"); + + delete (bucketName, awsAccessKeyId, awsSecretKey); + } + + public void delete(String bucketName, String awsAccessKeyId, String awsSecretKey) throws Exception + { + log.debug("will delete", bucketName); + SimpleAWSCredentials credentials = new SimpleAWSCredentials(awsAccessKeyId, awsSecretKey); + AmazonS3 s3 = new AmazonS3Client(credentials); + AmazonIdentityManagement im = new AmazonIdentityManagementClient(credentials); + + log.debug("deriving names"); + deriveNames(bucketName); + + deleteUser(im, groupName, readWriteIdentity, policyReadWriteName, false); + deleteUser(im, groupName, writeIdentity, policyWriteName, false); + + log.debug("deleting group", groupName); + im.deleteGroup(new DeleteGroupRequest().withGroupName(groupName)); + + deleteBucketContents(s3, bucketName); + + log.debug("deleting bucket"); + s3.deleteBucket(new DeleteBucketRequest(bucketName)); + } +} diff --git a/java/core/src/core/mail/streamserver/BogusSslContextFactory.java b/java/core/src/core/mail/streamserver/BogusSslContextFactory.java new file mode 100644 index 0000000..03c5db9 --- /dev/null +++ b/java/core/src/core/mail/streamserver/BogusSslContextFactory.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package mail.streamserver; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.Security; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; + +/** + * Factory to create a bogus SSLContext. + * + * @author Apache MINA Project + */ +public class BogusSslContextFactory { + + /** + * Protocol to use. + */ + private static final String PROTOCOL = "TLS"; + + private static final String KEY_MANAGER_FACTORY_ALGORITHM; + + static { + String algorithm = Security + .getProperty("ssl.KeyManagerFactory.algorithm"); + if (algorithm == null) { + algorithm = KeyManagerFactory.getDefaultAlgorithm(); + } + + KEY_MANAGER_FACTORY_ALGORITHM = algorithm; + } + + /** + * Bougus Server certificate keystore file name. + */ + private static final String BOGUS_KEYSTORE = "bogus.cert"; + + // NOTE: The keystore was generated using keytool: + // keytool -genkey -alias bogus -keysize 512 -validity 3650 + // -keyalg RSA -dname "CN=bogus.com, OU=XXX CA, + // O=Bogus Inc, L=Stockholm, S=Stockholm, C=SE" + // -keypass boguspw -storepass boguspw -keystore bogus.cert + + /** + * Bougus keystore password. + */ + private static final char[] BOGUS_PW = { 'b', 'o', 'g', 'u', 's', 'p', 'w' }; + + private static SSLContext serverInstance = null; + + private static SSLContext clientInstance = null; + + /** + * Get SSLContext singleton. + * + * @return SSLContext + * @throws java.security.GeneralSecurityException + * + */ + public static SSLContext getInstance(boolean server) + throws GeneralSecurityException { + SSLContext retInstance = null; + if (server) { + synchronized(BogusSslContextFactory.class) { + if (serverInstance == null) { + try { + serverInstance = createBougusServerSslContext(); + } catch (Exception ioe) { + throw new GeneralSecurityException( + "Can't create Server SSLContext:" + ioe); + } + } + } + retInstance = serverInstance; + } else { + synchronized (BogusSslContextFactory.class) { + if (clientInstance == null) { + clientInstance = createBougusClientSslContext(); + } + } + retInstance = clientInstance; + } + return retInstance; + } + + private static SSLContext createBougusServerSslContext() + throws GeneralSecurityException, IOException { + // Create keystore + KeyStore ks = KeyStore.getInstance("JKS"); + InputStream in = null; + try { + in = BogusSslContextFactory.class + .getResourceAsStream(BOGUS_KEYSTORE); + ks.load(in, BOGUS_PW); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ignored) { + } + } + } + + // Set up key manager factory to use our key store + KeyManagerFactory kmf = KeyManagerFactory + .getInstance(KEY_MANAGER_FACTORY_ALGORITHM); + kmf.init(ks, BOGUS_PW); + + // Initialize the SSLContext to work with our key managers. + SSLContext sslContext = SSLContext.getInstance(PROTOCOL); + sslContext.init(kmf.getKeyManagers(), + BogusTrustManagerFactory.X509_MANAGERS, null); + + return sslContext; + } + + private static SSLContext createBougusClientSslContext() + throws GeneralSecurityException { + SSLContext context = SSLContext.getInstance(PROTOCOL); + context.init(null, BogusTrustManagerFactory.X509_MANAGERS, null); + return context; + } + +} diff --git a/java/core/src/core/mail/streamserver/BogusTrustManagerFactory.java b/java/core/src/core/mail/streamserver/BogusTrustManagerFactory.java new file mode 100644 index 0000000..7d420ea --- /dev/null +++ b/java/core/src/core/mail/streamserver/BogusTrustManagerFactory.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package mail.streamserver; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactorySpi; +import javax.net.ssl.X509TrustManager; + +/** + * Bogus trust manager factory. Creates BogusX509TrustManager + * + * @author Apache MINA Project + */ +class BogusTrustManagerFactory extends TrustManagerFactorySpi { + + static final X509TrustManager X509 = new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] x509Certificates, + String s) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] x509Certificates, + String s) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + + static final TrustManager[] X509_MANAGERS = new TrustManager[] { X509 }; + + public BogusTrustManagerFactory() { + } + + @Override + protected TrustManager[] engineGetTrustManagers() { + return X509_MANAGERS; + } + + @Override + protected void engineInit(KeyStore keystore) throws KeyStoreException { + // noop + } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) + throws InvalidAlgorithmParameterException { + // noop + } +} diff --git a/java/core/src/core/mail/streamserver/MailServerSessionDb.java b/java/core/src/core/mail/streamserver/MailServerSessionDb.java new file mode 100644 index 0000000..57a0974 --- /dev/null +++ b/java/core/src/core/mail/streamserver/MailServerSessionDb.java @@ -0,0 +1,176 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.streamserver; + +import java.math.BigInteger; +import java.util.Random; + +import mail.server.db.MailUserDb; + +import org.apache.james.cli.probe.impl.JmxServerProbe; + +import core.constants.ConstantsEnvironmentKeys; +import core.exceptions.InternalException; +import core.exceptions.PublicMessageException; +import core.server.captcha.Captcha; +import core.server.mailextra.MailExtraDb; +import core.srp.server.SRPServerUserSessionDb; +import core.util.Environment; +import core.util.JSONSerializer; +import core.util.LogOut; +import core.util.SimpleSerializer; +import core.util.Strings; +import core.util.Triple; + + +public class MailServerSessionDb implements SRPServerUserSessionDb +{ + static final boolean USE_CAPTCHA = true; + + + static LogOut log = new LogOut(MailServerSessionDb.class); + MailUserDb db; + MailExtraDb payment; + Captcha captcha; + + public MailServerSessionDb (MailUserDb db) + { + this.db = db; + this.captcha = new Captcha(); + this.payment = new MailExtraDb(); + } + + public void setBlock (String userName, byte[] block) throws Exception + { + log.debug("setBlock", userName, Strings.toString(block)); + Environment e = JSONSerializer.deserialize(block); + + String newPassword = e.get(ConstantsEnvironmentKeys.SMTP_PASSWORD); + + JmxServerProbe jamesConnection = new JmxServerProbe("localhost"); + if (newPassword != null) + jamesConnection.setPassword(userName, newPassword); + + db.setBlock(userName, block); + } + + public byte[] getBlock (String userName) throws Exception + { + log.debug("getBlock", userName); + return db.getBlock(userName); + } + + @Override + public Triple getUserVVS(String userName) throws Exception + { + return db.getVVS(userName); + } + + @Override + public void createUser(String version, String userName, BigInteger v, BigInteger s, byte[] extra) throws Exception + { + log.debug("createUser", version, userName); + try + { + JmxServerProbe jamesConnection = new JmxServerProbe("localhost"); + + if (USE_CAPTCHA) + { + String token = SimpleSerializer.deserialize(extra); + captcha.useToken(token, Captcha.SignUp); + } + else + System.out.println("Not checking captcha!"); + + db.createUser(version, userName, v.toByteArray(), s.toByteArray()); + payment.addDaysTo(userName,0); + + Random random = new Random(); + String randomLong = BigInteger.valueOf(Math.abs(random.nextLong())).toString(32); + + jamesConnection.addUser(userName, randomLong); + } + catch (PublicMessageException e) + { + log.debug("create user caught", e); + throw e; + } + catch (Exception e) + { + log.debug("create user caught", e); + throw new InternalException(e); + } + } + + public void deleteUser(String userName) + { + log.debug("deleteUser", userName); + try + { + JmxServerProbe jamesConnection = new JmxServerProbe("localhost"); + + db.deleteUser(userName); + jamesConnection.removeUser(userName); + } + catch (PublicMessageException e) + { + log.debug("create user caught", e); + throw e; + } + catch (Exception e) + { + log.debug("create user caught", e); + throw new InternalException(e); + } + } + + @Override + public void rateLimitFailure(String userName) throws PublicMessageException + { + try + { + // db.rateLimitFailure(userName); + } + catch (PublicMessageException e) + { + throw e; + } + catch (Exception e) + { + throw new InternalException(e); + } + } + + @Override + public void markFailure(String userName) throws PublicMessageException + { + log.debug("markFailure", userName); + try + { + db.markFailure(userName); + } + catch (PublicMessageException e) + { + throw e; + } + catch (Exception e) + { + throw new InternalException(e); + } + } + + public void checkRoomForNewUser() throws Exception + { + log.debug("checkRoomForNewUser"); + db.checkRoomForNewUser(); + } + + @Override + public void testCreate(String version, String userName) throws Exception + { + log.debug("testCreate", userName); + db.testCreateUser(version, userName); + } +} diff --git a/java/core/src/core/mail/streamserver/MailServerUserSession.java b/java/core/src/core/mail/streamserver/MailServerUserSession.java new file mode 100644 index 0000000..eda2973 --- /dev/null +++ b/java/core/src/core/mail/streamserver/MailServerUserSession.java @@ -0,0 +1,74 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.streamserver; + +import org.json.JSONObject; + +import core.client.messages.Delete; +import core.client.messages.Get; +import core.client.messages.Message; +import core.client.messages.Put; +import core.client.messages.Response; +import core.constants.ConstantsClient; +import core.constants.ConstantsPushNotifications; +import core.io.IoChain; +import core.server.mailextra.MailExtraDb; +import core.srp.server.SRPServerUserSession; +import core.util.Block; +import core.util.SimpleSerializer; + + +public class MailServerUserSession extends IoChain +{ + String userName; + MailServerSessionDb db; + + public MailServerUserSession (MailServerSessionDb db, SRPServerUserSession sender) + { + super(sender); + + this.db = db; + } + + @Override + public void open () + { + userName = ((SRPServerUserSession)sender).getUserName(); + } + + public void doDelete () throws Exception + { + db.deleteUser(userName); + send(SimpleSerializer.serialize(new Response())); + } + + @Override + public void onReceive (byte[] bytes) throws Exception + { + Message message = SimpleSerializer.deserialize(bytes); + if (message instanceof Delete) + { + doDelete(); + } + else + if (message instanceof Put) + { + db.setBlock(userName, ((Put)message).getBlock()); + send(SimpleSerializer.serialize(new Response(db.getBlock(userName)))); + } + else + if (message instanceof Get) + { + send(SimpleSerializer.serialize(new Response(db.getBlock(userName)))); + } + else + throw new Exception ("Unknown message type"); + } + + public void checkRoomForNewUser() throws Exception + { + db.checkRoomForNewUser(); + } +} diff --git a/java/core/src/core/mail/streamserver/MailStreamServerMain.java b/java/core/src/core/mail/streamserver/MailStreamServerMain.java new file mode 100644 index 0000000..c849731 --- /dev/null +++ b/java/core/src/core/mail/streamserver/MailStreamServerMain.java @@ -0,0 +1,52 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.streamserver; + +import java.net.InetSocketAddress; +import java.nio.charset.Charset; + +import mail.server.db.MailUserDb; + +import org.apache.mina.filter.codec.ProtocolCodecFilter; +import org.apache.mina.filter.codec.textline.TextLineCodecFactory; +import org.apache.mina.filter.logging.LoggingFilter; +import org.apache.mina.transport.socket.nio.NioSocketAcceptor; +import core.constants.ConstantsServer; + + +/** + * (Entry point) NetCat client. NetCat client connects to the specified + * endpoint and prints out received data. NetCat client disconnects + * automatically when no data is read for 10 seconds. + * + * @author Apache MINA Project + */ +public class MailStreamServerMain +{ + public static void main(String[] args) throws Exception + { + Class.forName("com.mysql.jdbc.Driver"); + MailUserDb userDb = new MailUserDb(); + userDb.ensureTables(); + + // Create TCP/IP connector. + NioSocketAcceptor acceptor = new NioSocketAcceptor(); + + // Start communication. + acceptor.getFilterChain().addLast("logger", new LoggingFilter()); + TextLineCodecFactory textLineCodec = new TextLineCodecFactory(Charset.forName("UTF-8")); + textLineCodec.setDecoderMaxLineLength(100 * 1000); + textLineCodec.setEncoderMaxLineLength(100 * 1000); + acceptor.getFilterChain().addLast( + "codec", + new ProtocolCodecFilter(textLineCodec) + ); + + acceptor.setHandler(new SRPProtocolHandler()); + acceptor.bind(new InetSocketAddress(ConstantsServer.MAIL_AUTH_PORT)); + + System.out.println("Listening on port " + ConstantsServer.MAIL_AUTH_PORT); + } +} diff --git a/java/core/src/core/mail/streamserver/SRPProtocolHandler.java b/java/core/src/core/mail/streamserver/SRPProtocolHandler.java new file mode 100644 index 0000000..1f83658 --- /dev/null +++ b/java/core/src/core/mail/streamserver/SRPProtocolHandler.java @@ -0,0 +1,150 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.streamserver; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import mail.server.db.MailUserDb; + +import org.apache.mina.core.buffer.IoBuffer; +import org.apache.mina.core.service.IoHandler; +import org.apache.mina.core.service.IoHandlerAdapter; +import org.apache.mina.core.session.IdleStatus; +import org.apache.mina.core.session.IoSession; + +import core.constants.ConstantsServer; +import core.crypt.CryptorRSAAES; +import core.crypt.CryptorRSAJCE; +import core.io.IoChainBase64; +import core.io.IoChainFinishedException; +import core.io.IoChainNewLinePackets; +import core.io.IoChainAccumulator; +import core.srp.server.SRPServerUserSession; +import core.util.ExternalResource; +import core.util.LogOut; + + +/** + * {@link IoHandler} implementation for NetCat client. This class extended + * {@link IoHandlerAdapter} for convenience. + * + * @author Apache MINA Project + */ +public class SRPProtocolHandler extends IoHandlerAdapter +{ + static LogOut log = new LogOut(SRPProtocolHandler.class); + static final int TIMEOUT_SECONDS = ConstantsServer.AUTH_TIMEOUT; + + MailUserDb db; + Map sessions = new HashMap(); + CryptorRSAAES cryptorRSA; + + public SRPProtocolHandler () throws Exception + { + db = new MailUserDb(); + cryptorRSA = new CryptorRSAAES(new CryptorRSAJCE(ExternalResource.getResourceAsStream(getClass(), "keystore.jks"), null)); + } + + @Override + public void exceptionCaught(IoSession session, Throwable cause) + { + log.debug("exceptionCaught", session, cause); + session.close(true); + } + + @Override + public void sessionOpened(IoSession session) + { + log.debug("sessionOpened", session); + + // Set reader idle time to 10 seconds. + // sessionIdle(...) method will be invoked when no data is read + // for 10 seconds. + session.getConfig().setIdleTime(IdleStatus.READER_IDLE, TIMEOUT_SECONDS); + + try + { + MailServerSessionDb sessionDb = new MailServerSessionDb(db); + + MailServerUserSession userSession = + new MailServerUserSession(sessionDb, + new SRPServerUserSession( + cryptorRSA, sessionDb, + new IoChainBase64( + new IoChainNewLinePackets( + new IoChainAccumulator() + ) + ) + ) + ); + + userSession.run(); + sessions.put(session, userSession); + } + catch (Exception e) + { + log.debug("sessionOpened caught", e); + session.close(true); + } + } + + @Override + public void sessionClosed(IoSession session) + { + log.debug("sessionClosed", session, "Total", session.getReadBytes(), "byte(s)"); + sessions.remove(session); + } + + @Override + public void sessionIdle(IoSession session, IdleStatus status) { + // Close the connection if reader is idle. + if (status == IdleStatus.READER_IDLE) { + session.close(true); + } + } + + public void write (IoSession session, byte[] bytes) + { + IoBuffer out = IoBuffer.allocate(bytes.length); + out.setAutoExpand(true); + out.put (bytes); + out.flip(); + + session.write(out); + } + + @Override + public void messageReceived(IoSession session, Object message) { + log.debug("messageReceived"); + + try + { + IoChainAccumulator userSession = + (IoChainAccumulator)sessions.get(session).getFinalSender(); + + String str = message.toString() + "\n"; + userSession.receive(str.getBytes()); + + List packets = userSession.getAndClearPackets(); + for(byte[] packet: packets) + write(session, packet); + + Exception e = userSession.getAndClearException(); + if (e != null) + throw e; + + if (userSession.isClosed()) + throw new IoChainFinishedException(); + } + catch (Exception e) + { + log.debug ("messageReceived caught", e); + e.printStackTrace(); + session.close(true); + } + } +} \ No newline at end of file diff --git a/java/core/src/core/mail/streamserver/SSLContextGenerator.java b/java/core/src/core/mail/streamserver/SSLContextGenerator.java new file mode 100644 index 0000000..be6f708 --- /dev/null +++ b/java/core/src/core/mail/streamserver/SSLContextGenerator.java @@ -0,0 +1,42 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.streamserver; + +import java.io.File; +import java.security.KeyStore; +import javax.net.ssl.SSLContext; +import org.apache.mina.filter.ssl.KeyStoreFactory; +import org.apache.mina.filter.ssl.SslContextFactory; + +import core.util.Streams; + + +public class SSLContextGenerator +{ + public SSLContext getSslContext() throws Exception + { + SSLContext sslContext = null; + + final KeyStoreFactory keyStoreFactory = new KeyStoreFactory(); + keyStoreFactory.setData(Streams.readFullyBytes(this.getClass().getResourceAsStream("keystore.jks"))); + keyStoreFactory.setPassword("password"); + + final KeyStoreFactory trustStoreFactory = new KeyStoreFactory(); + trustStoreFactory.setData(Streams.readFullyBytes(this.getClass().getResourceAsStream("truststore.jks"))); + trustStoreFactory.setPassword("password"); + + final SslContextFactory sslContextFactory = new SslContextFactory(); + final KeyStore keyStore = keyStoreFactory.newInstance(); + sslContextFactory.setKeyManagerFactoryKeyStore(keyStore); + + final KeyStore trustStore = trustStoreFactory.newInstance(); + sslContextFactory.setTrustManagerFactoryKeyStore(trustStore); + sslContextFactory.setKeyManagerFactoryKeyStorePassword("password"); + sslContext = sslContextFactory.newInstance(); + System.out.println("SSL provider is: " + sslContext.getProvider()); + + return sslContext; + } +} \ No newline at end of file diff --git a/java/core/src/core/mail/util/JavaMailToJSON.java b/java/core/src/core/mail/util/JavaMailToJSON.java new file mode 100644 index 0000000..eb7aa25 --- /dev/null +++ b/java/core/src/core/mail/util/JavaMailToJSON.java @@ -0,0 +1,290 @@ +/** + * Author: Timothy Prepscius + * License: GPLv3 Affero + keep my name in the code! + */ +package mail.server.util; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Date; +import java.util.Enumeration; +import java.util.Properties; + +import javax.mail.Address; +import javax.mail.BodyPart; +import javax.mail.Header; +import javax.mail.Message; +import javax.mail.Part; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import javax.mail.internet.MimeUtility; + +import org.json.JSONArray; +import org.json.JSONObject; + +import core.constants.ConstantsMailJson; +import core.util.Base64; +import core.util.LogNull; +import core.util.Pair; +import core.util.Streams; + +public class JavaMailToJSON +{ + public static class MailDescription + { + public String author; + public String subject; + public String body; + } ; + + static LogNull log = new LogNull(JavaMailToJSON.class); + + public JavaMailToJSON() + { + + } + + public String portion(String s) + { + int end = s.indexOf('\n'); + if (end == -1) + end = s.length(); + + return s.substring(0, Math.min(end, 32)); + } + + public String encode(byte[] b) + { + return new String(Base64.encode(b)); + } + + public String encode(String s) throws UnsupportedEncodingException + { + return encode(s.getBytes("UTF-8")); + } + + public JSONArray parseHeaders (Enumeration e) throws Exception + { + JSONArray headers = new JSONArray(); + while (e.hasMoreElements()) + { + Header h = (Header)e.nextElement(); + JSONArray jh = new JSONArray(); + + String k = MimeUtility.decodeText(h.getName()); + String v = MimeUtility.decodeText(h.getValue()); + log.debug("header",k,v); + jh.put(k); + jh.put(encode(v)); + + headers.put(jh); + } + + return headers; + } + + public JSONObject parseDates (MimeMessage message) throws Exception + { + Date receivedDate = message.getReceivedDate(); + Date sentDate = message.getSentDate(); + + JSONObject dates = new JSONObject(); + if (receivedDate!=null) + dates.put(ConstantsMailJson.Received, "" + receivedDate.getTime()); + if (sentDate!=null) + dates.put(ConstantsMailJson.Sent, "" + sentDate.getTime()); + + return dates; + } + + public JSONObject parseAddresses (MimeMessage message, MailDescription description) throws Exception + { + JSONObject ja = new JSONObject(); + + ArrayList> ta = new ArrayList>(); + + Message.RecipientType[] recipientTypes = { Message.RecipientType.TO, Message.RecipientType.CC, Message.RecipientType.BCC }; + for (Message.RecipientType r : recipientTypes) + { + try + { + Address[] as = message.getRecipients(r); + ta.add(new Pair(r.toString().toLowerCase(), as)); + } + catch (Exception e) + { + log.error("Failed to read addresses for " + r.toString()); + } + } + + try + { + ta.add(new Pair(ConstantsMailJson.ReplyTo, message.getReplyTo())); + } + catch (Exception e) + { + log.error("Failed to read addresses for reply-to"); + } + + try + { + ta.add(new Pair(ConstantsMailJson.From, message.getFrom() )); + } + catch (Exception e) + { + log.error("Failed to read addresses for from"); + } + + for (Pair tas : ta) + { + if (tas.second != null) + { + JSONArray jas = new JSONArray(); + for (Address a : tas.second) + { + InternetAddress ia = (InternetAddress)a; + + if (ia != null) + { + JSONObject jia = new JSONObject(); + String name = ia.getPersonal(); + String email = ia.getAddress(); + + if (tas.first.equals(ConstantsMailJson.From)) + description.author = name; + + if (name != null) + jia.put(ConstantsMailJson.Name, encode(MimeUtility.decodeText(name))); + + if (email != null) + jia.put(ConstantsMailJson.Email, encode(MimeUtility.decodeText(email))); + + jas.put(jia); + } + } + + ja.put(tas.first, jas); + } + } + + return ja; + } + + public JSONObject parseContent (Part message, MailDescription description) throws Exception + { + log.debug("parseContent"); + + JSONObject c = new JSONObject(); + c.put(ConstantsMailJson.Headers, parseHeaders(message.getAllHeaders())); + + Object jValue = null; + String jType = ConstantsMailJson.Unknown; + + Object content = message.getContent(); + if (content instanceof String) + { + String v = (String)content; + log.debug("content", "string", portion(v)); + + jType = ConstantsMailJson.String; + jValue = v; + } + else + if (content instanceof MimeMultipart) + { + log.debug("content", "multipart"); + + MimeMultipart m = (MimeMultipart)content; + + JSONArray parts = new JSONArray(); + + for (int i=0; i