diff --git a/CMakeLists.txt b/CMakeLists.txt index c2054bc..f71efdb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,18 +1,22 @@ # Global build file for Bongo -project(bongo C) +cmake_minimum_required(VERSION 3.10) +project(bongo VERSION 0.7.0 LANGUAGES C) +if(POLICY CMP0177) + cmake_policy(SET CMP0177 NEW) +endif() +include(CTest) # set version of cmake required, and module path -cmake_minimum_required(VERSION 2.6) -cmake_policy(VERSION 2.6) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/") -set(BONGO_V_MAJOR 0) -set(BONGO_V_MINOR 1) -set(BONGO_V_BUILD 0) -set(BONGO_V_STR "${BONGO_V_MAJOR}.${BONGO_V_MINOR}.${BONGO_V_BUILD}") +set(BONGO_V_MAJOR "${PROJECT_VERSION_MAJOR}") +set(BONGO_V_MINOR "${PROJECT_VERSION_MINOR}") +set(BONGO_V_BUILD "${PROJECT_VERSION_PATCH}") +set(BONGO_V_PATCH "${PROJECT_VERSION_PATCH}") +set(BONGO_V_STR "${PROJECT_VERSION}") # arguments we can supply to the build system - TODO -set(BONGO_USER "root" CACHE STRING "User account Bongo should run under") +set(BONGO_USER "bongo" CACHE STRING "User account Bongo should run under") option(CONN_TRACE "Enable connection tracing" OFF) include(cmake/BongoCompiler.cmake) @@ -42,9 +46,13 @@ include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}/include/) # now build our various libraries, agents, binaries. # DO NOT ALTER (SO)VERSION ON LIBRARIES WITHOUT ASKING! -foreach (LIBRARY xpl streamio msgapi connio util json cal) +foreach (LIBRARY xpl streamio msgapi connio util json cal mailauth sieve sasl) add_subdirectory (src/libs/${LIBRARY}) - set_target_properties(bongo${LIBRARY} PROPERTIES + set(LIBRARY_TARGET bongo${LIBRARY}) + if(LIBRARY STREQUAL "sieve") + set(LIBRARY_TARGET libbongosieve) + endif() + set_target_properties(${LIBRARY_TARGET} PROPERTIES SOVERSION 0 VERSION ${BONGO_V_STR} ) @@ -53,7 +61,7 @@ endforeach (LIBRARY) add_subdirectory(src/libs/python/libbongo) add_subdirectory(src/libs/python/bongo) -foreach (AGENT antispam avirus director imap pop queue rules smtp store) +foreach (AGENT antispam avirus collector director imap pop queue rules sieve smtp store) add_subdirectory (src/agents/${AGENT}) endforeach (AGENT) @@ -61,6 +69,8 @@ foreach (EXECUTABLE config manager testtool queuetool storetool sendmail) add_subdirectory (src/apps/${EXECUTABLE}) endforeach (EXECUTABLE) +add_subdirectory(src/www) + add_subdirectory (init) # install various other files @@ -74,5 +84,12 @@ install(DIRECTORY zoneinfo FILES_MATCHING PATTERN "*.zone" PATTERN ".svn" EXCLUDE) +install(DIRECTORY DESTINATION ${XPL_DEFAULT_DBF_DIR} + DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE + GROUP_READ GROUP_EXECUTE) + +install(FILES src/agents/collector/provider-presets.json + DESTINATION ${XPL_DEFAULT_DATA_DIR}/providers) + # include rules for make distribution tarballs include(cmake/Distribution.cmake) diff --git a/README b/README index f17a57f..5714725 100644 --- a/README +++ b/README @@ -1,7 +1,21 @@ -##### Ths project is no longer maintained #### -##### It is left here for refernce purposes #### - -Bongo is an easy-to-use mail and calendar system, offering a simple yet powerful user interface. +Bongo is an easy-to-use mail and calendar system, offering a simple yet powerful user interface. The goal is to make sharing, organisation, and communication simpler, quicker, and more useful. -You can find out more on http://www.bongo-project.org +This repository contains the maintained Python 3 and CMake based Bongo 0.7 +development line. + +Build requirements include a C11 compiler, CMake, Python 3 with development +headers, python-dateutil, vobject, nh3, GnuTLS, SQLite, Cyrus SASL, GNU +Mailutils, libical and the libraries detected by CMake. Optional mail +authentication support is enabled when libspf2, OpenDKIM, OpenDMARC and libsrs2 +are installed. + +Typical build and test commands are: + + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build + ctest --test-dir build --output-on-failure + +Use `-DCMAKE_INSTALL_PREFIX=/usr` for distribution packages. CPack source +archives are named `bongo-.tar.*`; platform names are reserved for +binary archives. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..3e0b872 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,127 @@ +# Bongo roadmap + +## 0.7.0 implementation status (2026-07-16) + +Completed foundations: CMake/CPack packaging, GCC/Clang warning cleanup, +Python 3.14 compatibility, system vobject/dateutil migration, SMTP/IMAP/POP +build targets, SPF/DKIM/DMARC/SRS libraries, Cyrus SASL PLAIN/LOGIN support, +RFC 5228 validation/ManageSieve scaffolding, systemd templates, and sanitizer +test coverage. + +Still required before the 0.7.0 release: production integration of external +account collection, internal relay policy and normalization, user filter/task +and signature storage, complete IMAP/POP/SMTP capability work, TOTP and app +passwords, complete CalDAV/CardDAV behavior, interoperability tests, and +distribution Ebuilds/packages. No release tag is valid until these items are +implemented and tested. + +## Required for Bongo 0.7.0: modern mail protocols + +- Separate SMTP relay on port 25 from authenticated submission on ports 587 + (STARTTLS) and 465 (implicit TLS). Submission must require authentication by + default and use its own relay and sender policies. +- Add an optional internal relay listener (port 26 by default) for appliances + and trusted services. It must be disabled by default, bind independently and + refuse to start without an explicit source-network allowlist. +- Configure the internal relay through the installation wizard, the web admin + interface and the console configuration tool. All three must share one schema + and validation for bind address, networks, normalization domain, explicit + device mappings, verified DNS suffixes, TLS compatibility and rate limits. +- Add external mail accounts as per-user database objects, never global server + configuration. One account combines inbound IMAP/POP collection settings, + retention/deletion policy, folder mapping and duplicate tracking with its + authenticated outbound SMTP identity. Store provider passwords, app passwords + and SMTP tokens only in the encrypted credential store. Users manage their own + accounts in web settings; administrators control policy and may assist through + admin and console interfaces. Include provider connection tests, certificate + verification, secret rotation and explicit Proton Bridge/SMTP-token modes. +- Detect mailing lists primarily from RFC 2919 `List-ID` and RFC 2369 + `List-Post` headers. Expose filter conditions for any mailing list and for a + specific canonical List-ID; existing move/copy actions then file messages in + a user-selected mail folder. +- Show a per-user `!` task when a new List-ID is first detected. From that task + the user can create a folder rule, ignore it once, permanently disable future + prompts for that List-ID, or delete the task. Deleting a task must never + delete its message; message deletion is a separate explicit action. Persist + task state by user and canonical List-ID so ignored/disabled prompts do not + reappear, and provide equivalent controls in web settings. +- Provide a general per-user filter editor. Conditions include sender address, + sender domain, recipient, subject, arbitrary headers, message size, mailing + list/List-ID and membership of the sender in a contact group (for example, + group `Family`). Actions include move/copy to a folder, forward, delete and + stop processing. Validate every rule server-side and never grant the web + process unrestricted write access to the system configuration store. +- Add per-user plain-text and HTML signatures to the restored web composer. + Allow multiple signatures and select a default per sender identity or + external account. Never append a personal signature a second time to mail + composed by IMAP/SMTP clients. Keep PlusPack signatures as a separate, + administrator-controlled organization disclaimer feature. +- Use RFC 5228 Sieve as the public filter import/export format and provide an + RFC 5804 ManageSieve service on port 4190 with STARTTLS and secure SASL. + Web filters, imported scripts and ManageSieve must share one parser, + capability set, validator and active-script store; unsupported extensions + must produce an explicit error rather than being silently discarded. +- Provide a native RFC 5230 `vacation` action and RFC 6131 + `vacation-seconds` for the web out-of-office assistant. Bongo must own + sender suppression, loop prevention, rate limiting, reply generation and + audit logging; do not execute Mailutils' external vacation delivery module. +- Use Cyrus SASL as the common authentication mechanism implementation for + SMTP submission, IMAP, POP3 and ManageSieve. Keep authorization, app-password + policy and auditing in Bongo callbacks; do not duplicate PLAIN, SCRAM or + OAUTHBEARER protocol implementations in individual agents. +- Support SASL PLAIN and LOGIN over TLS, SASL initial responses, SCRAM-SHA-256, + app passwords and optional OAUTHBEARER/EXTERNAL. Never advertise mechanisms + that are unavailable or unsafe on the current connection. +- Add POP3 SASL and integrate its authentication with app-password policy. +- Add IMAP IDLE, UIDPLUS, MOVE, SPECIAL-USE, LIST-EXTENDED and LITERAL+. +- Add IMAP ENABLE and UTF8=ACCEPT, then advertise IMAP4rev2 only after every + mandatory rev2 behavior and interoperability test passes. +- Implement SMTPUTF8 end to end through submission, relay, LMTP, queue, DSN, + DKIM and SRS handling. +- Add outbound certificate verification and policy support for DANE, MTA-STS, + TLS-RPT and REQUIRETLS. +- Add CONDSTORE/QRESYNC, ESEARCH/SEARCHRES, SORT/THREAD, BINARY and APPENDLIMIT. +- Cover every advertised capability with protocol tests and Thunderbird + interoperability tests. + +## Required for Bongo 0.7.0: authentication hardening + +- Add optional TOTP two-factor authentication for the web interface and + administration. +- Add single-display recovery codes and a secure recovery/reset workflow. +- Add individually revocable app passwords for IMAP, POP3, SMTP submission, + CalDAV and CardDAV. +- Allow each app password to be restricted by service, expiry time and, + optionally, source network. +- Store app passwords with Argon2id and encrypt TOTP secrets at rest. Never + store or display reusable plaintext credentials. +- When 2FA is enabled, allow administrators to require app passwords and + disable the account password for non-interactive protocols. +- Record creation, last use, service and source address without logging the + credential itself. + +Possible implementation dependencies are liboath, libargon2 and optional +libqrencode. This work is a release requirement for Bongo 0.7.0 and must be +implemented and tested after CalDAV/CardDAV and the web restoration, before +packaging and release. + +## Groupware + +- Complete CalDAV event discovery, synchronization and CRUD operations. +- Add CardDAV contacts using Bongo's existing address-book store. +- Add Thunderbird-oriented setup and RFC 6764 service discovery. +- Keep Outlook/MAPI/OpenChange integration deferred until after 0.7.0. Preserve + stable service boundaries and identifiers in authentication, contacts, + calendars, folders and filters so that a future MAPI compatibility layer can + be added without coupling it to the web UI or replacing the core data model. + +## External library cleanup + +- Prefer maintained system libraries over historical bundled snapshots. +- Keep libical as the native iCalendar parser/converter and update the minimum + supported system version after cross-architecture testing. +- Bongo 0.7 uses system vobject/python-dateutil, Python's `json` and `email` + standard libraries, and no longer ships the historical bundled copies. +- SimpleTAL remains temporarily embedded only for the legacy administration + templates; replace that layer after the restored web administration UI has + equivalent coverage. diff --git a/cmake/Directories.cmake b/cmake/Directories.cmake index 53291dc..a405205 100644 --- a/cmake/Directories.cmake +++ b/cmake/Directories.cmake @@ -21,7 +21,9 @@ set(INCLUDEDIR ${PROJECT_BINARY_DIR}/include) # various paths for where objects get installed / work from set(XPL_DEFAULT_DATA_DIR ${CMAKE_INSTALL_PREFIX}/share/bongo) -set(XPL_DEFAULT_STATE_DIR ${CMAKE_INSTALL_PREFIX}/var/bongo) +set(BONGO_STATE_DIR /var/lib/bongo CACHE PATH + "Persistent Bongo state directory") +set(XPL_DEFAULT_STATE_DIR ${BONGO_STATE_DIR}) set(XPL_DEFAULT_BIN_DIR ${CMAKE_INSTALL_PREFIX}/sbin) set(XPL_DEFAULT_LIB_DIR ${CMAKE_INSTALL_PREFIX}/${LIB_DIR_NAME}) diff --git a/cmake/Distribution.cmake b/cmake/Distribution.cmake index 6a003c1..72f1e2f 100644 --- a/cmake/Distribution.cmake +++ b/cmake/Distribution.cmake @@ -2,10 +2,28 @@ set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Mail and calendering made simple") set(CPACK_PACKAGE_VENDOR "Bongo Project") set(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/README") set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/COPYING") -set(CPACK_PACKAGE_VERSION_MAJOR "${BONGO_V_MAJOR}") -set(CPACK_PACKAGE_VERSION_MINOR "${BONGO_V_MINOR}") -set(CPACK_PACKAGE_VERSION_PATCH "${BONGO_V_PATCH}") +set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") +set(CPACK_PACKAGE_VERSION_MAJOR "${PROJECT_VERSION_MAJOR}") +set(CPACK_PACKAGE_VERSION_MINOR "${PROJECT_VERSION_MINOR}") +set(CPACK_PACKAGE_VERSION_PATCH "${PROJECT_VERSION_PATCH}") +set(CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}") +set(CPACK_SOURCE_IGNORE_FILES + "/\\.git/" + "/\\.git$" + "/__pycache__/" + "\\.py[co]$" + "/CMakeFiles/" + "/CMakeCache\\.txt$" + "/cmake_install\\.cmake$" + "/Makefile$" + "~$" + "\\.sw[op]$") set(CPACK_PACKAGE_INSTALL_DIRECTORY "CMake ${CMake_VERSION_MAJOR}.${CMake_VERSION_MINOR}") +# Several legacy install rules use absolute destinations derived from +# CMAKE_INSTALL_PREFIX. Always stage those paths below CPack's temporary +# directory; package generation must never write into the host filesystem. +set(CPACK_SET_DESTDIR ON) + include(CPack) diff --git a/cmake/EmbedFile.cmake b/cmake/EmbedFile.cmake new file mode 100644 index 0000000..c230b9e --- /dev/null +++ b/cmake/EmbedFile.cmake @@ -0,0 +1,18 @@ +if(NOT DEFINED INPUT OR NOT DEFINED OUTPUT OR NOT DEFINED SYMBOL) + message(FATAL_ERROR "EmbedFile.cmake requires INPUT, OUTPUT and SYMBOL") +endif() + +file(READ "${INPUT}" content HEX) +string(REGEX MATCHALL ".." bytes "${content}") +set(source "unsigned char ${SYMBOL}[] = {\n") +set(column 0) +foreach(byte IN LISTS bytes) + string(APPEND source "0x${byte},") + math(EXPR column "${column} + 1") + if(column EQUAL 16) + string(APPEND source "\n") + set(column 0) + endif() +endforeach() +string(APPEND source "0x00\n};\n") +file(WRITE "${OUTPUT}" "${source}") diff --git a/cmake/FindOpenDKIM.cmake b/cmake/FindOpenDKIM.cmake new file mode 100644 index 0000000..5187de0 --- /dev/null +++ b/cmake/FindOpenDKIM.cmake @@ -0,0 +1,14 @@ +find_path(OpenDKIM_INCLUDE_DIR NAMES opendkim/dkim.h) +find_library(OpenDKIM_LIBRARY NAMES opendkim) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(OpenDKIM REQUIRED_VARS OpenDKIM_LIBRARY OpenDKIM_INCLUDE_DIR) + +if(OpenDKIM_FOUND AND NOT TARGET OpenDKIM::OpenDKIM) + add_library(OpenDKIM::OpenDKIM UNKNOWN IMPORTED) + set_target_properties(OpenDKIM::OpenDKIM PROPERTIES + IMPORTED_LOCATION "${OpenDKIM_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${OpenDKIM_INCLUDE_DIR}") +endif() + +mark_as_advanced(OpenDKIM_INCLUDE_DIR OpenDKIM_LIBRARY) diff --git a/cmake/FindOpenDMARC.cmake b/cmake/FindOpenDMARC.cmake new file mode 100644 index 0000000..8ade864 --- /dev/null +++ b/cmake/FindOpenDMARC.cmake @@ -0,0 +1,14 @@ +find_path(OpenDMARC_INCLUDE_DIR NAMES opendmarc/dmarc.h) +find_library(OpenDMARC_LIBRARY NAMES opendmarc) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(OpenDMARC REQUIRED_VARS OpenDMARC_LIBRARY OpenDMARC_INCLUDE_DIR) + +if(OpenDMARC_FOUND AND NOT TARGET OpenDMARC::OpenDMARC) + add_library(OpenDMARC::OpenDMARC UNKNOWN IMPORTED) + set_target_properties(OpenDMARC::OpenDMARC PROPERTIES + IMPORTED_LOCATION "${OpenDMARC_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${OpenDMARC_INCLUDE_DIR}") +endif() + +mark_as_advanced(OpenDMARC_INCLUDE_DIR OpenDMARC_LIBRARY) diff --git a/cmake/FindSPF2.cmake b/cmake/FindSPF2.cmake new file mode 100644 index 0000000..b1d685b --- /dev/null +++ b/cmake/FindSPF2.cmake @@ -0,0 +1,14 @@ +find_path(SPF2_INCLUDE_DIR NAMES spf2/spf.h) +find_library(SPF2_LIBRARY NAMES spf2) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SPF2 REQUIRED_VARS SPF2_LIBRARY SPF2_INCLUDE_DIR) + +if(SPF2_FOUND AND NOT TARGET SPF2::SPF2) + add_library(SPF2::SPF2 UNKNOWN IMPORTED) + set_target_properties(SPF2::SPF2 PROPERTIES + IMPORTED_LOCATION "${SPF2_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${SPF2_INCLUDE_DIR}") +endif() + +mark_as_advanced(SPF2_INCLUDE_DIR SPF2_LIBRARY) diff --git a/cmake/FindSRS2.cmake b/cmake/FindSRS2.cmake new file mode 100644 index 0000000..5504843 --- /dev/null +++ b/cmake/FindSRS2.cmake @@ -0,0 +1,14 @@ +find_path(SRS2_INCLUDE_DIR NAMES srs2.h) +find_library(SRS2_LIBRARY NAMES srs2) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SRS2 REQUIRED_VARS SRS2_LIBRARY SRS2_INCLUDE_DIR) + +if(SRS2_FOUND AND NOT TARGET SRS2::SRS2) + add_library(SRS2::SRS2 UNKNOWN IMPORTED) + set_target_properties(SRS2::SRS2 PROPERTIES + IMPORTED_LOCATION "${SRS2_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${SRS2_INCLUDE_DIR}") +endif() + +mark_as_advanced(SRS2_INCLUDE_DIR SRS2_LIBRARY) diff --git a/cmake/Requirements.cmake b/cmake/Requirements.cmake index 7be3fa2..5d8e00a 100644 --- a/cmake/Requirements.cmake +++ b/cmake/Requirements.cmake @@ -82,6 +82,23 @@ pkg_check_modules (SQLITE REQUIRED sqlite3) # check for curl pkg_check_modules (CURL REQUIRED libcurl) +# RFC 5228 Sieve parser and validator used by the filter UI and ManageSieve. +find_path(MAILUTILS_INCLUDE_DIR mailutils/sieve.h REQUIRED) +find_library(MAILUTILS_SIEVE_LIBRARY NAMES mu_sieve REQUIRED) +find_library(MAILUTILS_LIBRARY NAMES mailutils REQUIRED) +set(MAILUTILS_LIBRARIES ${MAILUTILS_SIEVE_LIBRARY} ${MAILUTILS_LIBRARY}) + +# Shared SASL implementation for SMTP, IMAP, POP3 and ManageSieve. +pkg_check_modules(SASL2 REQUIRED libsasl2>=2.1.28) + +# Native mail authentication. Bongo links to the protocol libraries directly; +# it does not require the corresponding milter daemons at runtime. +find_package(SPF2 REQUIRED) +find_package(OpenDKIM REQUIRED) +find_package(OpenDMARC REQUIRED) +find_package(SRS2 REQUIRED) +pkg_check_modules(PSL REQUIRED libpsl) + # check for libical check_include_file(libical/ical.h HAVE_ICAL_H) if (NOT HAVE_ICAL_H) @@ -99,34 +116,43 @@ else (HAVE_ICAL) endif (HAVE_ICAL) # check for Python -include(FindPythonLibs) -if (NOT PYTHONLIBS_FOUND) +find_package(Python3 COMPONENTS Interpreter Development REQUIRED) +set(PYTHON_EXECUTABLE "${Python3_EXECUTABLE}") +set(PYTHON_LIBRARIES "${Python3_LIBRARIES}") +set(PYTHON_INCLUDE_DIRS "${Python3_INCLUDE_DIRS}") +set(PYTHON_INCLUDE_PATH "${Python3_INCLUDE_DIRS}") +function(python_add_module target) + add_library(${target} MODULE ${ARGN}) +endfunction() +if (NOT Python3_Development_FOUND) message(FATAL_ERROR "Bongo requires Python development libraries installed") -endif (NOT PYTHONLIBS_FOUND) +endif () -execute_process(COMMAND python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()" +execute_process( + COMMAND "${Python3_EXECUTABLE}" -c "import dateutil, nh3, vobject" + RESULT_VARIABLE PYTHON_NH3_FOUND +) +if (NOT PYTHON_NH3_FOUND EQUAL 0) + message(FATAL_ERROR "Bongo requires the Python dateutil, nh3, and vobject modules") +endif () + +execute_process(COMMAND "${Python3_EXECUTABLE}" -c "import sysconfig; print(sysconfig.get_path('purelib', vars={'base':'', 'platbase':''}))" RESULT_VARIABLE PYTHON_SITEPACKAGES_FOUND OUTPUT_VARIABLE PYTHON_SITEPACKAGES_PATH_RAW ) string(STRIP "${PYTHON_SITEPACKAGES_PATH_RAW}" PYTHON_SITEPACKAGES_PATH_RAW) -execute_process(COMMAND python -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)" +execute_process(COMMAND "${Python3_EXECUTABLE}" -c "import sysconfig; print(sysconfig.get_path('platlib', vars={'base':'', 'platbase':''}))" RESULT_VARIABLE PYTHON_SITELIB_FOUND OUTPUT_VARIABLE PYTHON_SITELIB_PATH_RAW ) string(STRIP "${PYTHON_SITELIB_PATH_RAW}" PYTHON_SITELIB_PATH_RAW) -string(LENGTH "${CMAKE_INSTALL_PREFIX}" CMAKE_INSTALL_PREFIX_LEN) -string(SUBSTRING "${PYTHON_SITEPACKAGES_PATH_RAW}" 0 ${CMAKE_INSTALL_PREFIX_LEN} PYTHON_SITEPACKAGES_PATH_RAW_PREFIX) - if (PYTHON_SITEPACKAGES_FOUND EQUAL 0) - if (PYTHON_SITEPACKAGES_PATH_RAW_PREFIX STREQUAL CMAKE_INSTALL_PREFIX) - set(PYTHON_SITEPACKAGES_PATH "${PYTHON_SITEPACKAGES_PATH_RAW}") - set(PYTHON_SITELIB_PATH "${PYTHON_SITELIB_PATH_RAW}") - else () - set(PYTHON_SITEPACKAGES_PATH "${CMAKE_INSTALL_PREFIX}/${PYTHON_SITEPACKAGES_PATH_RAW}") - set(PYTHON_SITELIB_PATH "${CMAKE_INSTALL_PREFIX}/${PYTHON_SITELIB_PATH_RAW}") - endif () + # sysconfig returns paths relative to an empty base with a leading slash. + # Prefix them exactly once so /usr/local never becomes /usr/local/usr. + set(PYTHON_SITEPACKAGES_PATH "${CMAKE_INSTALL_PREFIX}${PYTHON_SITEPACKAGES_PATH_RAW}") + set(PYTHON_SITELIB_PATH "${CMAKE_INSTALL_PREFIX}${PYTHON_SITELIB_PATH_RAW}") else (PYTHON_SITEPACKAGES_FOUND EQUAL 0) message(FATAL_ERROR "Couldn't determine where to install Python modules") endif (PYTHON_SITEPACKAGES_FOUND EQUAL 0) diff --git a/config.h.cmake b/config.h.cmake index 1bd00dd..43f88eb 100644 --- a/config.h.cmake +++ b/config.h.cmake @@ -3,6 +3,7 @@ // our package name #cmakedefine PACKAGE "@CMAKE_PROJECT_NAME@" +#define BONGO_VERSION "@PROJECT_VERSION@" // the way we do includes on the python code seems to cause problems // with the cmake style of defines. @@ -77,7 +78,9 @@ // Legacy defines below #cmakedefine LINUX #cmakedefine UNIX -#cmakedefine HAVE_NANOSLEEP +#ifndef HAVE_NANOSLEEP +#cmakedefine HAVE_NANOSLEEP 1 +#endif #cmakedefine _BONGO_XPL_BIG_ENDIAN #cmakedefine _BONGO_XPL_LITTLE_ENDIAN #cmakedefine _BONGO_HAVE_DLFCN_H diff --git a/docs/antispam.md b/docs/antispam.md new file mode 100644 index 0000000..985d06f --- /dev/null +++ b/docs/antispam.md @@ -0,0 +1,40 @@ +# Spam and malware scanning + +Bongo uses two independent scanners. `bongoantispam` sends mail to a standard +SpamAssassin `spamd` service for content classification. `bongoavirus` uses +ClamAV for malware detection. Neither service replaces the other. + +The antispam agent is fail-open: a connection failure, malformed response or +configured size limit leaves the original message available for normal +delivery and records the problem in the log. It never sends a rejection or +bounce to the apparent sender. A successful SpamAssassin `PROCESS` response +replaces the queued copy with the processed message containing the normal +`X-Spam-*` headers and marks the envelope as already checked. + +The default `antispam` JSON configuration is: + +```json +{ + "version": 1, + "enabled": true, + "timeout": 15, + "maximum_message_size": 52428800, + "maximum_response_size": 53477376, + "hosts": ["127.0.0.1"] +} +``` + +`hosts` accepts the existing `host:port:weight` syntax and can list several +`spamd` servers. Port 783 is used when no port is specified. The response limit +must be at least as large as the input limit because SpamAssassin adds headers. + +Spam folder rules should match SpamAssassin's `X-Spam-Status` header. A high +score quarantine is deliberately not implemented through the historical +global quarantine queue: quarantine state belongs to the recipient and must be +visible and releasable through that user's task list. Until that per-user path +is active, mail is tagged and can be filed by Sieve without being destroyed. + +The systemd unit only orders Bongo after common ClamAV and SpamAssassin unit +names when those services are present. It does not pull either scanner in, so +remote scanners and installations without one of the optional services remain +supported. diff --git a/include/bongoagent.h b/include/bongoagent.h index b49b84e..fffd15a 100644 --- a/include/bongoagent.h +++ b/include/bongoagent.h @@ -195,7 +195,7 @@ struct _BongoGlobals { }; extern struct _BongoGlobals BongoGlobals; -static BongoConfigItem GlobalConfig[] = { +static BongoConfigItem GlobalConfig[] __attribute__((unused)) = { { BONGO_JSON_STRING, "o:hostname/s", &BongoGlobals.hostname }, { BONGO_JSON_STRING, "o:hostaddr/s", &BongoGlobals.hostaddr }, { BONGO_JSON_STRING, "o:postmaster/s", &BongoGlobals.postmaster }, diff --git a/include/bongosasl.h b/include/bongosasl.h new file mode 100644 index 0000000..4898f2d --- /dev/null +++ b/include/bongosasl.h @@ -0,0 +1,40 @@ +#ifndef BONGO_SASL_H +#define BONGO_SASL_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct BongoSaslSession BongoSaslSession; +typedef int (*BongoSaslVerify)(void *context, const char *user, + const char *password, size_t password_length); + +int BongoSaslInitialize(void); +void BongoSaslShutdown(void); +int BongoSaslSessionCreate(BongoSaslSession **session, const char *service, + const char *hostname, const char *local_address, + const char *remote_address, int tls_active, + BongoSaslVerify verify, void *context); +void BongoSaslSessionDestroy(BongoSaslSession **session); +int BongoSaslMechanisms(BongoSaslSession *session, const char **mechanisms); +int BongoSaslStart(BongoSaslSession *session, const char *mechanism, + const void *input, size_t input_length, + const void **output, size_t *output_length); +int BongoSaslStep(BongoSaslSession *session, const void *input, + size_t input_length, const void **output, + size_t *output_length); +const char *BongoSaslUsername(BongoSaslSession *session); +int BongoSaslDecode64(const char *input, size_t input_length, void *output, + size_t output_capacity, size_t *output_length); +int BongoSaslEncode64(const void *input, size_t input_length, char *output, + size_t output_capacity, size_t *output_length); + +enum { BONGO_SASL_FAIL = -1, BONGO_SASL_COMPLETE = 0, + BONGO_SASL_CONTINUE = 1 }; + +#ifdef __cplusplus +} +#endif +#endif diff --git a/include/bongosieve.h b/include/bongosieve.h new file mode 100644 index 0000000..ff112ea --- /dev/null +++ b/include/bongosieve.h @@ -0,0 +1,98 @@ +#ifndef BONGO_SIEVE_H +#define BONGO_SIEVE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Validate one complete UTF-8 RFC 5228 script without executing it. */ +int BongoSieveValidate(const char *script, size_t length); + +typedef enum { + BONGO_SIEVE_KEEP, + BONGO_SIEVE_DISCARD, + BONGO_SIEVE_FILEINTO, + BONGO_SIEVE_REDIRECT, + BONGO_SIEVE_REJECT, + BONGO_SIEVE_VACATION +} BongoSieveAction; +typedef struct { + BongoSieveAction action; + const char *argument; + const char *subject; + const char *from; + const char *handle; + const char *addresses[32]; + size_t address_count; + unsigned long days; + unsigned long seconds; + int has_seconds; + int mime; +} BongoSieveResult; +typedef int (*BongoSieveActionCallback)(const BongoSieveResult *result, + void *data); +/* Evaluate a script without allowing Mailutils to deliver or send anything. + * Each resulting action is reported to Bongo through callback. */ +int BongoSieveEvaluate(const char *script, size_t script_length, + const char *message, size_t message_length, + BongoSieveActionCallback callback, void *data); + +typedef struct BongoSieveStore BongoSieveStore; + +int BongoSieveStoreOpen(BongoSieveStore **store, const char *path); +void BongoSieveStoreClose(BongoSieveStore **store); +int BongoSieveStorePut(BongoSieveStore *store, const char *user, + const char *name, const char *script, size_t length); +int BongoSieveStoreGet(BongoSieveStore *store, const char *user, + const char *name, char **script, size_t *length, + int *active); +int BongoSieveStoreSetActive(BongoSieveStore *store, const char *user, + const char *name); +int BongoSieveStoreDelete(BongoSieveStore *store, const char *user, + const char *name); +int BongoSieveStoreRename(BongoSieveStore *store, const char *user, + const char *old_name, const char *new_name); +int BongoSieveVacationClaim(BongoSieveStore *store, const char *user, + const char *sender, const char *handle, + unsigned long interval_seconds); +void BongoSieveVacationRelease(BongoSieveStore *store, const char *user, + const char *sender, const char *handle); +typedef int (*BongoSieveListCallback)(const char *name, int active, void *data); +int BongoSieveStoreList(BongoSieveStore *store, const char *user, + BongoSieveListCallback callback, void *data); + +typedef enum { + BONGO_MANAGESIEVE_INVALID = 0, + BONGO_MANAGESIEVE_AUTHENTICATE, + BONGO_MANAGESIEVE_CAPABILITY, + BONGO_MANAGESIEVE_CHECKSCRIPT, + BONGO_MANAGESIEVE_DELETESCRIPT, + BONGO_MANAGESIEVE_GETSCRIPT, + BONGO_MANAGESIEVE_HAVESPACE, + BONGO_MANAGESIEVE_LISTSCRIPTS, + BONGO_MANAGESIEVE_LOGOUT, + BONGO_MANAGESIEVE_NOOP, + BONGO_MANAGESIEVE_PUTSCRIPT, + BONGO_MANAGESIEVE_RENAMESCRIPT, + BONGO_MANAGESIEVE_SETACTIVE, + BONGO_MANAGESIEVE_STARTTLS +} BongoManageSieveCommand; + +typedef struct { + BongoManageSieveCommand command; + char arguments[3][1025]; + size_t argument_count; + size_t literal_length; + int literal_nonsynchronizing; +} BongoManageSieveRequest; + +int BongoManageSieveParse(const char *line, size_t length, + BongoManageSieveRequest *request); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/bongoutil.h b/include/bongoutil.h index d8529f6..a21da1d 100644 --- a/include/bongoutil.h +++ b/include/bongoutil.h @@ -73,8 +73,8 @@ enum CommandColors { typedef int (* CommandHandler)(void *param); typedef struct _ProtocolCommand { - unsigned char *name; - unsigned char *help; + const char *name; + const char *help; int length; @@ -96,7 +96,7 @@ typedef struct _ProtocolCommandTree { ProtocolCommand proxy; } ProtocolCommandTree; -extern const unsigned char *Base64Chars; +extern const char Base64Chars[]; /***** Begin BongoKeyword *****/ #define BONGO_KEYWORD_VALUES_PER_BYTE 256 @@ -118,7 +118,7 @@ extern const unsigned char *Base64Chars; typedef unsigned long BongoKeywordTable[BONGO_KEYWORD_VALUES_PER_BYTE]; typedef struct { - char *string; + const char *string; unsigned long len; unsigned long id; } BongoKeyword; @@ -131,22 +131,22 @@ typedef struct { BOOL caseInsensitive; } BongoKeywordIndex; -BongoKeywordIndex *BongoKeywordIndexCreate(char **strings, unsigned long count, BOOL makeCaseInsensitive); +BongoKeywordIndex *BongoKeywordIndexCreate(const char *const *strings, unsigned long count, BOOL makeCaseInsensitive); void BongoKeywordIndexFree(BongoKeywordIndex *Index); -long BongoKeywordFind(BongoKeywordIndex *keywordTable, unsigned char *searchString); -long BongoKeywordBegins(BongoKeywordIndex *keywordIndex, unsigned char *searchString); +long BongoKeywordFind(BongoKeywordIndex *keywordTable, const char *searchString); +long BongoKeywordBegins(BongoKeywordIndex *keywordIndex, const char *searchString); BongoKeywordIndex *BongoKeywordIndexCreateProtocolCommands(ProtocolCommand *ProtocolCommands); #define BongoKeywordIndexCreateFromTable(INDEX, TABLE, COLUMN, CASEINSENSITIVE) \ { \ - char **strings; \ + const char **strings; \ unsigned long count; \ unsigned long row; \ count = 0; \ while(TABLE[count]COLUMN != NULL) { \ count++; \ } \ - strings = MemMalloc(sizeof(unsigned char *) * (count + 1)); \ + strings = MemMalloc(sizeof(*strings) * (count + 1)); \ if (strings) { \ for (row = 0; row < count; row++) { \ strings[row] = TABLE[row]COLUMN; \ @@ -165,7 +165,7 @@ unsigned long *QuickSortIndexCreate(char **stringList, unsigned long count, BOOL void QuickSortIndexFree(unsigned long *index); void LoadProtocolCommandTree(ProtocolCommandTree *tree, ProtocolCommand *commands); -ProtocolCommand *ProtocolCommandTreeSearch(ProtocolCommandTree *tree, const unsigned char *command); +ProtocolCommand *ProtocolCommandTreeSearch(ProtocolCommandTree *tree, const char *command); void RemoveLineEnding(char *s); BOOL QuickNCmp(char *str1, char *str2, int len); @@ -173,10 +173,10 @@ BOOL QuickCmp(char *str1, char *str2); #define BongoStrNCpy(dest, src, len) {strncpy((dest), (src), (len)); (dest)[(len) - 1] = '\0';} -unsigned char *DecodeBase64(unsigned char *EncodedString); -unsigned char *EncodeBase64(const unsigned char *UnencodedString); +char *DecodeBase64(char *EncodedString); +char *EncodeBase64(const char *UnencodedString); -BOOL HashCredential(unsigned char *Credential, unsigned char *Hash); +BOOL HashCredential(const char *Credential, char *Hash); /* String functions */ diff --git a/include/connio.h b/include/connio.h index 80615b2..52cee3e 100644 --- a/include/connio.h +++ b/include/connio.h @@ -41,6 +41,7 @@ typedef gnutls_certificate_credentials gnutls_certificate_credentials_t; typedef struct { gnutls_certificate_credentials_t cert_cred; + unsigned long options; } bongo_ssl_context; #define CONN_BUFSIZE 1023 @@ -78,6 +79,7 @@ typedef struct { #define SSL_ALLOW_CHAIN (1 << 3) #define SSL_DISABLE_EMPTY_FRAGMENTS (1 << 4) #define SSL_DONT_INSERT_EMPTY_FRAGMENTS (1 << 5) +#define SSL_ALLOW_LEGACY_PROTOCOLS (1 << 6) void CHOP_NEWLINE(char *s); #if 0 @@ -271,7 +273,7 @@ int ConnSend(Connection *Conn, char *Buffer, unsigned int Length); int ConnReceive(Connection *Conn, char *Buffer, unsigned int Length); int ConnRead(Connection *Conn, char *Dest, int Length); -int ConnReadCount(Connection *Conn, char *Dest, int Count); +int ConnReadCount(Connection *Conn, void *Dest, int Count); int ConnReadLine(Connection *Conn, char *Line, int Length); int ConnReadAnswer(Connection *Conn, char *Line, int Length); long ConnReadToAllocatedBuffer(Connection *c, char **buffer, unsigned long *bufferSize); @@ -280,7 +282,7 @@ int ConnReadToFileUntilEOS(Connection *Src, FILE *Dest); int ConnReadToConn(Connection *Src, Connection *Dest, int Count); int ConnReadToConnUntilEOS(Connection *Src, Connection *Dest); -int ConnWrite(Connection *Conn, const char *Source, int Count); +int ConnWrite(Connection *Conn, const void *Source, int Count); int ConnWriteF(Connection *Conn, const char *Format, ...) XPL_PRINTF(2, 3); int ConnWriteVF(Connection *c, const char *format, va_list ap); int ConnWriteFile(Connection *Conn, FILE *Source); diff --git a/include/memmgr.h b/include/memmgr.h index a475c58..5dd9044 100644 --- a/include/memmgr.h +++ b/include/memmgr.h @@ -75,7 +75,7 @@ XPL_BEGIN_C_LINKAGE /* Private Pool API's */ typedef BOOL (*PoolEntryCB)(void *Buffer, void *ClientData); -EXPORT void *MemPrivatePoolAlloc(unsigned char *Ident, size_t AllocationSize, unsigned int MinAllocCount, unsigned int MaxAllocCount, BOOL Dynamic, BOOL Temporary, PoolEntryCB AllocCB, PoolEntryCB FreeCB, void *ClientData); +EXPORT void *MemPrivatePoolAlloc(const char *Ident, size_t AllocationSize, unsigned int MinAllocCount, unsigned int MaxAllocCount, BOOL Dynamic, BOOL Temporary, PoolEntryCB AllocCB, PoolEntryCB FreeCB, void *ClientData); EXPORT void MemPrivatePoolFree(void *PoolHandle); EXPORT void *MemPrivatePoolGetEntryDirect(void *PoolHandle, const char *SourceFile, unsigned long SourceLine); diff --git a/include/msgaddr.h b/include/msgaddr.h index 8f497e4..4feb95c 100644 --- a/include/msgaddr.h +++ b/include/msgaddr.h @@ -109,7 +109,7 @@ EXPORT const unsigned char MsgAddressCharacters[]; -EXPORT BOOL MsgParseAddress(unsigned char *AddressLine, size_t AddressLineLength, unsigned char **local_part, unsigned char **domain); +EXPORT BOOL MsgParseAddress(char *AddressLine, size_t AddressLineLength, char **local_part, char **domain); EXPORT int MsgIsComment(unsigned char *Base, unsigned char *Limit, unsigned char **Out); EXPORT BOOL MsgIsXText(unsigned char *In, unsigned char *Limit, unsigned char **Out); @@ -127,7 +127,7 @@ IMPORT BOOL MsgIsXText(unsigned char *In, unsigned char *Limit, unsigned char ** extern const unsigned char MsgAddressCharacters[]; -BOOL MsgParseAddress(unsigned char *AddressLine, size_t AddressLineLength, unsigned char **local_part, unsigned char **domain); +BOOL MsgParseAddress(char *AddressLine, size_t AddressLineLength, char **local_part, char **domain); //BOOL MsgIsAddress(unsigned char *AddressLine, size_t AddressLineLength, unsigned char *Delimiters, unsigned char **LineOut); int MsgIsComment(unsigned char *Base, unsigned char *Limit, unsigned char **Out); BOOL MsgIsXText(unsigned char *In, unsigned char *Limit, unsigned char **Out); diff --git a/include/msgdate.h b/include/msgdate.h index 26fcdd1..124aa5c 100644 --- a/include/msgdate.h +++ b/include/msgdate.h @@ -109,11 +109,11 @@ #define TZ_DATELINE 74 typedef struct _MsgDateFormat { - unsigned char **monthShort; - unsigned char **monthLong; - unsigned char **wDayShort; - unsigned char **wDayLong; - unsigned char **AmPm; + const char *const *monthShort; + const char *const *monthLong; + const char *const *wDayShort; + const char *const *wDayLong; + const char *const *AmPm; unsigned long wDayStart; @@ -139,7 +139,7 @@ EXPORT void MsgGetDate(long rataDie, unsigned long *day, unsigned long *month, u EXPORT long MsgGetRataDie(unsigned long day, unsigned long month, unsigned long year); /* Date Interface */ -EXPORT BOOL MsgGetRFC822Date(long offset, long utcCime, unsigned char *dateBuffer); +EXPORT BOOL MsgGetRFC822Date(long offset, long utcCime, char *dateBuffer); EXPORT long MsgGetUTCOffset(void); EXPORT unsigned long MsgGetUTC(unsigned long day, unsigned long month, unsigned long year, unsigned long hour, unsigned long minute, unsigned long second); @@ -147,7 +147,7 @@ EXPORT BOOL MsgGetDMY(unsigned long utcTime, unsigned long *day, unsigned long * EXPORT unsigned long MsgGetTimezoneID(long day, long wDay, unsigned long month, unsigned long hour, long dstDay, long dstWDay, unsigned long dstMonth, unsigned long dstHour, long offset, long dstOffset); EXPORT long MsgGetUTCOffsetByDate(unsigned long timezone, unsigned long day, unsigned long month, unsigned long year, unsigned long hour); EXPORT long MsgGetUTCOffsetByUTC(unsigned long timezone, unsigned long utc); -EXPORT unsigned long MsgPrint(unsigned char *buffer, int bufLen, unsigned char *format, unsigned long time, MsgDateFormat *dateInfo); +EXPORT unsigned long MsgPrint(char *buffer, int bufLen, const char *format, unsigned long time, const MsgDateFormat *dateInfo); unsigned long MsgLookupRFC822Month(const char *name); uint64_t MsgParseRFC822DateTime(const char *date); diff --git a/include/rulesrv.h b/include/rulesrv.h index 6e3daeb..5ea9e5a 100644 --- a/include/rulesrv.h +++ b/include/rulesrv.h @@ -74,6 +74,8 @@ #define RULE_COND_HASMIMETYPE 'f' /* Arg1: Mime type to look for */ #define RULE_COND_HASMIMETYPENOT 'g' /* Arg1: Mime type to look for */ +#define RULE_COND_MAILINGLIST 'h' /* No args */ +#define RULE_COND_LISTID 'i' /* Arg1: RFC 2919 list identifier */ #define RULE_ACT_REPLY 'A' /* Arg1 : Filename */ #define RULE_ACT_DELETE 'B' /* No args */ diff --git a/include/streamio.h b/include/streamio.h index a39e5ad..e5a91d5 100644 --- a/include/streamio.h +++ b/include/streamio.h @@ -62,7 +62,7 @@ typedef struct _StreamStruct { typedef struct { - unsigned char *Charset; + const char *Charset; int (*Decoder)(struct _StreamStruct *Stream, struct _StreamStruct *NextStream); int (*Encoder)(struct _StreamStruct *Stream, struct _StreamStruct *NextStream); unsigned long CharsetType; @@ -73,9 +73,9 @@ typedef int (*StreamCodecFunc)(struct _StreamStruct *Stream, struct _StreamSt typedef BOOL (*TemplateHandlerFunc)(void *Client, void *Session, unsigned long TemplateID, void *Token, void *GotoToken, void *ObjectData); /* Helpers from streamio.c */ -StreamCodecFunc FindCodec(unsigned char *Charset, BOOL Encoder); -StreamCodecFunc FindCodecDecoder(unsigned char *Charset); -StreamCodecFunc FindCodecEncoder(unsigned char *Charset); +StreamCodecFunc FindCodec(const char *Charset, BOOL Encoder); +StreamCodecFunc FindCodecDecoder(const char *Charset); +StreamCodecFunc FindCodecEncoder(const char *Charset); BOOL StreamioInit(void); BOOL StreamioShutdown(void); @@ -86,4 +86,3 @@ extern StreamDescStruct StreamList[]; #endif #endif /* _STREAMIO_H_ */ - diff --git a/init/CMakeLists.txt b/init/CMakeLists.txt index 217f10c..3a7c58c 100644 --- a/init/CMakeLists.txt +++ b/init/CMakeLists.txt @@ -10,3 +10,26 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/bongo.xml.solaris-smf ${CMAKE_CURRENT #configure_file(${CMAKE_CURRENT_SOURCE_DIR}/http-bongo.solaris-smf.in ${CMAKE_CURRENT_BINARY_DIR}/http-bongo.solaris-smf) #configure_file(${CMAKE_CURRENT_SOURCE_DIR}/http-bongo.xml.solaris-smf ${CMAKE_CURRENT_BINARY_DIR}/http-bongo.xml.solaris-smf) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/svc-bongo.solaris-smf.in ${CMAKE_CURRENT_BINARY_DIR}/svc-bongo.solaris-smf) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/bongo.service.in + ${CMAKE_CURRENT_BINARY_DIR}/bongo.service @ONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/bongo.tmpfiles.in + ${CMAKE_CURRENT_BINARY_DIR}/bongo.conf @ONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/bongo.sysusers.in + ${CMAKE_CURRENT_BINARY_DIR}/bongo-user.conf @ONLY) + +set(SYSTEMD_SYSTEM_UNIT_DIR "lib/systemd/system" CACHE PATH + "Directory for systemd system service units") +set(SYSTEMD_TMPFILES_DIR "lib/tmpfiles.d" CACHE PATH + "Directory for systemd tmpfiles configuration") +set(SYSTEMD_SYSUSERS_DIR "lib/sysusers.d" CACHE PATH + "Directory for systemd sysusers configuration") +option(INSTALL_SYSTEMD_UNITS "Install systemd service and tmpfiles files" ON) + +if(INSTALL_SYSTEMD_UNITS) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/bongo.service + DESTINATION ${SYSTEMD_SYSTEM_UNIT_DIR}) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/bongo.conf + DESTINATION ${SYSTEMD_TMPFILES_DIR}) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/bongo-user.conf + DESTINATION ${SYSTEMD_SYSUSERS_DIR}) +endif() diff --git a/init/bongo.service.in b/init/bongo.service.in new file mode 100644 index 0000000..e914761 --- /dev/null +++ b/init/bongo.service.in @@ -0,0 +1,27 @@ +[Unit] +Description=Bongo mail and calendar server +Documentation=https://gitea.disconnected-by-peer.at/geos_one/bongo +Wants=network-online.target +After=network-online.target nss-lookup.target local-fs.target clamd.service clamav-daemon.service spamassassin.service spamd.service + +[Service] +Type=simple +ExecStart=@SBIN_INSTALL_DIR@/bongo-manager -k +ExecReload=/bin/kill -USR1 $MAINPID +Restart=on-failure +RestartSec=5s +TimeoutStartSec=60s +TimeoutStopSec=45s +KillMode=control-group +KillSignal=SIGTERM +UMask=0077 +LimitNOFILE=65536 +TasksMax=4096 +PrivateTmp=true +PrivateDevices=true +ProtectHome=true +ProtectSystem=full +ReadWritePaths=@XPL_DEFAULT_STATE_DIR@ + +[Install] +WantedBy=multi-user.target diff --git a/init/bongo.sysusers.in b/init/bongo.sysusers.in new file mode 100644 index 0000000..42d276e --- /dev/null +++ b/init/bongo.sysusers.in @@ -0,0 +1 @@ +u @BONGO_USER@ - "Bongo mail and calendar server" @XPL_DEFAULT_STATE_DIR@ - diff --git a/init/bongo.tmpfiles.in b/init/bongo.tmpfiles.in new file mode 100644 index 0000000..8ed09db --- /dev/null +++ b/init/bongo.tmpfiles.in @@ -0,0 +1,9 @@ +d @XPL_DEFAULT_STATE_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_DBF_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_WORK_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_CACHE_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_SCMS_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_SPOOL_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_SYSTEM_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_MAIL_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_COOKIE_DIR@ 0700 @BONGO_USER@ @BONGO_USER@ - diff --git a/po/bongo-web/de.po b/po/bongo-web/de.po new file mode 100644 index 0000000..55e08f5 --- /dev/null +++ b/po/bongo-web/de.po @@ -0,0 +1,105 @@ +msgid "" +msgstr "" +"Project-Id-Version: Bongo 0.7.0\n" +"PO-Revision-Date: 2026-07-15 00:00+0200\n" +"Last-Translator: Bongo Project\n" +"Language-Team: German\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "Email address" +msgstr "E-Mail-Adresse" +msgid "Password" +msgstr "Passwort" +msgid "Sign in" +msgstr "Anmelden" +msgid "Sign out" +msgstr "Abmelden" +msgid "Mail" +msgstr "Mail" +msgid "Calendar" +msgstr "Kalender" +msgid "Contacts" +msgstr "Kontakte" +msgid "Administration" +msgstr "Administration" +msgid "Mail, calendar and contacts" +msgstr "Mail, Kalender und Kontakte" +msgid "Login failed." +msgstr "Anmeldung fehlgeschlagen." +msgid "Too many login attempts. Please try again later." +msgstr "Zu viele Anmeldeversuche. Bitte später erneut versuchen." +msgid "Calendar access is available through CalDAV." +msgstr "Der Kalenderzugriff ist über CalDAV verfügbar." +msgid "Contact access is available through CardDAV." +msgstr "Der Kontaktzugriff ist über CardDAV verfügbar." +msgid "The secure webmail connection is being enabled." +msgstr "Die sichere Webmail-Anbindung wird aktiviert." +msgid "Administrative functions require a server-side role check." +msgstr "Administrative Funktionen erfordern eine serverseitige Rollenprüfung." +msgid "Folder" +msgstr "Ordner" +msgid "Refresh" +msgstr "Aktualisieren" +msgid "(no subject)" +msgstr "(kein Betreff)" +msgid "Back" +msgstr "Zurück" +msgid "Load external images" +msgstr "Externe Bilder laden" +msgid "[external image blocked]" +msgstr "[externes Bild blockiert]" +msgid "Compose new mail" +msgstr "Neue E-Mail verfassen" +msgid "From" +msgstr "Von" +msgid "To" +msgstr "An" +msgid "Cc" +msgstr "Kopie" +msgid "Bcc" +msgstr "Blindkopie" +msgid "Subject" +msgstr "Betreff" +msgid "Message" +msgstr "Nachricht" +msgid "Send" +msgstr "Senden" +msgid "Cancel" +msgstr "Abbrechen" +msgid "Message sent." +msgstr "Nachricht wurde gesendet." +msgid "The message could not be sent." +msgstr "Die Nachricht konnte nicht gesendet werden." +msgid "Invalid message fields." +msgstr "Ungültige Nachrichtenfelder." +msgid "Tasks" +msgstr "Aufgaben" +msgid "No open tasks." +msgstr "Keine offenen Aufgaben." +msgid "Ignore" +msgstr "Ignorieren" +msgid "Disable" +msgstr "Deaktivieren" +msgid "Done" +msgstr "Erledigt" +msgid "Settings" +msgstr "Einstellungen" +msgid "Personal settings" +msgstr "Persönliche Einstellungen" +msgid "Open tasks" +msgstr "Offene Aufgaben" +msgid "Filters" +msgstr "Filter" +msgid "External mail accounts" +msgstr "Externe E-Mail-Konten" +msgid "Sender identities" +msgstr "Absenderidentitäten" +msgid "Two-factor authentication" +msgstr "Zwei-Faktor-Authentifizierung" +msgid "App passwords" +msgstr "App-Passwörter" +msgid "Language and display" +msgstr "Sprache und Darstellung" diff --git a/po/bongo-web/fr.po b/po/bongo-web/fr.po index 0841ad3..240dc6e 100644 --- a/po/bongo-web/fr.po +++ b/po/bongo-web/fr.po @@ -12,10 +12,58 @@ msgstr "" "PO-Revision-Date: 2007-06-30 17:48+1000\n" "Last-Translator: Alexander Hixon \n" "Language-Team: fr\n" +"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +msgid "Email address" +msgstr "Adresse e-mail" +msgid "Password" +msgstr "Mot de passe" +msgid "Sign in" +msgstr "Se connecter" +msgid "Sign out" +msgstr "Se déconnecter" +msgid "Contacts" +msgstr "Contacts" +msgid "Mail, calendar and contacts" +msgstr "Courrier, calendrier et contacts" +msgid "Login failed." +msgstr "Échec de la connexion." +msgid "Too many login attempts. Please try again later." +msgstr "Trop de tentatives de connexion. Veuillez réessayer plus tard." +msgid "Calendar access is available through CalDAV." +msgstr "L’accès au calendrier est disponible via CalDAV." +msgid "Contact access is available through CardDAV." +msgstr "L’accès aux contacts est disponible via CardDAV." +msgid "The secure webmail connection is being enabled." +msgstr "La connexion sécurisée au webmail est en cours d’activation." +msgid "Administrative functions require a server-side role check." +msgstr "Les fonctions administratives nécessitent une vérification du rôle côté serveur." +msgid "Folder" +msgstr "Dossier" +msgid "Refresh" +msgstr "Actualiser" +msgid "(no subject)" +msgstr "(sans objet)" +msgid "Back" +msgstr "Retour" +msgid "Load external images" +msgstr "Charger les images externes" +msgid "[external image blocked]" +msgstr "[image externe bloquée]" +msgid "Cc" +msgstr "Cc" +msgid "Bcc" +msgstr "Cci" +msgid "Subject" +msgstr "Objet" +msgid "The message could not be sent." +msgstr "Le message n’a pas pu être envoyé." +msgid "Invalid message fields." +msgstr "Champs du message non valides." + #: src/www/js/AddressBook.js:232 #, fuzzy msgid "Add Contact" @@ -70,9 +118,8 @@ msgstr "" #: src/www/js/OccurrenceEditor.js:137 src/www/js/OccurrenceEditor.js:143 #: src/www/js/OccurrenceEditor.js:262 src/www/js/Widgets.js:1079 #: src/www/js/Widgets.js:1126 -#, fuzzy msgid "Cancel" -msgstr "Annulation" +msgstr "Annuler" #: src/www/js/AddressBook.js:608 src/www/js/CalendarEditor.js:915 #: src/www/js/OccurrenceEditor.js:138 src/www/js/OccurrenceEditor.js:144 @@ -380,9 +427,8 @@ msgid "Save Draft" msgstr "Save draft" #: src/www/js/Composer.js:163 -#, fuzzy msgid "Send" -msgstr "Send" +msgstr "Envoyer" #: src/www/js/Composer.js:321 msgid "Untitled" @@ -402,7 +448,7 @@ msgstr "" #: src/www/js/Composer.js:459 msgid "Message sent." -msgstr "" +msgstr "Message envoyé." #: src/www/js/Composer.js:471 msgid "Error sending message: " @@ -456,7 +502,7 @@ msgstr "" #: src/www/js/Dragonfly.js:906 msgid "Compose new mail" -msgstr "" +msgstr "Nouveau message" #: src/www/js/Dragonfly.js:908 src/www/js/Dragonfly.js:909 #, fuzzy @@ -627,12 +673,11 @@ msgstr "Item" #: src/www/js/Mail.js:662 msgid "To" -msgstr "" +msgstr "À" #: src/www/js/Mail.js:662 -#, fuzzy msgid "From" -msgstr "From:" +msgstr "De" #: src/www/js/MailListView.js:101 src/www/js/MailListView.js:182 msgid "No messages here." @@ -1331,6 +1376,45 @@ msgstr "" #~ msgid "mailSubscriptionsLabel" #~ msgstr "Mailing Lists" +msgid "Tasks" +msgstr "Tâches" + +msgid "No open tasks." +msgstr "Aucune tâche ouverte." + +msgid "Disable" +msgstr "Désactiver" + +msgid "Done" +msgstr "Terminé" + +msgid "Settings" +msgstr "Paramètres" + +msgid "Personal settings" +msgstr "Paramètres personnels" + +msgid "Open tasks" +msgstr "Tâches ouvertes" + +msgid "Filters" +msgstr "Filtres" + +msgid "External mail accounts" +msgstr "Comptes de messagerie externes" + +msgid "Sender identities" +msgstr "Identités d’expéditeur" + +msgid "Two-factor authentication" +msgstr "Authentification à deux facteurs" + +msgid "App passwords" +msgstr "Mots de passe d’application" + +msgid "Language and display" +msgstr "Langue et affichage" + #~ msgid "mailNoMessages" #~ msgstr "No messages found." diff --git a/po/bongo-web/pt_BR.po b/po/bongo-web/pt_BR.po index e2f3c0b..1fe1fb5 100644 --- a/po/bongo-web/pt_BR.po +++ b/po/bongo-web/pt_BR.po @@ -3,19 +3,66 @@ # This file is distributed under the same license as the Bongo package. # Igor Morgado , 2007 # -#, fuzzy msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: bongo-devel@gna.org\n" +"Project-Id-Version: Bongo 0.7.0\n" +"Report-Msgid-Bugs-To: bongo-devel@disconnected-by-peer.at\n" "POT-Creation-Date: 2007-07-13 11:46+0100\n" "PO-Revision-Date: 2007-11-08 17:47+BRST\n" "Last-Translator: Igor Morgado \n" "Language-Team: Portuguese\n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=pt_BR.UTF-8\n" +"Language: pt_BR\n" +"Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +msgid "Email address" +msgstr "Endereço de e-mail" +msgid "Password" +msgstr "Senha" +msgid "Sign in" +msgstr "Entrar" +msgid "Sign out" +msgstr "Sair" +msgid "Contacts" +msgstr "Contatos" +msgid "Mail, calendar and contacts" +msgstr "E-mail, calendário e contatos" +msgid "Login failed." +msgstr "Falha no login." +msgid "Too many login attempts. Please try again later." +msgstr "Muitas tentativas de login. Tente novamente mais tarde." +msgid "Calendar access is available through CalDAV." +msgstr "O acesso ao calendário está disponível por CalDAV." +msgid "Contact access is available through CardDAV." +msgstr "O acesso aos contatos está disponível por CardDAV." +msgid "The secure webmail connection is being enabled." +msgstr "A conexão segura ao webmail está sendo ativada." +msgid "Administrative functions require a server-side role check." +msgstr "As funções administrativas exigem verificação de função no servidor." +msgid "Folder" +msgstr "Pasta" +msgid "Refresh" +msgstr "Atualizar" +msgid "(no subject)" +msgstr "(sem assunto)" +msgid "Back" +msgstr "Voltar" +msgid "Load external images" +msgstr "Carregar imagens externas" +msgid "[external image blocked]" +msgstr "[imagem externa bloqueada]" +msgid "Cc" +msgstr "Cc" +msgid "Bcc" +msgstr "Cco" +msgid "Subject" +msgstr "Assunto" +msgid "The message could not be sent." +msgstr "Não foi possível enviar a mensagem." +msgid "Invalid message fields." +msgstr "Campos da mensagem inválidos." + #: src/www/js/AddressBook.js:232 msgid "Add Contact" msgstr "Adicionar contato" @@ -870,3 +917,42 @@ msgstr "Adicionar como padrão" #: src/www/js/Widgets.js:1131 msgid "Set" msgstr "Configurar" + +msgid "Tasks" +msgstr "Tarefas" + +msgid "No open tasks." +msgstr "Nenhuma tarefa aberta." + +msgid "Disable" +msgstr "Desativar" + +msgid "Done" +msgstr "Concluído" + +msgid "Settings" +msgstr "Configurações" + +msgid "Personal settings" +msgstr "Configurações pessoais" + +msgid "Open tasks" +msgstr "Tarefas abertas" + +msgid "Filters" +msgstr "Filtros" + +msgid "External mail accounts" +msgstr "Contas de e-mail externas" + +msgid "Sender identities" +msgstr "Identidades do remetente" + +msgid "Two-factor authentication" +msgstr "Autenticação de dois fatores" + +msgid "App passwords" +msgstr "Senhas de aplicativos" + +msgid "Language and display" +msgstr "Idioma e aparência" diff --git a/src/agents/antispam/antispam.c b/src/agents/antispam/antispam.c index 2037fbf..9e9770a 100644 --- a/src/agents/antispam/antispam.c +++ b/src/agents/antispam/antispam.c @@ -125,7 +125,8 @@ ProcessConnection(void *clientp, Connection *conn) BONGO_ENVELOPE_NEXT(envelopeLine); } - SpamdCheck(&ASpam.spamd, client, client->qID, hasFlags, msgFlags); + if (ASpam.spamd.enabled) + SpamdCheck(&ASpam.spamd, client, client->qID, hasFlags, msgFlags); //blocked = SpamdCheck(&ASpam.spamd, client, client->qID, hasFlags, msgFlags); done: diff --git a/src/agents/antispam/antispam.h b/src/agents/antispam/antispam.h index 555da6a..f14ecdd 100644 --- a/src/agents/antispam/antispam.h +++ b/src/agents/antispam/antispam.h @@ -85,6 +85,8 @@ typedef struct { GArray *hostlist; BOOL enabled; unsigned long connectionTimeout; + unsigned long maximumMessageSize; + unsigned long maximumResponseSize; } SpamdConfig; typedef struct _ASpamGlobals { diff --git a/src/agents/antispam/spamd.c b/src/agents/antispam/spamd.c index 05b4186..029e204 100644 --- a/src/agents/antispam/spamd.c +++ b/src/agents/antispam/spamd.c @@ -68,6 +68,7 @@ SpamdCheck(SpamdConfig *spamd, ASpamClient *client, const char *queueID, BOOL ha int ccode; BOOL infected; BOOL success; + BOOL spam = FALSE; double score = 0; Connection *conn; /* Holds the current connection to spamd. */ @@ -95,6 +96,11 @@ SpamdCheck(SpamdConfig *spamd, ASpamClient *client, const char *queueID, BOOL ha size = atol(client->line); } + if (size > spamd->maximumMessageSize) { + Log(LOG_NOTICE, "(%s) Message size %lu exceeds spam scan limit %lu; delivering unscanned", + queueID, size, spamd->maximumMessageSize); + goto ErrConnFree; + } if ( (size != 0) && ((ccode = ConnWrite(conn, "PROCESS SPAMC/1.3\r\n", 19) ) != -1) && ((ccode = ConnWriteF(conn, "Content-length: %ld\r\n\r\n", size)) != -1) @@ -153,18 +159,24 @@ SpamdCheck(SpamdConfig *spamd, ASpamClient *client, const char *queueID, BOOL ha */ ptr = client->line + strlen("Content-length: "); size = atol(ptr); - if (size == 0) { + if (size == 0 || size > spamd->maximumResponseSize) { + Log(LOG_ERROR, "(%s) Invalid or oversized spamd response (%lu bytes)", + queueID, size); goto ErrConnFree; } } else if (strncmp(client->line, "Spam: ", strlen("Spam: ")) == 0) { /* Looking for the spam score. */ + spam = !strncasecmp(client->line + strlen("Spam: "), "True", 4); ptr = strchr(client->line, ';'); - score = atof(++ptr); - Log(LOG_INFO, "(%s) spam score of %f", queueID, score); + if (!ptr || !*++ptr) goto ErrConnFree; + score = strtod(ptr, &ptr2); + if (ptr2 == ptr) goto ErrConnFree; + Log(LOG_INFO, "(%s) spam=%s score=%.3f", queueID, + spam ? "yes" : "no", score); } } - infected = TRUE; + infected = spam; /* Create a new message with spam headers added before deleting this one. * * We will use the envelope that we read out in the beginning @@ -324,7 +336,10 @@ static BongoConfigItem SpamdHostList = { BONGO_JSON_STRING, NULL, &ASpam.spamd.h // ASpam.nmap.queue - defaults to Q_INCOMING static BongoConfigItem SpamdConfigSchema[] = { + { BONGO_JSON_BOOL, "o:enabled/b", &ASpam.spamd.enabled }, { BONGO_JSON_INT, "o:timeout/i", &ASpam.spamd.connectionTimeout }, + { BONGO_JSON_INT, "o:maximum_message_size/i", &ASpam.spamd.maximumMessageSize }, + { BONGO_JSON_INT, "o:maximum_response_size/i", &ASpam.spamd.maximumResponseSize }, { BONGO_JSON_ARRAY, "o:hosts/a", &SpamdHostList}, { BONGO_JSON_NULL, NULL, NULL }, }; @@ -338,11 +353,16 @@ SpamdReadConfiguration(SpamdConfig *spamd) /* Set up default spamd options. */ spamd->connectionTimeout = SPAMD_DEFAULT_CONNECTION_TIMEOUT; + spamd->enabled = TRUE; + spamd->maximumMessageSize = 50UL * 1024UL * 1024UL; + spamd->maximumResponseSize = 51UL * 1024UL * 1024UL; ConnAddressPoolStartup(&spamd->hosts, 0, 0); if (!ReadBongoConfiguration(SpamdConfigSchema, "antispam")) return FALSE; + if (!spamd->maximumMessageSize || + spamd->maximumResponseSize < spamd->maximumMessageSize) return FALSE; for (i=0; i < ASpam.spamd.hostlist->len; i++) { char *hostitem = g_array_index(ASpam.spamd.hostlist, char*, i); char *lHost = MemStrdup(hostitem); diff --git a/src/agents/avirus/avirus.c b/src/agents/avirus/avirus.c index b295a1e..763fdb5 100755 --- a/src/agents/avirus/avirus.c +++ b/src/agents/avirus/avirus.c @@ -59,93 +59,86 @@ AVirusClientFree(void *client) return; } -BOOL VirusCheck(AVirusClient *client, const char *queueID, BOOL hasFlags, unsigned long msgFlags, unsigned long senderIp, char *senderUserName) { +#define CLAMD_INSTREAM_CHUNK_SIZE 8192 + +AVirusScanResult VirusCheck(AVirusClient *client, const char *queueID, BOOL hasFlags, unsigned long msgFlags, unsigned long senderIp, char *senderUserName) { int ccode; long size; - BOOL infected = FALSE; + long remaining; + char chunk[CLAMD_INSTREAM_CHUNK_SIZE]; Connection *conn; + UNUSED_PARAMETER_REFACTOR(hasFlags); + UNUSED_PARAMETER_REFACTOR(msgFlags); + UNUSED_PARAMETER_REFACTOR(senderIp); + UNUSED_PARAMETER_REFACTOR(senderUserName); + conn = ConnAddressPoolConnect(&(AVirus.clamd.hosts), AVirus.clamd.connectionTimeout); - if (conn) { - Connection *data; - unsigned short port; - - ConnWrite(conn, "STREAM\r\n", strlen("STREAM\r\n")); - ConnFlush(conn); - ccode = ConnReadAnswer(conn, client->line, CONN_BUFSIZE); - - if (!ccode || strncmp(client->line, "PORT ", strlen("PORT ")) != 0 || (port = atoi(client->line + strlen("PORT "))) == 0) { - ConnFree(conn); - return -1; - } - - data = ConnAlloc(TRUE); - if (!data) { - ConnFree(conn); - return -1; - } - - memcpy(&data->socketAddress, &conn->socketAddress, sizeof(struct sockaddr_in)); - data->socketAddress.sin_port = htons(port); - if (ConnConnectEx(data, NULL, 0, NULL, client->conn->trace.destination) < 0) { - ConnFree(conn); - ConnFree(data); - return -1; - } - - size = 0; - if (((ccode = NMAPSendCommandF(client->conn, "QRETR %s MESSAGE\r\n", queueID)) != -1) - && ((ccode = NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, TRUE)) != -1) - && ccode == 2023) { - char *ptr; - - ptr = strchr (client->line, ' '); - if (ptr) { - *ptr = '\0'; - } - - size = atol(client->line); - } - - if (size == 0) { - ConnFree(conn); - ConnFree(data); - return -1; - } - - ccode = ConnReadToConn(client->conn, data, size); - - ConnFree(data); - - if ((ccode == -1) || ((ccode = NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, TRUE)) != 1000)) { - Log(LOG_DEBUG, "result: %d", ccode); - ConnFree(conn); - return -1; - } - - while ((ccode = ConnReadAnswer(conn, client->line, CONN_BUFSIZE)) != -1) { - char *ptr; - - ptr = strrchr(client->line, ' '); - if (XplStrCaseCmp(ptr + 1, "FOUND") == 0) { - *ptr = '\0'; - ptr = client->line + strlen("stream: "); - -#if 0 - if(client->foundViruses.used == client->foundViruses.allocated) { - client->foundViruses.names = MemRealloc(client->foundViruses.names, sizeof(char*) * (client->foundViruses.allocated + MIME_REALLOC_SIZE)); - } - client->foundViruses.names[client->foundViruses.used++] = MemStrdup(ptr); - - XplSafeIncrement(AVirus.stats.viruses); -#endif - infected = TRUE; - } - } - ConnFree(conn); + if (!conn) { + return AVIRUS_SCAN_ERROR; } - return infected; + size = 0; + if (((ccode = NMAPSendCommandF(client->conn, "QRETR %s MESSAGE\r\n", queueID)) == -1) + || ((ccode = NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, TRUE)) != 2023)) { + ConnFree(conn); + return AVIRUS_SCAN_ERROR; + } + { + char *end = NULL; + size = strtol(client->line, &end, 10); + if (end == client->line || size <= 0) { + ConnFree(conn); + return AVIRUS_SCAN_ERROR; + } + } + + if (ConnWrite(conn, "nINSTREAM\n", sizeof("nINSTREAM\n") - 1) == -1) { + ConnFree(conn); + return AVIRUS_SCAN_ERROR; + } + + remaining = size; + while (remaining > 0) { + int count = remaining > (long)sizeof(chunk) ? (int)sizeof(chunk) : (int)remaining; + uint32_t networkCount = htonl((uint32_t)count); + + if (ConnReadCount(client->conn, chunk, count) != count + || ConnWrite(conn, (const char *)&networkCount, sizeof(networkCount)) == -1 + || ConnWrite(conn, chunk, count) == -1) { + ConnFree(conn); + return AVIRUS_SCAN_ERROR; + } + remaining -= count; + } + { + uint32_t endMarker = 0; + if (ConnWrite(conn, (const char *)&endMarker, sizeof(endMarker)) == -1 + || ConnFlush(conn) == -1) { + ConnFree(conn); + return AVIRUS_SCAN_ERROR; + } + } + + if (NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, TRUE) != 1000) { + ConnFree(conn); + return AVIRUS_SCAN_ERROR; + } + + ccode = ConnReadAnswer(conn, client->line, CONN_BUFSIZE); + ConnFree(conn); + if (ccode <= 0) { + return AVIRUS_SCAN_ERROR; + } + if (strstr(client->line, " FOUND") != NULL) { + return AVIRUS_SCAN_INFECTED; + } + if (strstr(client->line, " OK") != NULL) { + return AVIRUS_SCAN_CLEAN; + } + + Log(LOG_ERROR, "ClamAV scan failed for queue %s: %s", queueID, client->line); + return AVIRUS_SCAN_ERROR; } void BounceToPostmaster(AVirusClient *client, char *senderUserName) { @@ -268,16 +261,16 @@ static __inline int ProcessConnection(void *clientp, Connection *conn) { AVirusClient *client = clientp; - unsigned char *envelopeLine; + char *envelopeLine; BOOL hasFlags = FALSE; unsigned long msgFlags = 0; int ccode; unsigned long source = 0; - unsigned char *ptr; + char *ptr; char *ptr2; char *senderUserName = NULL; - BOOL blocked = FALSE; + AVirusScanResult scanResult; client->conn = conn; ccode = BongoQueueAgentHandshake(client->conn, client->line, client->qID, &client->envelopeLength, &client->messageLength); @@ -309,9 +302,9 @@ ProcessConnection(void *clientp, Connection *conn) BONGO_ENVELOPE_NEXT(envelopeLine); } - blocked = VirusCheck(client, client->qID, hasFlags, msgFlags, source, senderUserName); + scanResult = VirusCheck(client, client->qID, hasFlags, msgFlags, source, senderUserName); - if (blocked == TRUE) { + if (scanResult == AVIRUS_SCAN_INFECTED) { /* we found a virus of some kind. drop the mail */ Log(LOG_DEBUG, "Queue ID %s Infected and blocked", client->qID); ConnWriteF(client->conn, "QDELE %s\r\n", client->qID); @@ -330,6 +323,11 @@ ProcessConnection(void *clientp, Connection *conn) /* create the bounce to the postmaster */ BounceToPostmaster(client, senderUserName); } + } else if (scanResult == AVIRUS_SCAN_ERROR) { + /* Do not acknowledge an unscanned message. Closing the queue-agent + * transaction leaves it queued so a later worker can retry it. */ + Log(LOG_ERROR, "ClamAV could not scan queue ID %s; message remains queued for retry", client->qID); + ccode = -1; } else { Log(LOG_DEBUG, "Queue ID %s clean", client->qID); NMAPSendCommand(client->conn, "QDONE\r\n", 7); @@ -345,7 +343,7 @@ ProcessConnection(void *clientp, Connection *conn) MemFree(senderUserName); } - return(0); + return ccode == -1 ? -1 : 0; } static BongoConfigItem ClamHostList = { @@ -424,7 +422,7 @@ ReadConfiguration(void) { for (i=0; i < AVirus.clamd.hostlist->len; i++) { char *hostitem = g_array_index(AVirus.clamd.hostlist, char*, i); char *lHost = MemStrdup(hostitem); - char *host; + char *host = NULL; int port=AVIRUS_DEFAULT_PORT, weight=AVIRUS_DEFAULT_WEIGHT; ParseHost(lHost, &host, &port, &weight); ConnAddressPoolAddHost(&AVirus.clamd.hosts, host, port, weight); @@ -493,6 +491,7 @@ XplServiceCode(SignalHandler) static void AntiVirusServer(void *ignored) { + UNUSED_PARAMETER(ignored); int minThreads, maxThreads, minSleep; BongoQueueAgentGetThreadPoolParameters(&AVirus.agent, &minThreads, &maxThreads, &minSleep); @@ -514,6 +513,8 @@ AntiVirusServer(void *ignored) { int XplServiceMain(int argc, char *argv[]) { + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); int ccode; int startupOpts; diff --git a/src/agents/avirus/avirus.h b/src/agents/avirus/avirus.h index ac8b097..2febded 100755 --- a/src/agents/avirus/avirus.h +++ b/src/agents/avirus/avirus.h @@ -65,19 +65,25 @@ typedef enum _AVirusClientFlags { AVIRUS_CLIENT_FLAG_EXITING = (1 << 2) } AVirusClientFlags; +typedef enum { + AVIRUS_SCAN_ERROR = -1, + AVIRUS_SCAN_CLEAN = 0, + AVIRUS_SCAN_INFECTED = 1 +} AVirusScanResult; + typedef struct { AVirusClientFlags flags; Connection *conn; - unsigned int envelopeLength; - unsigned int messageLength; - unsigned int envelopeLines; + int envelopeLength; + int messageLength; + int envelopeLines; char qID[16]; /* holds the queueID */ - unsigned char *envelope; - unsigned char line[CONN_BUFSIZE + 1]; - unsigned char command[CONN_BUFSIZE + 1]; + char *envelope; + char line[CONN_BUFSIZE + 1]; + char command[CONN_BUFSIZE + 1]; } AVirusClient; @@ -132,7 +138,7 @@ extern AVirusGlobals AVirus; /* spamd.c */ void BounceToSender(AVirusClient *client, char *senderUserName); void BounceToPostmaster(AVirusClient *client, char *senderUserName); -BOOL VirusCheck(AVirusClient *client, const char *queueID, BOOL hasFlags, unsigned long msgFlags, unsigned long senderIp, char *senderUserName); +AVirusScanResult VirusCheck(AVirusClient *client, const char *queueID, BOOL hasFlags, unsigned long msgFlags, unsigned long senderIp, char *senderUserName); void VirusShutdown(); void VirusStartup(); BOOL VirusReadConfiguration(BongoJsonNode *node); diff --git a/src/agents/collector/CMakeLists.txt b/src/agents/collector/CMakeLists.txt new file mode 100644 index 0000000..e180ab1 --- /dev/null +++ b/src/agents/collector/CMakeLists.txt @@ -0,0 +1,32 @@ +StrictCompile() + +set(EXTERNAL_ACCOUNTS_SQL_C ${CMAKE_CURRENT_BINARY_DIR}/external-accounts-sql.c) +add_custom_command( + OUTPUT ${EXTERNAL_ACCOUNTS_SQL_C} + COMMAND ${CMAKE_COMMAND} + -DINPUT=${CMAKE_CURRENT_SOURCE_DIR}/external-accounts.sql + -DOUTPUT=${EXTERNAL_ACCOUNTS_SQL_C} + -DSYMBOL=sql_external_accounts + -P ${CMAKE_SOURCE_DIR}/cmake/EmbedFile.cmake + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/external-accounts.sql + ${CMAKE_SOURCE_DIR}/cmake/EmbedFile.cmake + VERBATIM +) + +add_executable(bongocollector + collector.c + external_accounts.c + ${EXTERNAL_ACCOUNTS_SQL_C} +) + +target_link_libraries(bongocollector + bongoxpl + bongoconnio + bongoutil + bongostreamio + bongomsgapi + ${CURL_LIBRARIES} + ${SQLITE_LIBRARIES} +) + +install(TARGETS bongocollector DESTINATION ${SBIN_INSTALL_DIR}) diff --git a/src/agents/collector/collector.c b/src/agents/collector/collector.c index f1a4ac4..0468711 100644 --- a/src/agents/collector/collector.c +++ b/src/agents/collector/collector.c @@ -35,27 +35,48 @@ #include #include "collector.h" +#include "external_accounts.h" #define TOK_ARR_SZ 10 -CollectorGlobals Collector = {{0,}, 0, }; +CollectorGlobals Collector = {0}; static void CollectorLoop(void *ignored) { + sqlite3 *accounts = NULL; + time_t lastCalendarCollection = 0; + + (void) ignored; XplRenameThread(XplGetThreadID(), AGENT_DN); while (Collector.agent.state < BONGO_AGENT_STATE_STOPPING) { - MsgCollectAllUsers(); + time_t now = time(NULL); + int due; + + if (accounts == NULL) accounts = CollectorAccountsOpen(); + if (accounts != NULL) { + due = CollectorAccountsDueCount(accounts, (long) now); + if (due < 0) { + Log(LOG_ERROR, "Unable to query due external mail accounts"); + } else if (due > 0) { + Log(LOG_DEBUG, "%d external mail account(s) are due for collection", due); + } + } + if (lastCalendarCollection == 0 || now - lastCalendarCollection >= 6 * 60 * 60) { + MsgCollectAllUsers(); + lastCalendarCollection = now; + } if (Collector.agent.state >= BONGO_AGENT_STATE_STOPPING) { break; } - /* Refresh every 6 hours for now */ - sleep(60*60*6); + sleep(60); } + CollectorAccountsClose(accounts); + #if VERBOSE /* Shutting down */ XplConsolePrintf(AGENT_NAME ": Shutting down.\r\n"); @@ -82,6 +103,10 @@ XplServiceMain(int argc, char *argv[]) int ccode; int startupOpts; + (void) argc; + (void) argv; + (void) GlobalConfig; + if (XplSetRealUser(MsgGetUnprivilegedUser()) < 0) { XplConsolePrintf(AGENT_NAME ": Could not drop to unprivileged user '%s'\r\n" AGENT_NAME ": exiting.\n", MsgGetUnprivilegedUser()); return -1; diff --git a/src/agents/collector/external-accounts.sql b/src/agents/collector/external-accounts.sql new file mode 100644 index 0000000..c086a50 --- /dev/null +++ b/src/agents/collector/external-accounts.sql @@ -0,0 +1,70 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS external_accounts ( + id INTEGER PRIMARY KEY, + owner TEXT NOT NULL, + label TEXT NOT NULL, + email_address TEXT NOT NULL, + enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)), + verified_at INTEGER, + inbound_protocol TEXT NOT NULL DEFAULT 'none' + CHECK (inbound_protocol IN ('none', 'imap', 'pop3')), + inbound_host TEXT, + inbound_port INTEGER CHECK (inbound_port BETWEEN 1 AND 65535), + inbound_tls TEXT NOT NULL DEFAULT 'implicit' + CHECK (inbound_tls IN ('implicit', 'starttls')), + inbound_username TEXT, + inbound_secret_id TEXT, + poll_interval INTEGER NOT NULL DEFAULT 300 CHECK (poll_interval >= 60), + destination_folder TEXT NOT NULL DEFAULT 'INBOX', + delete_policy TEXT NOT NULL DEFAULT 'never' + CHECK (delete_policy IN ('never', 'after_import', 'after_days')), + delete_after_days INTEGER CHECK (delete_after_days > 0), + outbound_mode TEXT NOT NULL DEFAULT 'smtp' + CHECK (outbound_mode IN ('direct', 'smtp', 'proton_bridge')), + outbound_host TEXT, + outbound_port INTEGER CHECK (outbound_port BETWEEN 1 AND 65535), + outbound_tls TEXT NOT NULL DEFAULT 'starttls' + CHECK (outbound_tls IN ('implicit', 'starttls')), + outbound_username TEXT, + outbound_secret_id TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + last_checked_at INTEGER, + last_success_at INTEGER, + last_error TEXT, + UNIQUE (owner, email_address), + CHECK (delete_policy != 'after_days' OR delete_after_days IS NOT NULL), + CHECK (inbound_protocol = 'none' OR + (inbound_host IS NOT NULL AND inbound_port IS NOT NULL AND + inbound_username IS NOT NULL AND inbound_secret_id IS NOT NULL)), + CHECK (outbound_mode = 'direct' OR + (outbound_host IS NOT NULL AND outbound_port IS NOT NULL AND + outbound_username IS NOT NULL AND outbound_secret_id IS NOT NULL)) +); + +CREATE INDEX IF NOT EXISTS external_accounts_due + ON external_accounts (enabled, last_checked_at, poll_interval); + +CREATE TABLE IF NOT EXISTS external_account_messages ( + account_id INTEGER NOT NULL REFERENCES external_accounts(id) ON DELETE CASCADE, + remote_mailbox TEXT NOT NULL, + remote_uidvalidity TEXT NOT NULL DEFAULT '', + remote_uid TEXT NOT NULL, + message_id TEXT, + imported_at INTEGER NOT NULL, + delete_after INTEGER, + deleted_remote_at INTEGER, + PRIMARY KEY (account_id, remote_mailbox, remote_uidvalidity, remote_uid) +); + +CREATE INDEX IF NOT EXISTS external_account_deletions + ON external_account_messages (account_id, delete_after, deleted_remote_at); + +CREATE TABLE IF NOT EXISTS external_account_identities ( + account_id INTEGER NOT NULL REFERENCES external_accounts(id) ON DELETE CASCADE, + email_address TEXT NOT NULL, + verified_at INTEGER, + is_default INTEGER NOT NULL DEFAULT 0 CHECK (is_default IN (0, 1)), + PRIMARY KEY (account_id, email_address) +); diff --git a/src/agents/collector/external_accounts.c b/src/agents/collector/external_accounts.c new file mode 100644 index 0000000..98cc10b --- /dev/null +++ b/src/agents/collector/external_accounts.c @@ -0,0 +1,76 @@ +#include + +#include +#include +#include + +#define LOGGERNAME "collector-accounts" +#include +#include + +#include "external_accounts.h" + +extern unsigned char sql_external_accounts[]; + +sqlite3 * +CollectorAccountsOpen(void) +{ + char path[XPL_MAX_PATH]; + char *error = NULL; + sqlite3 *database = NULL; + int result; + + if (snprintf(path, sizeof(path), "%s/%s", XPL_DEFAULT_DBF_DIR, + "external-accounts.sqlite") >= (int) sizeof(path)) return NULL; + result = sqlite3_open_v2(path, &database, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | + SQLITE_OPEN_FULLMUTEX, NULL); + if (result != SQLITE_OK) { + Log(LOG_ERROR, "Cannot open external account database %s: %s", path, + database != NULL ? sqlite3_errmsg(database) : "unknown error"); + if (database != NULL) sqlite3_close(database); + return NULL; + } + sqlite3_busy_timeout(database, 5000); + if (sqlite3_exec(database, "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;", + NULL, NULL, &error) != SQLITE_OK || + sqlite3_exec(database, (const char *) sql_external_accounts, + NULL, NULL, &error) != SQLITE_OK) { + Log(LOG_ERROR, "Cannot initialize external account database: %s", + error != NULL ? error : sqlite3_errmsg(database)); + sqlite3_free(error); + sqlite3_close(database); + return NULL; + } + if (chmod(path, S_IRUSR | S_IWUSR) != 0) { + Log(LOG_ERROR, "Cannot protect external account database %s: %s", path, + strerror(errno)); + sqlite3_close(database); + return NULL; + } + return database; +} + +void +CollectorAccountsClose(sqlite3 *database) +{ + if (database != NULL) sqlite3_close(database); +} + +int +CollectorAccountsDueCount(sqlite3 *database, long now) +{ + sqlite3_stmt *statement = NULL; + int count = -1; + const char query[] = + "SELECT count(*) FROM external_accounts WHERE enabled = 1 AND " + "inbound_protocol != 'none' AND " + "(last_checked_at IS NULL OR last_checked_at + poll_interval <= ?)"; + + if (database == NULL || + sqlite3_prepare_v2(database, query, -1, &statement, NULL) != SQLITE_OK) return -1; + sqlite3_bind_int64(statement, 1, (sqlite3_int64) now); + if (sqlite3_step(statement) == SQLITE_ROW) count = sqlite3_column_int(statement, 0); + sqlite3_finalize(statement); + return count; +} diff --git a/src/agents/collector/external_accounts.h b/src/agents/collector/external_accounts.h new file mode 100644 index 0000000..5f4d9cd --- /dev/null +++ b/src/agents/collector/external_accounts.h @@ -0,0 +1,10 @@ +#ifndef BONGO_EXTERNAL_ACCOUNTS_H +#define BONGO_EXTERNAL_ACCOUNTS_H + +#include + +sqlite3 *CollectorAccountsOpen(void); +void CollectorAccountsClose(sqlite3 *database); +int CollectorAccountsDueCount(sqlite3 *database, long now); + +#endif diff --git a/src/agents/collector/provider-presets.json b/src/agents/collector/provider-presets.json new file mode 100644 index 0000000..5bfad08 --- /dev/null +++ b/src/agents/collector/provider-presets.json @@ -0,0 +1,144 @@ +{ + "schema_version": 1, + "reviewed": "2026-07-16", + "providers": [ + { + "id": "gmx", + "name": "GMX", + "domains": ["gmx.at", "gmx.de", "gmx.net", "gmx.com"], + "imap": {"host": "imap.gmx.net", "port": 993, "tls": "implicit"}, + "pop3": {"host": "pop.gmx.net", "port": 995, "tls": "implicit"}, + "smtp": {"host": "mail.gmx.net", "port": 587, "tls": "starttls", "alternative_port": 465}, + "authentication": ["password"], + "username": "email_address", + "setup_note": "enable_pop_imap", + "source": "https://hilfe.gmx.net/pop-imap/imap/imap-serverdaten.html" + }, + { + "id": "webde", + "name": "WEB.DE", + "domains": ["web.de"], + "imap": {"host": "imap.web.de", "port": 993, "tls": "implicit"}, + "pop3": {"host": "pop3.web.de", "port": 995, "tls": "implicit"}, + "smtp": {"host": "smtp.web.de", "port": 587, "tls": "starttls", "alternative_port": 465}, + "authentication": ["password"], + "username": "email_address", + "setup_note": "enable_pop_imap", + "source": "https://hilfe.web.de/pop-imap/imap/imap-serverdaten.html" + }, + { + "id": "gmail", + "name": "Gmail", + "domains": ["gmail.com", "googlemail.com"], + "imap": {"host": "imap.gmail.com", "port": 993, "tls": "implicit"}, + "pop3": {"host": "pop.gmail.com", "port": 995, "tls": "implicit"}, + "smtp": {"host": "smtp.gmail.com", "port": 587, "tls": "starttls", "alternative_port": 465}, + "authentication": ["oauth2", "app_password"], + "preferred_authentication": "oauth2", + "username": "email_address", + "source": "https://support.google.com/mail/answer/7126229" + }, + { + "id": "outlook", + "name": "Outlook.com / Hotmail / Live", + "domains": ["outlook.com", "hotmail.com", "hotmail.de", "live.com", "live.at", "live.de", "msn.com"], + "imap": {"host": "outlook.office365.com", "port": 993, "tls": "implicit"}, + "pop3": {"host": "outlook.office365.com", "port": 995, "tls": "implicit"}, + "smtp": {"host": "smtp-mail.outlook.com", "port": 587, "tls": "starttls"}, + "authentication": ["oauth2"], + "preferred_authentication": "oauth2", + "username": "email_address", + "setup_note": "enable_pop_imap", + "source": "https://support.microsoft.com/en-US/Outlook/pop-imap-and-smtp-settings-for-outlook-com" + }, + { + "id": "yahoo", + "name": "Yahoo Mail", + "domains": ["yahoo.com", "yahoo.de", "ymail.com", "rocketmail.com"], + "imap": {"host": "imap.mail.yahoo.com", "port": 993, "tls": "implicit"}, + "smtp": {"host": "smtp.mail.yahoo.com", "port": 465, "tls": "implicit", "alternative_port": 587}, + "authentication": ["app_password"], + "username": "email_address", + "source": "https://help.yahoo.com/kb/imap-internet-message-access-protocol-sln4075.html" + }, + { + "id": "icloud", + "name": "iCloud Mail", + "domains": ["icloud.com", "me.com", "mac.com"], + "imap": {"host": "imap.mail.me.com", "port": 993, "tls": "implicit"}, + "smtp": {"host": "smtp.mail.me.com", "port": 587, "tls": "starttls"}, + "authentication": ["app_password"], + "username": "provider_specific", + "source": "https://support.apple.com/en-ie/102525" + }, + { + "id": "aol", + "name": "AOL Mail", + "domains": ["aol.com"], + "imap": {"host": "imap.aol.com", "port": 993, "tls": "implicit"}, + "pop3": {"host": "pop.aol.com", "port": 995, "tls": "implicit"}, + "smtp": {"host": "smtp.aol.com", "port": 465, "tls": "implicit"}, + "authentication": ["app_password"], + "username": "email_address", + "source": "https://help.aol.com/articles/how-do-i-use-other-email-applications-to-send-and-receive-my-aol-mail" + }, + { + "id": "zoho", + "name": "Zoho Mail", + "domains": ["zoho.com", "zohomail.com"], + "imap": {"host": "imap.zoho.com", "port": 993, "tls": "implicit"}, + "smtp": {"host": "smtp.zoho.com", "port": 587, "tls": "starttls", "alternative_port": 465}, + "authentication": ["password", "app_password"], + "username": "email_address", + "setup_note": "availability_depends_on_region_and_plan", + "source": "https://www.zoho.com/mail/help/imap-access.html" + }, + { + "id": "freenet", + "name": "freenet Mail", + "domains": ["freenet.de"], + "imap": {"host": "mx.freenet.de", "port": 993, "tls": "implicit"}, + "pop3": {"host": "mx.freenet.de", "port": 995, "tls": "implicit"}, + "smtp": {"host": "mx.freenet.de", "port": 587, "tls": "starttls", "alternative_port": 465}, + "authentication": ["password"], + "username": "email_address", + "setup_note": "enable_pop_imap", + "source": "https://kundenservice.freenet.de/faq/e-mail-client-mail/einstellungen-serverdaten-fuer-alle-e-mail-programme/" + }, + { + "id": "mailcom", + "name": "mail.com", + "domains": ["mail.com", "email.com"], + "imap": {"host": "imap.mail.com", "port": 993, "tls": "implicit"}, + "smtp": {"host": "smtp.mail.com", "port": 587, "tls": "starttls", "alternative_port": 465}, + "authentication": ["password"], + "username": "email_address", + "setup_note": "imap_may_require_premium", + "source": "https://support.mail.com/premium/imap/server.html" + }, + { + "id": "proton", + "name": "Proton Mail", + "domains": ["proton.me", "protonmail.com", "protonmail.ch", "pm.me"], + "special_mode": "proton_bridge_or_smtp_token", + "authentication": ["bridge_password", "smtp_token"], + "setup_note": "paid_plan_or_custom_domain_restrictions", + "source": "https://proton.me/support/imap-smtp-and-pop3-setup" + }, + { + "id": "manual", + "name": "Other provider", + "domains": [], + "special_mode": "manual", + "authentication": ["password", "app_password", "oauth2"] + } + ], + "unsupported": [ + { + "id": "tuta", + "name": "Tuta Mail", + "reason": "no_imap_pop_smtp_access", + "source": "https://tuta.com/blog/desktop-clients-tutanota" + } + ] +} diff --git a/src/agents/imap/CMakeLists.txt b/src/agents/imap/CMakeLists.txt index d2bb5e2..2be35b0 100644 --- a/src/agents/imap/CMakeLists.txt +++ b/src/agents/imap/CMakeLists.txt @@ -18,6 +18,7 @@ target_link_libraries(bongoimap bongojson bongomsgapi bongostreamio + bongosasl ) install(TARGETS bongoimap DESTINATION ${SBIN_INSTALL_DIR}) diff --git a/src/agents/imap/copy.c b/src/agents/imap/copy.c index c0c207e..08ba8e7 100644 --- a/src/agents/imap/copy.c +++ b/src/agents/imap/copy.c @@ -34,7 +34,7 @@ CopyMessageRangeToTarget(ImapSession *session, MessageInformation *message, count = rangeCount; do { - if (NMAPSendCommandF(storeConn, "COPY %llx %llx\r\n", message->guid, target) != -1) { + if (NMAPSendCommandF(storeConn, "COPY %" PRIx64 " %" PRIx64 "\r\n", message->guid, target) != -1) { if ((ccode = NMAPReadResponse(storeConn, NULL, 0, 0)) == 1000) { count--; if (count > 0) { @@ -86,8 +86,8 @@ __inline static long HandleCopy(ImapSession *session, BOOL ByUID) { FolderInformation *targetFolder; - unsigned char *messageSet; - unsigned char *ptr; + char *messageSet; + char *ptr; FolderPath targetPath; long ccode; char *ptr2; diff --git a/src/agents/imap/event.c b/src/agents/imap/event.c index 06a0b8a..85ebf2b 100644 --- a/src/agents/imap/event.c +++ b/src/agents/imap/event.c @@ -291,7 +291,7 @@ MessageListAddNewMessages(Connection *storeConn, OpenedFolder *folder, NewEvent long ccode; long result; long headerSize; - unsigned char reply[1024]; + char reply[1024]; do { result = STATUS_CONTINUE; @@ -377,6 +377,8 @@ MarkPurgedMessages(Connection *clientConn, StoreEvents *events, OpenedFolder *fo unsigned long messageId; unsigned long count; + UNUSED_PARAMETER(clientConn); + purge = &(events->purge[0]); count = events->purgeCount; @@ -408,12 +410,18 @@ SendRememberedEvents(Connection *clientConn, Connection *storeConn, OpenedFolder if (typesAllowed & STORE_EVENT_PURGE) { sequenceChanged = TRUE; ccode = SendPurgeEvents(clientConn, events, selectedFolder); + if (ccode != STATUS_CONTINUE) { + return ccode; + } events->purgeCount = 0; events->remembered &= ~STORE_EVENT_PURGE; } else { /* we can't send anything to the client, but we do need to update */ /* the messageList so commands do not try to access these messages */ ccode = MarkPurgedMessages(clientConn, events, selectedFolder); + if (ccode != STATUS_CONTINUE) { + return ccode; + } } } @@ -449,7 +457,7 @@ GenerateEvents(OpenedFolder *selectedFolder, OpenedFolder *updatedFolder) if (oldListMessagesLeft > 0) { if (newListMessage->uid == oldListMessage->uid) { /* Message in both lists */ - if (newListMessage->flags != newListMessage->flags) { + if (newListMessage->flags != oldListMessage->flags) { RememberFlagEvent(&(selectedFolder->events), newListMessage->uid, newListMessage->flags); } oldListMessage++; diff --git a/src/agents/imap/fetch.c b/src/agents/imap/fetch.c index f11f437..541444d 100644 --- a/src/agents/imap/fetch.c +++ b/src/agents/imap/fetch.c @@ -1,20 +1,20 @@ /**************************************************************************** * * Copyright (c) 2001 Novell, Inc. All Rights Reserved. - * + * * This program is free software; you can redistribute it and/or * modify it under the terms of version 2 of the GNU General Public License * as published by the Free Software Foundation. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program; if not, contact Novell, Inc. - * - * To contact Novell about this file by physical or electronic mail, you + * + * To contact Novell about this file by physical or electronic mail, you * may find current contact information at www.novell.com. * ****************************************************************************/ @@ -34,13 +34,13 @@ BongoKeywordIndex *FetchFlagsAttIndex = NULL; BongoKeywordIndex *FetchFlagsSoloIndex = NULL; BongoKeywordIndex *FetchFlagsSectionIndex = NULL; -unsigned char TopLevelMime[] = "Content-Type: message/rfc822\r\nContent-Tranfer-Encoding: 7bit\r\n"; +char TopLevelMime[] = "Content-Type: message/rfc822\r\nContent-Tranfer-Encoding: 7bit\r\n"; unsigned long TopLevelMimeLen = sizeof(TopLevelMime) - 1; static BOOL -FindRFC822Header(unsigned char *Header, unsigned char *SearchFor, unsigned char **Buffer, int BufSize) +FindRFC822Header(char *Header, char *SearchFor, char **Buffer, int BufSize) { - unsigned char *NL, *ptr, *ptr2, *start=Header, *ptr3; + char *NL, *ptr, *ptr2, *ptr3; BOOL KeepGoing=TRUE; int len; @@ -63,7 +63,7 @@ FindRFC822Header(unsigned char *Header, unsigned char *SearchFor, unsigned char ptr2=strchr(ptr, ':'); if (ptr2) { *ptr2='\0'; - + if (XplStrCaseCmp(ptr, SearchFor)==0) { ptr3=ptr2+1; while (IsWhiteSpace(*ptr3)) @@ -100,30 +100,31 @@ FindRFC822Header(unsigned char *Header, unsigned char *SearchFor, unsigned char KeepGoing=FALSE; } - start=ptr; } return(FALSE); } static long -SendRFC822Address(ImapSession *session, unsigned char *Address, unsigned char *Answer) +SendRFC822Address(ImapSession *session, char *Address, char *Answer) { long ccode = 0; RFC822AddressStruct *Current; RFC822AddressStruct *Rfc822AddressList = NULL; unsigned long Len; + UNUSED_PARAMETER(Answer); + RFC822ParseAddressList(&Rfc822AddressList, Address, ".MissingHostName."); if (Rfc822AddressList) { if (ConnWrite(session->client.conn, "(", 1) != -1) { - Current = Rfc822AddressList; + Current = Rfc822AddressList; while (Current) { if (Current->Personal) { Len = strlen(Current->Personal); - if ((ccode = ConnWriteF(session->client.conn, "({%lu}\r\n", Len) != -1) && - (ccode = ConnWrite(session->client.conn, Current->Personal, Len) != -1) && + if ((ccode = ConnWriteF(session->client.conn, "({%lu}\r\n", Len) != -1) && + (ccode = ConnWrite(session->client.conn, Current->Personal, Len) != -1) && (ccode = ConnWrite(session->client.conn, " ", 1)) != -1) { ; } else { @@ -138,8 +139,8 @@ SendRFC822Address(ImapSession *session, unsigned char *Address, unsigned char *A } if (Current->ADL) { - if (((ccode = ConnWrite(session->client.conn, "\"", 1)) != -1) && - ((ccode = ConnWrite(session->client.conn, Current->ADL, strlen(Current->ADL))) != -1) && + if (((ccode = ConnWrite(session->client.conn, "\"", 1)) != -1) && + ((ccode = ConnWrite(session->client.conn, Current->ADL, strlen(Current->ADL))) != -1) && ((ccode = ConnWrite(session->client.conn, "\" ", 2)) != -1)) { ; } else { @@ -154,8 +155,8 @@ SendRFC822Address(ImapSession *session, unsigned char *Address, unsigned char *A } if (Current->Mailbox) { - if (((ccode = ConnWrite(session->client.conn, "\"", 1)) != -1) && - ((ccode = ConnWrite(session->client.conn, Current->Mailbox, strlen(Current->Mailbox))) != -1) && + if (((ccode = ConnWrite(session->client.conn, "\"", 1)) != -1) && + ((ccode = ConnWrite(session->client.conn, Current->Mailbox, strlen(Current->Mailbox))) != -1) && ((ccode = ConnWrite(session->client.conn, "\" ", 2)) != -1)) { ; } else { @@ -170,8 +171,8 @@ SendRFC822Address(ImapSession *session, unsigned char *Address, unsigned char *A } if (Current->Host) { - if (((ccode = ConnWrite(session->client.conn, "\"", 1)) != -1) && - ((ccode = ConnWrite(session->client.conn, Current->Host, strlen(Current->Host))) != -1) && + if (((ccode = ConnWrite(session->client.conn, "\"", 1)) != -1) && + ((ccode = ConnWrite(session->client.conn, Current->Host, strlen(Current->Host))) != -1) && ((ccode = ConnWrite(session->client.conn, "\")", 2)) != -1)) { ; } else { @@ -192,7 +193,7 @@ SendRFC822Address(ImapSession *session, unsigned char *Address, unsigned char *A if (ConnWrite(session->client.conn, ") ", 2) != -1) { return(STATUS_CONTINUE); } - + } return(STATUS_ABORT); } @@ -208,9 +209,9 @@ SendRFC822Address(ImapSession *session, unsigned char *Address, unsigned char *A } static void -MakeRFC822Header(unsigned char *Header, unsigned long *HSize) +MakeRFC822Header(char *Header, unsigned long *HSize) { - unsigned char *NL, *ptr, *ptr2, *start=Header; + char *NL, *ptr, *ptr2, *start=Header; BOOL StripLine, KeepGoing=TRUE; ptr=Header; @@ -256,18 +257,18 @@ MakeRFC822Header(unsigned char *Header, unsigned long *HSize) *HSize=strlen(Header); } -__inline static unsigned char * -GetSubHeader(unsigned char *header, unsigned long headerSize, unsigned char **fields, unsigned long fieldCount, BOOL not, unsigned long *newHeaderLen) +__inline static char * +GetSubHeader(char *header, unsigned long headerSize, char **fields, unsigned long fieldCount, BOOL not, unsigned long *newHeaderLen) { - unsigned char *newHeader; - unsigned char *start; - unsigned char *colon; - unsigned char *newLine; - unsigned char *dest; + char *newHeader; + char *start; + char *colon; + char *newLine; + char *dest; unsigned long i; unsigned long lineLen; BOOL keepLine; - + newHeader = MemMalloc(headerSize); if (newHeader) { start = header; @@ -289,7 +290,7 @@ GetSubHeader(unsigned char *header, unsigned long headerSize, unsigned char **fi keepLine = not; break; } - + /* this line is a match */ keepLine = !not; break; @@ -341,9 +342,9 @@ GetSubHeader(unsigned char *header, unsigned long headerSize, unsigned char **fi } __inline static void -ParseMIMEDLine(unsigned char *MIME, unsigned char *Type, unsigned long TypeSize, unsigned char *SubType, unsigned long SubTypeSize, unsigned char *Charset, unsigned long CharsetSize, unsigned char *Encoding, unsigned long EncodingSize, unsigned char *Name, unsigned long NameSize, long *headStart, unsigned long *headSize, unsigned long *StartPos, unsigned long *Size, unsigned long *HeaderSize, unsigned long *Lines) +ParseMIMEDLine(char *MIME, char *Type, unsigned long TypeSize, char *SubType, unsigned long SubTypeSize, char *Charset, unsigned long CharsetSize, char *Encoding, unsigned long EncodingSize, char *Name, unsigned long NameSize, long *headStart, unsigned long *headSize, unsigned long *StartPos, unsigned long *Size, unsigned long *HeaderSize, unsigned long *Lines) { - unsigned char *p, *p2; + char *p, *p2; /* Type */ p2 = strchr(MIME, ' '); if (p2) { @@ -483,7 +484,7 @@ FreeFetchResourcesSuccess(FetchStruct *FetchRequest) if (!(FetchRequest->hasAllocated & ALLOCATED_FLAGS)) { return; } - + MemFree(FetchRequest->flag); return; } @@ -497,7 +498,7 @@ FreeFetchResourcesFailure(FetchStruct *FetchRequest) } __inline static long -ReadBodyPart(ImapSession *session, unsigned char **Part, unsigned long ID, unsigned long Start, unsigned long Size) +ReadBodyPart(ImapSession *session, char **Part, unsigned long ID, unsigned long Start, unsigned long Size) { size_t count; long ccode; @@ -506,19 +507,19 @@ ReadBodyPart(ImapSession *session, unsigned char **Part, unsigned long ID, unsig if (*Part) { if (Size) { /* Request the body part*/ - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", session->folder.selected.message[ID].guid, Start, Size) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", session->folder.selected.message[ID].guid, Start, Size) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (NMAPReadCount(session->store.conn, *Part, count) == (int)count) { if (NMAPReadCrLf(session->store.conn) == 2) { return(STATUS_CONTINUE); } } - + MemFree(*Part); *Part = NULL; return(STATUS_ABORT); } - + MemFree(*Part); *Part = NULL; return(CheckForNMAPCommError(ccode)); @@ -537,9 +538,9 @@ ReadBodyPart(ImapSession *session, unsigned char **Part, unsigned long ID, unsig } __inline static long -SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) +SendEnvelope(ImapSession *session, char *Header, char *Answer) { - unsigned char *component = NULL; + char *component = NULL; long ccode; unsigned long len; @@ -569,8 +570,8 @@ SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) /* now env_subject */ if (FindRFC822Header(Header, "Subject", &component, 0)) { len=strlen(component); - if ((ConnWriteF(session->client.conn, "{%d}\r\n", (int)len) != -1) && - (ConnWrite(session->client.conn, component, len) != -1) && + if ((ConnWriteF(session->client.conn, "{%d}\r\n", (int)len) != -1) && + (ConnWrite(session->client.conn, component, len) != -1) && (ConnWrite(session->client.conn, " ", 1) != -1)) { MemFree(component); component = NULL; @@ -578,7 +579,7 @@ SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) MemFree(component); return(STATUS_ABORT); } - + } else { if (ConnWrite(session->client.conn, "NIL ", 4) != -1) { ; @@ -711,16 +712,17 @@ SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) /* now env_in_reply_to */ if (FindRFC822Header(Header, "In-Reply-To", &component, 0)) { - unsigned char *ptr = component; + char *ptr = component; int inRepToLen = strlen(component); /* Break up reply to session, Answer may not be large enough */ /* to hold all of the recipients in the reply to field. */ do { - if (*ptr == '"' || *ptr == '\\' || *ptr < 0x20 || *ptr > 0x7f) { - if ((ConnWriteF(session->client.conn, "{%lu}\r\n", (long unsigned int)inRepToLen) != -1) && - (ConnWrite(session->client.conn, component, inRepToLen) != -1) && + if (*ptr == '"' || *ptr == '\\' || (unsigned char)*ptr < 0x20 || + (unsigned char)*ptr > 0x7f) { + if ((ConnWriteF(session->client.conn, "{%lu}\r\n", (long unsigned int)inRepToLen) != -1) && + (ConnWrite(session->client.conn, component, inRepToLen) != -1) && (ConnWrite(session->client.conn, " ", 1) != -1)) { break; } else { @@ -728,13 +730,13 @@ SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) return(STATUS_ABORT); } } - + if (*(++ptr)) { continue; } - if ((ConnWrite(session->client.conn, "\"", 1) != -1) && - (ConnWrite(session->client.conn, component, inRepToLen) != -1) && + if ((ConnWrite(session->client.conn, "\"", 1) != -1) && + (ConnWrite(session->client.conn, component, inRepToLen) != -1) && (ConnWrite(session->client.conn, "\" ", 2) != -1)) { break; } else { @@ -754,8 +756,8 @@ SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) /* now env_message_id */ if (FindRFC822Header(Header, "Message-Id", &component, 0)) { - if ((ConnWrite(session->client.conn, "\"", 1) != -1) && - (ConnWrite(session->client.conn, component, strlen(component)) != -1) && + if ((ConnWrite(session->client.conn, "\"", 1) != -1) && + (ConnWrite(session->client.conn, component, strlen(component)) != -1) && (ConnWrite(session->client.conn, "\"", 1) != -1)) { MemFree(component); component = NULL; @@ -779,9 +781,10 @@ SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) return(STATUS_ABORT); } -static long +static long FetchFlagResponderUid(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; @@ -791,12 +794,13 @@ FetchFlagResponderUid(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderFlags(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; - unsigned char addSpace[2]; + char addSpace[2]; long ccode; ccode = ConnWrite(session->client.conn, "FLAGS (", strlen("FLAGS (")); @@ -806,15 +810,15 @@ FetchFlagResponderFlags(void *param1, void *param2, void *param3) if ((FetchRequest->message->flags & STORE_MSG_FLAG_ANSWERED) && (ccode != -1)) { ccode = ConnWriteF(session->client.conn, "\\Answered"); addSpace[0]=' '; - } + } if ((FetchRequest->message->flags & STORE_MSG_FLAG_DELETED) && (ccode != -1)) { ccode = ConnWriteF(session->client.conn, "%s\\Deleted",addSpace); addSpace[0]=' '; - } + } if ((FetchRequest->message->flags & STORE_MSG_FLAG_RECENT) && (ccode != -1)) { ccode = ConnWriteF(session->client.conn, "%s\\Recent", addSpace); addSpace[0]=' '; - } + } if ((FetchRequest->message->flags & STORE_MSG_FLAG_SEEN) && (ccode != -1)) { ccode = ConnWriteF(session->client.conn, "%s\\Seen", addSpace); addSpace[0]=' '; @@ -827,20 +831,21 @@ FetchFlagResponderFlags(void *param1, void *param2, void *param3) ccode = ConnWriteF(session->client.conn, "%s\\Flagged", addSpace); addSpace[0]=' '; } - + if (ccode != -1) { ccode = ConnWrite(session->client.conn, ")", strlen(")")); if (ccode != -1) { return(STATUS_CONTINUE); } } - + return(STATUS_ABORT); } -static long +static long FetchFlagResponderEnvelope(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; if (ConnWrite(session->client.conn, "ENVELOPE ", strlen("ENVELOPE ")) != -1) { @@ -849,9 +854,10 @@ FetchFlagResponderEnvelope(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderXSender(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; long ccode; @@ -871,16 +877,16 @@ FetchFlagResponderXSender(void *param1, void *param2, void *param3) authLen = strlen(session->store.response); if (!strchr(session->store.response, '@')) { size_t hostlen = strlen(BongoGlobals.hostname); - if ((ConnWriteF(session->client.conn, "XSENDER {%lu}\r\n", authLen + 1 + hostlen) != -1) && - (ConnWrite(session->client.conn, session->store.response, authLen) != -1) && - (ConnWrite(session->client.conn, "@", 1) != -1) && + if ((ConnWriteF(session->client.conn, "XSENDER {%lu}\r\n", authLen + 1 + hostlen) != -1) && + (ConnWrite(session->client.conn, session->store.response, authLen) != -1) && + (ConnWrite(session->client.conn, "@", 1) != -1) && (ConnWrite(session->client.conn, BongoGlobals.hostname, hostlen) != -1)) { return(STATUS_CONTINUE); - } + } return(STATUS_ABORT); } - if ((ConnWriteF(session->client.conn, "XSENDER {%lu}\r\n", authLen) != -1) && + if ((ConnWriteF(session->client.conn, "XSENDER {%lu}\r\n", authLen) != -1) && (ConnWrite(session->client.conn, session->store.response, authLen) != -1)) { return(STATUS_CONTINUE); } @@ -896,8 +902,12 @@ FetchFlagResponderInternalDate(void *param1, void *param2, void *param3) ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; struct tm ltm; + time_t internalDate; - gmtime_r(&FetchRequest->message->internalDate, <m); + UNUSED_PARAMETER(param3); + + internalDate = (time_t)FetchRequest->message->internalDate; + gmtime_r(&internalDate, <m); if (ConnWriteF(session->client.conn, "INTERNALDATE \"%02d-%3s-%04d %02d:%02d:%02d +0000\"", ltm.tm_mday, Imap.command.months[ltm.tm_mon], 1900+ltm.tm_year, ltm.tm_hour, ltm.tm_min, ltm.tm_sec) != -1) { return(STATUS_CONTINUE); @@ -905,23 +915,23 @@ FetchFlagResponderInternalDate(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; - unsigned char **multiPartTypeList; + char **multiPartTypeList; long *rfcSizeList; long ccode; - unsigned char Type[MIME_TYPE_LEN+1]; - unsigned char Subtype[MIME_SUBTYPE_LEN+1]; - unsigned char Charset[MIME_CHARSET_LEN+1]; - unsigned char Encoding[MIME_ENCODING_LEN+1]; - unsigned char Name[MIME_NAME_LEN+1]; - unsigned long mimeOffset; + char Type[MIME_TYPE_LEN+1]; + char Subtype[MIME_SUBTYPE_LEN+1]; + char Charset[MIME_CHARSET_LEN+1]; + char Encoding[MIME_ENCODING_LEN+1]; + char Name[MIME_NAME_LEN+1]; + long mimeOffset; unsigned long mimeLen; - unsigned long SPos; - unsigned long Lines; + unsigned long SPos; + unsigned long Lines; unsigned long Size; unsigned long MPCount; unsigned long RFCCount; @@ -930,7 +940,9 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) unsigned long RFCSizeListLen = 20; unsigned long mimeResponseLine = 0; - multiPartTypeList = MemMalloc(sizeof(unsigned char *)*MultiPartTypeListLen); + UNUSED_PARAMETER(param3); + + multiPartTypeList = MemMalloc(sizeof(char *)*MultiPartTypeListLen); if (multiPartTypeList) { MPCount = 0; @@ -950,18 +962,18 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) char *, mimeResponseLine); switch(atol(mime_string)) { case 2002: { - ParseMIMEDLine(mime_string + 5, - Type, MIME_TYPE_LEN, - Subtype, MIME_SUBTYPE_LEN, - Charset, MIME_CHARSET_LEN, - Encoding, MIME_ENCODING_LEN, - Name, MIME_NAME_LEN, + ParseMIMEDLine(mime_string + 5, + Type, MIME_TYPE_LEN, + Subtype, MIME_SUBTYPE_LEN, + Charset, MIME_CHARSET_LEN, + Encoding, MIME_ENCODING_LEN, + Name, MIME_NAME_LEN, &mimeOffset, &mimeLen, - &SPos, - &Size, - &partHeaderSize, - &Lines); + &SPos, + &Size, + &partHeaderSize, + &Lines); /* First, set the default values if nothing is specified by the message */ if (Type[0]=='-') { @@ -977,25 +989,25 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) **MULTIPART** ************/ if (XplStrCaseCmp(Type, "Multipart")==0) { - if (MPCount < MultiPartTypeListLen) { + if (MPCount < MultiPartTypeListLen) { multiPartTypeList[MPCount++] = MemStrdup(Subtype); } else { - unsigned char *tmp; + char *tmp; MultiPartTypeListLen <<= 1; /* Double the Size */ - tmp = MemRealloc(multiPartTypeList, sizeof(unsigned char *) * MultiPartTypeListLen); + tmp = MemRealloc(multiPartTypeList, sizeof(char *) * MultiPartTypeListLen); if (tmp) { - multiPartTypeList = (unsigned char **)tmp; + multiPartTypeList = (char **)tmp; multiPartTypeList[MPCount++] = MemStrdup(Subtype); } else { while (MPCount > 0) { - MPCount--; + MPCount--; MemFree(multiPartTypeList[MPCount]); } MemFree(multiPartTypeList); MemFree(rfcSizeList); return(STATUS_MEMORY_ERROR); - } + } } if (ConnWrite(session->client.conn, "(", 1) != -1) { ; @@ -1022,29 +1034,29 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) ** MESSAGE ** ************/ } else if ((XplStrCaseCmp(Type, "Message")==0) && (XplStrCaseCmp(Subtype, "RFC822")==0)) { - unsigned char *PartHeader; + char *PartHeader; /* we have to print the basic stuff, then get the headers, print the envelope */ /* and then continue in the loop */ if (RFCCount < RFCSizeListLen) { rfcSizeList[RFCCount++] = Lines; } else { - unsigned char *tmp; + char *tmp; RFCSizeListLen <<= 1; /* Double the Size */ tmp = MemRealloc(rfcSizeList, sizeof(long) * RFCSizeListLen); if (tmp) { - rfcSizeList = (long *)tmp; + rfcSizeList = (long *)tmp; rfcSizeList[RFCCount++] = Lines; } else { while (MPCount > 0) { - MPCount--; + MPCount--; MemFree(multiPartTypeList[MPCount]); } MemFree(multiPartTypeList); MemFree(rfcSizeList); return(STATUS_MEMORY_ERROR); - } + } } /* Body Type & Subtype */ @@ -1058,7 +1070,7 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) /* Body parameter parentesized list */ if ((Charset[0]!='-') && (Name[0]!='\0')) { - unsigned char *langPtr; + char *langPtr; if ((Name[0] != '*') || (Name[1] != '=') || ((langPtr = strchr(Name + 2, '\'')) == NULL) || ((langPtr = strchr(langPtr + 1, '\'')) == NULL)) { if (ConnWriteF(session->client.conn, "(\"CHARSET\" \"%s\" \"NAME\" \"%s\") ", Charset, Name) != -1) { ; @@ -1069,7 +1081,7 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) } } else { /* this is an rfc2231 name */ - + if (ConnWriteF(session->client.conn, "(\"CHARSET\" \"%s\" \"NAME*\" \"%s\") ", Charset, Name + 2) != -1) { ; } else { @@ -1088,7 +1100,7 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) } } else if (Name[0]!='\0') { - unsigned char *langPtr; + char *langPtr; if ((Name[0] != '*') || (Name[1] != '=') || ((langPtr = strchr(Name + 2, '\'')) == NULL) || ((langPtr = strchr(langPtr + 1, '\'')) == NULL)) { if (ConnWriteF(session->client.conn, "(\"NAME\" \"%s\") ", Name) != -1) { ; @@ -1137,7 +1149,7 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) } /* Grab the envelope */ - + if ((ccode = ReadBodyPart(session, &PartHeader, FetchRequest->messageDetail.sequenceNumber, SPos, partHeaderSize)) == STATUS_CONTINUE) { ccode = SendEnvelope(session, PartHeader, session->store.response); MemFree(PartHeader); @@ -1169,7 +1181,7 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) /* Body parameter parentesized list */ if ((Charset[0]!='-') && (Name[0]!='\0')) { - unsigned char *langPtr; + char *langPtr; if ((Name[0] != '*') || (Name[1] != '=') || ((langPtr = strchr(Name + 2, '\'')) == NULL) || ((langPtr = strchr(langPtr + 1, '\'')) == NULL)) { if (ConnWriteF(session->client.conn, "(\"CHARSET\" \"%s\" \"NAME\" \"%s\") ", Charset, Name) != -1) { ; @@ -1197,7 +1209,7 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) return(STATUS_ABORT); } } else if (Name[0]!='\0') { - unsigned char *langPtr; + char *langPtr; if ((Name[0] != '*') || (Name[1] != '=') || ((langPtr = strchr(Name + 2, '\'')) == NULL) || ((langPtr = strchr(langPtr + 1, '\'')) == NULL)) { if (ConnWriteF(session->client.conn, "(\"NAME\" \"%s\") ", Name) != -1) { ; @@ -1301,15 +1313,16 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) return(STATUS_MEMORY_ERROR); } -static long +static long FetchFlagResponderRfc822Text(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; long ccode; size_t count; - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "RFC822.TEXT {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1327,9 +1340,10 @@ FetchFlagResponderRfc822Text(void *param1, void *param2, void *param3) return(STATUS_NMAP_COMM_ERROR); } -static long +static long FetchFlagResponderRfc822Size(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; @@ -1339,9 +1353,10 @@ FetchFlagResponderRfc822Size(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderRfc822Header(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; @@ -1356,12 +1371,13 @@ FetchFlagResponderRfc822Header(void *param1, void *param2, void *param3) static long FetchFlagResponderRfc822(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; long ccode; size_t count; - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "RFC822 {%lu}\r\n", count + FetchRequest->message->headerSize) != -1) { if (ConnWrite(session->client.conn, FetchRequest->messageDetail.header, FetchRequest->message->headerSize) != -1) { @@ -1372,7 +1388,7 @@ FetchFlagResponderRfc822(void *param1, void *param2, void *param3) } } } - + return(STATUS_ABORT); } @@ -1388,9 +1404,10 @@ FetchFlagResponderBody(void *param1, void *param2, void *param3) return(FetchFlagResponderBodyStructure(param1, param2, param3)); } -static long +static long FetchFlagResponderBodyHeader(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; @@ -1436,12 +1453,13 @@ FetchFlagResponderBodyHeaderPartial(void *param1, void *param2, void *param3) static long FetchFlagResponderBodyText(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; long ccode; size_t count; - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "BODY[TEXT] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1453,7 +1471,7 @@ FetchFlagResponderBodyText(void *param1, void *param2, void *param3) return(STATUS_ABORT); } - + return(CheckForNMAPCommError(ccode)); } @@ -1471,9 +1489,9 @@ FetchFlagResponderBodyTextPartial(void *param1, void *param2, void *param3) if (flag->partial.start < FetchRequest->message->bodySize) { if ((flag->partial.start + flag->partial.len) < FetchRequest->message->bodySize) { - ccode = NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize + flag->partial.start, flag->partial.len); + ccode = NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize + flag->partial.start, flag->partial.len); } else { - ccode = NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize + flag->partial.start, FetchRequest->message->bodySize - flag->partial.start); + ccode = NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize + flag->partial.start, FetchRequest->message->bodySize - flag->partial.start); } if (ccode != -1) { @@ -1504,12 +1522,13 @@ FetchFlagResponderBodyTextPartial(void *param1, void *param2, void *param3) static long FetchFlagResponderBodyBoth(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; long ccode; size_t count; - if (NMAPSendCommandF(session->store.conn, "READ %llx\r\n", FetchRequest->message->guid) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 "\r\n", FetchRequest->message->guid) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "BODY[] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1538,7 +1557,7 @@ FetchFlagResponderBodyBothPartial(void *param1, void *param2, void *param3) size_t count; if (flag->partial.start < FetchRequest->message->size) { - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->partial.start, flag->partial.len) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->partial.start, flag->partial.len) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "BODY[]<%lu> {%lu}\r\n", flag->partial.start, (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1563,9 +1582,11 @@ FetchFlagResponderBodyBothPartial(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyMime(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param2); + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; if (ConnWriteF(session->client.conn, "BODY[MIME] {%lu}\r\n", TopLevelMimeLen) != -1) { @@ -1579,6 +1600,7 @@ FetchFlagResponderBodyMime(void *param1, void *param2, void *param3) static long FetchFlagResponderBodyMimePartial(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param2); ImapSession *session = (ImapSession *)param1; FetchFlagStruct *flag = (FetchFlagStruct *)param3; @@ -1614,7 +1636,7 @@ FetchFlagResponderBodyHeaderFields(void *param1, void *param2, void *param3) FetchFlagStruct *flag = (FetchFlagStruct *)param3; long ccode; unsigned long j; - unsigned char *resultHeader; + char *resultHeader; unsigned long resultHeaderLen; if (!flag->fieldList.skip) { @@ -1631,8 +1653,8 @@ FetchFlagResponderBodyHeaderFields(void *param1, void *param2, void *param3) } return(STATUS_ABORT); - } - + } + if (ConnWriteF(session->client.conn, "\"%s\")]", flag->fieldList.field[j]) != -1) { resultHeader = GetSubHeader(FetchRequest->messageDetail.header, FetchRequest->message->headerSize, flag->fieldList.field, flag->fieldList.count, flag->fieldList.skip, &resultHeaderLen); if (resultHeader) { @@ -1640,7 +1662,7 @@ FetchFlagResponderBodyHeaderFields(void *param1, void *param2, void *param3) if (resultHeaderLen > 0) { if (ConnWriteF(session->client.conn, " {%lu}\r\n", resultHeaderLen + 2) != -1) { if (ConnWrite(session->client.conn, resultHeader, resultHeaderLen) != -1) { - if (ConnWrite(session->client.conn, "\r\n", 2) != -1) { + if (ConnWrite(session->client.conn, "\r\n", 2) != -1) { MemFree(resultHeader); return(STATUS_CONTINUE); } @@ -1706,8 +1728,8 @@ FetchFlagResponderBodyHeaderFields(void *param1, void *param2, void *param3) __inline static BOOL HasPart(GArray *mimeInfo, BodyPartRequestStruct *request) { - unsigned char type[MIME_TYPE_LEN+1]; - unsigned char dummy[MIME_NAME_LEN+1]; + char type[MIME_TYPE_LEN+1]; + char dummy[MIME_NAME_LEN+1]; unsigned long dummyLong; long depthChange = 0; @@ -1723,18 +1745,18 @@ HasPart(GArray *mimeInfo, BodyPartRequestStruct *request) char *mime_string = g_array_index(mimeInfo, char *, mimeResponseLine); switch (atol(mime_string)) { case 2002: { - ParseMIMEDLine(mime_string + 5, - type, MIME_TYPE_LEN, - dummy, MIME_NAME_LEN, - dummy, MIME_NAME_LEN, - dummy, MIME_NAME_LEN, - dummy, MIME_NAME_LEN, - &(request->mimeOffset), - &(request->mimeLen), - &(request->partOffset), - &(request->partLen), - &(request->messageHeaderLen), - &dummyLong); + ParseMIMEDLine(mime_string + 5, + type, MIME_TYPE_LEN, + dummy, MIME_NAME_LEN, + dummy, MIME_NAME_LEN, + dummy, MIME_NAME_LEN, + dummy, MIME_NAME_LEN, + &(request->mimeOffset), + &(request->mimeLen), + &(request->partOffset), + &(request->partLen), + &(request->messageHeaderLen), + &dummyLong); if (toupper(type[0]) == 'M') { if (XplStrCaseCmp(type, "MULTIPART") == 0) { @@ -1782,7 +1804,7 @@ HasPart(GArray *mimeInfo, BodyPartRequestStruct *request) return(TRUE); } - + matchDepthPart = 0; } else if (matchDepthPart > request->part[matchDepth]) { /* Not Found! Send Nothing */ @@ -1820,10 +1842,10 @@ HasPart(GArray *mimeInfo, BodyPartRequestStruct *request) } continue; } - + currentDepth--; continue; - + } /* Not Found; Send Nothing */ @@ -1833,7 +1855,7 @@ HasPart(GArray *mimeInfo, BodyPartRequestStruct *request) return(FALSE); } -static long +static long FetchFlagResponderBodyPartNumber(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -1852,7 +1874,7 @@ FetchFlagResponderBodyPartNumber(void *param1, void *param2, void *param3) } if (HasPart(FetchRequest->messageDetail.mimeInfo, &(flag->bodyPart))) { - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset, flag->bodyPart.partLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset, flag->bodyPart.partLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1864,7 +1886,7 @@ FetchFlagResponderBodyPartNumber(void *param1, void *param2, void *param3) return(STATUS_ABORT); } - + return(CheckForNMAPCommError(ccode)); } @@ -1878,7 +1900,7 @@ FetchFlagResponderBodyPartNumber(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartNumberPartial(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -1907,7 +1929,7 @@ FetchFlagResponderBodyPartNumberPartial(void *param1, void *param2, void *param3 resultLen = flag->bodyPart.partLen - flag->partial.start; } - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset + flag->partial.start, resultLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset + flag->partial.start, resultLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "]<%lu> {%lu}\r\n", flag->partial.start, (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1937,7 +1959,7 @@ FetchFlagResponderBodyPartNumberPartial(void *param1, void *param2, void *param3 return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartText(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -1954,9 +1976,9 @@ FetchFlagResponderBodyPartText(void *param1, void *param2, void *param3) } return(STATUS_ABORT); } - + if (HasPart(FetchRequest->messageDetail.mimeInfo, &(flag->bodyPart)) && flag->bodyPart.isMessage) { - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset + flag->bodyPart.messageHeaderLen, flag->bodyPart.partLen - flag->bodyPart.messageHeaderLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset + flag->bodyPart.messageHeaderLen, flag->bodyPart.partLen - flag->bodyPart.messageHeaderLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, ".TEXT] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1967,7 +1989,7 @@ FetchFlagResponderBodyPartText(void *param1, void *param2, void *param3) } return(STATUS_ABORT); } - + return(CheckForNMAPCommError(ccode)); } @@ -1982,7 +2004,7 @@ FetchFlagResponderBodyPartText(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartTextPartial(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -2015,7 +2037,7 @@ FetchFlagResponderBodyPartTextPartial(void *param1, void *param2, void *param3) /* len out of range */ resultLen = textLen - flag->partial.start; } - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, textStart + flag->partial.start, resultLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, textStart + flag->partial.start, resultLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, ".TEXT]<%lu> {%lu}\r\n", flag->partial.start, (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -2044,7 +2066,7 @@ FetchFlagResponderBodyPartTextPartial(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartHeader(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -2063,7 +2085,7 @@ FetchFlagResponderBodyPartHeader(void *param1, void *param2, void *param3) } if (HasPart(FetchRequest->messageDetail.mimeInfo, &(flag->bodyPart)) && flag->bodyPart.isMessage ) { - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset, flag->bodyPart.messageHeaderLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset, flag->bodyPart.messageHeaderLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, ".HEADER] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -2088,7 +2110,7 @@ FetchFlagResponderBodyPartHeader(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartHeaderPartial(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -2115,7 +2137,7 @@ FetchFlagResponderBodyPartHeaderPartial(void *param1, void *param2, void *param3 /* len out of range */ resultLen = flag->bodyPart.messageHeaderLen - flag->partial.start; } - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset + flag->partial.start, resultLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset + flag->partial.start, resultLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, ".HEADER]<%lu> {%lu}\r\n", flag->partial.start, (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -2139,18 +2161,18 @@ FetchFlagResponderBodyPartHeaderPartial(void *param1, void *param2, void *param3 return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartHeaderFields(void *param1, void *param2, void *param3) { FetchFlagStruct *flag = (FetchFlagStruct *)param3; ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; - + long ccode; unsigned long j; - unsigned char *fullHeader; + char *fullHeader; long fullHeaderLen; - unsigned char *resultHeader; + char *resultHeader; unsigned long resultHeaderLen; if (ConnWriteF(session->client.conn, "BODY[%lu", flag->bodyPart.part[0]) != -1) { @@ -2176,7 +2198,7 @@ FetchFlagResponderBodyPartHeaderFields(void *param1, void *param2, void *param3) continue; } return(STATUS_ABORT); - } + } } else { return(STATUS_ABORT); } @@ -2188,7 +2210,7 @@ FetchFlagResponderBodyPartHeaderFields(void *param1, void *param2, void *param3) } if (HasPart(FetchRequest->messageDetail.mimeInfo, &(flag->bodyPart)) && flag->bodyPart.isMessage) { - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset, flag->bodyPart.messageHeaderLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset, flag->bodyPart.messageHeaderLen) != -1) { ccode = NMAPReadResponse(session->store.conn, session->store.response, sizeof(session->store.response), TRUE); if (ccode == 2001) { fullHeaderLen = atoi(session->store.response); @@ -2252,7 +2274,7 @@ FetchFlagResponderBodyPartHeaderFields(void *param1, void *param2, void *param3) MemFree(resultHeader); return(STATUS_ABORT); } - + return(STATUS_MEMORY_ERROR); } } @@ -2280,7 +2302,7 @@ FetchFlagResponderBodyPartHeaderFields(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartMime(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -2298,7 +2320,7 @@ FetchFlagResponderBodyPartMime(void *param1, void *param2, void *param3) return(STATUS_ABORT); } if (HasPart(FetchRequest->messageDetail.mimeInfo, &(flag->bodyPart))) { - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.mimeOffset, flag->bodyPart.mimeLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.mimeOffset, flag->bodyPart.mimeLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, ".MIME] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -2322,7 +2344,7 @@ FetchFlagResponderBodyPartMime(void *param1, void *param2, void *param3) } -static long +static long FetchFlagResponderBodyPartMimePartial(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -2345,7 +2367,7 @@ FetchFlagResponderBodyPartMimePartial(void *param1, void *param2, void *param3) } else { count = flag->bodyPart.mimeLen - flag->partial.start; } - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.mimeOffset + flag->partial.start, (long)count) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.mimeOffset + flag->partial.start, (long)count) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, ".MIME] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -2369,7 +2391,7 @@ FetchFlagResponderBodyPartMimePartial(void *param1, void *param2, void *param3) } -static long +static long FetchFlagResponderBodyPart(void *param1, void *param2, void *param3) { FetchFlagStruct *flag = (FetchFlagStruct *)param3; @@ -2379,7 +2401,7 @@ FetchFlagResponderBodyPart(void *param1, void *param2, void *param3) static long FetchFlagResponderFast(void *param1, void *param2, void *param3) { - ImapSession *session = (ImapSession *)param1; + ImapSession *session = (ImapSession *)param1; long ccode; if ((ccode = FetchFlagResponderFlags(param1, param2, param3)) == STATUS_CONTINUE) { @@ -2394,10 +2416,10 @@ FetchFlagResponderFast(void *param1, void *param2, void *param3) return(ccode); } -static long +static long FetchFlagResponderFull(void *param1, void *param2, void *param3) { - ImapSession *session = (ImapSession *)param1; + ImapSession *session = (ImapSession *)param1; long ccode; if ((ccode = FetchFlagResponderFlags(param1, param2, param3)) == STATUS_CONTINUE) { @@ -2420,10 +2442,10 @@ FetchFlagResponderFull(void *param1, void *param2, void *param3) return(ccode); } -static long +static long FetchFlagResponderAll(void *param1, void *param2, void *param3) { - ImapSession *session = (ImapSession *)param1; + ImapSession *session = (ImapSession *)param1; long ccode; if ((ccode = FetchFlagResponderFlags(param1, param2, param3)) == STATUS_CONTINUE) { @@ -2443,9 +2465,9 @@ FetchFlagResponderAll(void *param1, void *param2, void *param3) } static unsigned long -ParseFetchFlagPartial(unsigned char **parentPtr, FetchFlagStruct *flag) +ParseFetchFlagPartial(char **parentPtr, FetchFlagStruct *flag) { - unsigned char *ptr; + char *ptr; ptr = *parentPtr; if (isdigit(*ptr)) { @@ -2478,12 +2500,12 @@ ParseFetchFlagPartial(unsigned char **parentPtr, FetchFlagStruct *flag) } __inline static unsigned long -ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) +ParseHeaderFields(char **parentPtr, FetchFlagStruct *flag) { - unsigned char *start; - unsigned char *end; - unsigned char *current; - unsigned char *fieldBuffer; + char *start; + char *end; + char *current; + char *fieldBuffer; unsigned long fieldBufferLen; unsigned long spaceCount; unsigned char len; @@ -2509,7 +2531,7 @@ ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) current++; continue; } - + spaceCount++; current++; } while (current < end); @@ -2518,7 +2540,7 @@ ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) if (spaceCount < FETCH_FIELD_ALLOC_THRESHOLD) { flag->fieldList.field = &(flag->fieldList.fieldEmbedded[0]); } else { - flag->fieldList.field = MemCalloc((spaceCount + 1), sizeof(unsigned char *)); + flag->fieldList.field = MemCalloc((spaceCount + 1), sizeof(char *)); if (flag->fieldList.field) { flag->hasAllocated |= ALLOCATED_FIELD_POINTERS; } else { @@ -2564,7 +2586,7 @@ ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) } len = current - start; - + break; } while (TRUE); } @@ -2584,10 +2606,10 @@ ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) return(F_PARSE_ERROR); } - + break; } - + return(F_PARSE_ERROR); } while (current < end); @@ -2610,7 +2632,7 @@ ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) return(ccode); } - + return(F_PARSE_ERROR); } return(F_SYSTEM_ERROR); @@ -2620,13 +2642,13 @@ ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) } __inline static unsigned long -ParseFetchFlagHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) +ParseFetchFlagHeaderFields(char **parentPtr, FetchFlagStruct *flag) { return(ParseHeaderFields(parentPtr, flag)); } __inline static unsigned long -ParseFetchFlagHeaderFieldsNot(unsigned char **parentPtr, FetchFlagStruct *flag) +ParseFetchFlagHeaderFieldsNot(char **parentPtr, FetchFlagStruct *flag) { flag->fieldList.skip = TRUE; return(ParseHeaderFields(parentPtr, flag)); @@ -2645,9 +2667,9 @@ static FetchFlag FetchFlagsSection[] = { }; static unsigned long -ParseFetchFlagBodyPart(unsigned char **parentPtr, FetchFlagStruct *flag) +ParseFetchFlagBodyPart(char **parentPtr, FetchFlagStruct *flag) { - unsigned char *ptr; + char *ptr; unsigned long *tmpPart; long sectionId; unsigned long ccode; @@ -2692,7 +2714,7 @@ ParseFetchFlagBodyPart(unsigned char **parentPtr, FetchFlagStruct *flag) ptr++; continue; } - + if (*ptr == ']') { ptr++; if (*ptr != '<') { @@ -2711,7 +2733,7 @@ ParseFetchFlagBodyPart(unsigned char **parentPtr, FetchFlagStruct *flag) return(ccode); } - + return(F_PARSE_ERROR); } @@ -2729,7 +2751,7 @@ ParseFetchFlagBodyPart(unsigned char **parentPtr, FetchFlagStruct *flag) *parentPtr = ptr; return(0); } - + return(ccode); } @@ -2836,9 +2858,9 @@ AddNewFlag(FetchStruct *FetchRequest, FetchResponder responder) } __inline static long -ParseFetchArguments(ImapSession *session, unsigned char *ptr, FetchStruct *FetchRequest) +ParseFetchArguments(ImapSession *session, char *ptr, FetchStruct *FetchRequest) { - unsigned char *Items; + char *Items; long allFlags = FetchRequest->flags; long flagID; FetchFlag flagInfo; @@ -2875,7 +2897,7 @@ ParseFetchArguments(ImapSession *session, unsigned char *ptr, FetchStruct *Fetch } continue; } - + allFlags |= flagInfo.value; ccode = flagInfo.parser(&ptr, currentFlag); @@ -2906,7 +2928,7 @@ ParseFetchArguments(ImapSession *session, unsigned char *ptr, FetchStruct *Fetch flagID = BongoKeywordBegins(FetchFlagsSoloIndex, ptr); if (flagID != -1) { flagInfo = FetchFlagsSolo[flagID]; - + ptr += flagInfo.nameLen; currentFlag = AddNewFlag(FetchRequest, flagInfo.responder); if (currentFlag) { @@ -2923,7 +2945,7 @@ ParseFetchArguments(ImapSession *session, unsigned char *ptr, FetchStruct *Fetch return(F_PARSE_ERROR); } - + allFlags |= flagInfo.value; ccode = flagInfo.parser(&ptr, currentFlag); @@ -2954,17 +2976,15 @@ __inline static long SetMessageSeenFlag(ImapSession *session, FetchStruct *FetchRequest) { long ccode; - unsigned long oldFlags; unsigned long newFlags; char *ptr; - if (NMAPSendCommandF(session->store.conn, "FLAG %llx +%lu\r\n", FetchRequest->message->guid, (unsigned long)STORE_MSG_FLAG_SEEN) != -1) { + if (NMAPSendCommandF(session->store.conn, "FLAG %" PRIx64 " +%lu\r\n", FetchRequest->message->guid, (unsigned long)STORE_MSG_FLAG_SEEN) != -1) { ccode = NMAPReadResponse(session->store.conn, session->store.response, sizeof(session->store.response), TRUE); if (ccode == 1000) { if (!(FetchRequest->flags & F_FLAGS)) { /* This fetch request does allow flags responses, but we have to wait for later to send them to the client */ - oldFlags = atol(session->store.response); ptr = strchr(session->store.response, ' '); if (ptr) { newFlags = atol(ptr + 1); @@ -2983,9 +3003,9 @@ GetMessageHeader(ImapSession *session, FetchStruct *FetchRequest) { int ccode; size_t count; - + /* Request the header */ - if (NMAPSendCommandF(session->store.conn, "READ %llx 0 %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " 0 %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { FetchRequest->message->headerSize = (long)count; FetchRequest->messageDetail.header = MemMalloc(FetchRequest->message->headerSize + 1); @@ -2999,9 +3019,9 @@ GetMessageHeader(ImapSession *session, FetchStruct *FetchRequest) } } } - } + } - return(STATUS_ABORT); + return(STATUS_ABORT); } return(CheckForNMAPCommError(ccode)); @@ -3023,7 +3043,7 @@ GetMimeInfo(ImapSession *session, FetchStruct *FetchRequest) g_array_free(FetchRequest->messageDetail.mimeInfo, TRUE); } - if (NMAPSendCommandF(session->store.conn, "MIME %llx\r\n", FetchRequest->message->guid) != -1) { + if (NMAPSendCommandF(session->store.conn, "MIME %" PRIx64 "\r\n", FetchRequest->message->guid) != -1) { ccode = NMAPReadResponse(session->store.conn, session->store.response, sizeof(session->store.response), FALSE); while(ccode != 1000) { if ((ccode > 2001) && (ccode < 2005)) { @@ -3037,7 +3057,7 @@ GetMimeInfo(ImapSession *session, FetchStruct *FetchRequest) ccode = NMAPReadResponse(session->store.conn, session->store.response, sizeof(session->store.response), FALSE); continue; } - + return(CheckForNMAPCommError(ccode)); } @@ -3083,7 +3103,7 @@ __inline static long MessageDetailsGet(ImapSession *session, FetchStruct *FetchRequest) { long ccode; - + if (FetchRequest->flags & F_NEED_HEADER) { if ((ccode = GetMessageHeader(session, FetchRequest)) == 0) { ; @@ -3179,8 +3199,8 @@ ImapCommandFetch(void *param) /* rfc 2180 discourages purge notifications during the store command */ if ((ccode = EventsSend(session, STORE_EVENT_NEW | STORE_EVENT_FLAG)) == STATUS_CONTINUE) { memset(&FetchRequest, 0, sizeof(FetchRequest)); - - FetchRequest.flags = ParseFetchArguments(session, session->command.buffer + 6, &FetchRequest); + + FetchRequest.flags = ParseFetchArguments(session, session->command.buffer + 6, &FetchRequest); if (!(FetchRequest.flags & F_ERROR)) { nextRange = FetchRequest.messageSet; @@ -3204,7 +3224,7 @@ ImapCommandFetch(void *param) } if (!(FetchRequest.flags & F_SYSTEM_ERROR)) { - FreeFetchResourcesFailure(&FetchRequest); + FreeFetchResourcesFailure(&FetchRequest); return(SendError(session->client.conn, session->command.tag, "FETCH", STATUS_INVALID_ARGUMENT)); } @@ -3232,12 +3252,12 @@ ImapCommandUidFetch(void *param) if ((ccode = EventsSend(session, STORE_EVENT_NEW | STORE_EVENT_FLAG)) == STATUS_CONTINUE) { memmove(session->command.buffer, session->command.buffer + 4, strlen(session->command.buffer + 4) + 1); memset(&FetchRequest, 0, sizeof(FetchRequest)); - - /* Respond with uid even if it has not been explicitly requested */ + + /* Respond with uid even if it has not been explicitly requested */ FetchRequest.flags = F_UID; firstFlag = AddNewFlag(&FetchRequest, FetchFlagResponderUid); if (firstFlag) { - FetchRequest.flags = ParseFetchArguments(session, session->command.buffer + 6, &FetchRequest); + FetchRequest.flags = ParseFetchArguments(session, session->command.buffer + 6, &FetchRequest); if (!(FetchRequest.flags & F_ERROR)) { nextRange = FetchRequest.messageSet; @@ -3290,17 +3310,17 @@ CommandFetchCleanup(void) BOOL CommandFetchInit(void) { - BongoKeywordIndexCreateFromTable(FetchFlagsAttIndex, FetchFlagsAtt, .name, TRUE); + BongoKeywordIndexCreateFromTable(FetchFlagsAttIndex, FetchFlagsAtt, .name, TRUE); if (FetchFlagsAttIndex != NULL) { - BongoKeywordIndexCreateFromTable(FetchFlagsSoloIndex, FetchFlagsSolo, .name, TRUE); + BongoKeywordIndexCreateFromTable(FetchFlagsSoloIndex, FetchFlagsSolo, .name, TRUE); if (FetchFlagsSoloIndex != NULL) { - BongoKeywordIndexCreateFromTable(FetchFlagsSectionIndex, FetchFlagsSection, .name, TRUE); + BongoKeywordIndexCreateFromTable(FetchFlagsSectionIndex, FetchFlagsSection, .name, TRUE); if (FetchFlagsSectionIndex) { return(TRUE); } BongoKeywordIndexFree(FetchFlagsSoloIndex); } BongoKeywordIndexFree(FetchFlagsAttIndex); - } + } return(FALSE); } diff --git a/src/agents/imap/fetch.h b/src/agents/imap/fetch.h index 7641486..afff4c6 100644 --- a/src/agents/imap/fetch.h +++ b/src/agents/imap/fetch.h @@ -66,9 +66,9 @@ typedef struct { typedef struct { unsigned long count; - unsigned char **field; - unsigned char *fieldEmbedded[FETCH_FIELD_ALLOC_THRESHOLD]; - unsigned char *fieldRequest; + char **field; + char *fieldEmbedded[FETCH_FIELD_ALLOC_THRESHOLD]; + char *fieldRequest; BOOL skip; } HeaderFieldStruct; @@ -91,7 +91,7 @@ typedef struct { typedef struct { unsigned long sequenceNumber; - unsigned char *header; + char *header; GArray *mimeInfo; } MessageDetail; @@ -110,7 +110,7 @@ typedef struct { typedef struct { unsigned long flags; - unsigned char *messageSet; + char *messageSet; unsigned long flagCount; FetchFlagStruct *flag; @@ -121,10 +121,10 @@ typedef struct { MessageDetail messageDetail; } FetchStruct; -typedef unsigned long (* FetchFlagParser)(unsigned char **ptr, FetchFlagStruct *flag); +typedef unsigned long (* FetchFlagParser)(char **ptr, FetchFlagStruct *flag); typedef struct { - unsigned char *name; + const char *name; unsigned long nameLen; unsigned long value; FetchFlagParser parser; diff --git a/src/agents/imap/imapd.c b/src/agents/imap/imapd.c index 7475653..ff862a5 100644 --- a/src/agents/imap/imapd.c +++ b/src/agents/imap/imapd.c @@ -40,52 +40,59 @@ #include #include +#include #include "imapd.h" ImapGlobal Imap; +#define IMAP_PROTOCOL_COMMAND(command, handler) \ + { command, IMAP_HELP_NOT_DEFINED, sizeof(command) - 1, handler, \ + NULL, NULL, NULL, BlackCommand } + static ProtocolCommand ImapProtocolCommands[] = { - { IMAP_COMMAND_CAPABILITY, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_CAPABILITY) - 1, ImapCommandCapability, NULL, NULL }, - { IMAP_COMMAND_NOOP, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_NOOP) - 1, ImapCommandNoop, NULL, NULL }, - { IMAP_COMMAND_LOGOUT, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_LOGOUT) - 1, ImapCommandLogout, NULL, NULL }, - { IMAP_COMMAND_STARTTLS, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_STARTTLS) - 1, ImapCommandStartTls, NULL, NULL }, - { IMAP_COMMAND_AUTHENTICATE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_AUTHENTICATE) - 1, ImapCommandAuthenticate, NULL, NULL }, - { IMAP_COMMAND_LOGIN, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_LOGIN) - 1, ImapCommandLogin, NULL, NULL }, - { IMAP_COMMAND_SELECT, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_SELECT) - 1, ImapCommandSelect, NULL, NULL }, - { IMAP_COMMAND_EXAMINE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_EXAMINE) - 1, ImapCommandExamine, NULL, NULL }, - { IMAP_COMMAND_CREATE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_CREATE) - 1, ImapCommandCreate, NULL, NULL }, - { IMAP_COMMAND_DELETE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_DELETE) - 1, ImapCommandDelete, NULL, NULL }, - { IMAP_COMMAND_RENAME, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_RENAME) - 1, ImapCommandRename, NULL, NULL }, - { IMAP_COMMAND_SUBSCRIBE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_SUBSCRIBE) - 1, ImapCommandSubscribe, NULL, NULL }, - { IMAP_COMMAND_UNSUBSCRIBE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_UNSUBSCRIBE) - 1, ImapCommandUnsubscribe, NULL, NULL }, - { IMAP_COMMAND_LIST, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_LIST) - 1, ImapCommandList, NULL, NULL }, - { IMAP_COMMAND_LSUB, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_LSUB) - 1, ImapCommandLsub, NULL, NULL }, - { IMAP_COMMAND_STATUS, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_STATUS) - 1, ImapCommandStatus, NULL, NULL }, - { IMAP_COMMAND_APPEND, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_APPEND) - 1, ImapCommandAppend, NULL, NULL }, - { IMAP_COMMAND_CHECK, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_CHECK) - 1, ImapCommandCheck, NULL, NULL }, - { IMAP_COMMAND_CLOSE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_CLOSE) - 1, ImapCommandClose, NULL, NULL }, - { IMAP_COMMAND_EXPUNGE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_EXPUNGE) - 1, ImapCommandExpunge, NULL, NULL }, - { IMAP_COMMAND_SEARCH, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_SEARCH) - 1, ImapCommandSearch, NULL, NULL }, - { IMAP_COMMAND_UID_SEARCH, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_UID_SEARCH) - 1, ImapCommandUidSearch, NULL, NULL }, - { IMAP_COMMAND_FETCH, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_FETCH) - 1, ImapCommandFetch, NULL, NULL }, - { IMAP_COMMAND_UID_FETCH, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_UID_FETCH) - 1, ImapCommandUidFetch, NULL, NULL }, - { IMAP_COMMAND_STORE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_STORE) - 1, ImapCommandStore, NULL, NULL }, - { IMAP_COMMAND_UID_STORE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_UID_STORE) - 1, ImapCommandUidStore, NULL, NULL }, - { IMAP_COMMAND_COPY, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_COPY) - 1, ImapCommandCopy, NULL, NULL }, - { IMAP_COMMAND_UID_COPY, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_UID_COPY) - 1, ImapCommandUidCopy, NULL, NULL }, - { IMAP_COMMAND_SETACL, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_SETACL) - 1, ImapCommandSetAcl, NULL, NULL }, - { IMAP_COMMAND_DELETEACL, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_DELETEACL) - 1, ImapCommandDeleteAcl, NULL, NULL }, - { IMAP_COMMAND_GETACL, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_GETACL) - 1, ImapCommandGetAcl, NULL, NULL }, - { IMAP_COMMAND_LISTRIGHTS, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_LISTRIGHTS) - 1, ImapCommandListRights, NULL, NULL }, - { IMAP_COMMAND_MYRIGHTS, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_MYRIGHTS) - 1, ImapCommandMyRights, NULL, NULL }, - { IMAP_COMMAND_SETQUOTA, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_SETQUOTA) - 1, ImapCommandSetQuota, NULL, NULL }, - { IMAP_COMMAND_GETQUOTA, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_GETQUOTA) - 1, ImapCommandGetQuota, NULL, NULL }, - { IMAP_COMMAND_GETQUOTAROOT, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_GETQUOTAROOT) - 1, ImapCommandGetQuotaRoot, NULL, NULL }, - { IMAP_COMMAND_NAMESPACE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_NAMESPACE) - 1, ImapCommandNameSpace, NULL, NULL }, - { NULL, NULL, 0, NULL, NULL, NULL } + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_CAPABILITY, ImapCommandCapability), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_NOOP, ImapCommandNoop), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_LOGOUT, ImapCommandLogout), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_STARTTLS, ImapCommandStartTls), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_AUTHENTICATE, ImapCommandAuthenticate), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_LOGIN, ImapCommandLogin), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_SELECT, ImapCommandSelect), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_EXAMINE, ImapCommandExamine), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_CREATE, ImapCommandCreate), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_DELETE, ImapCommandDelete), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_RENAME, ImapCommandRename), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_SUBSCRIBE, ImapCommandSubscribe), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_UNSUBSCRIBE, ImapCommandUnsubscribe), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_LIST, ImapCommandList), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_LSUB, ImapCommandLsub), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_STATUS, ImapCommandStatus), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_APPEND, ImapCommandAppend), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_CHECK, ImapCommandCheck), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_CLOSE, ImapCommandClose), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_EXPUNGE, ImapCommandExpunge), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_SEARCH, ImapCommandSearch), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_UID_SEARCH, ImapCommandUidSearch), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_FETCH, ImapCommandFetch), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_UID_FETCH, ImapCommandUidFetch), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_STORE, ImapCommandStore), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_UID_STORE, ImapCommandUidStore), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_COPY, ImapCommandCopy), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_UID_COPY, ImapCommandUidCopy), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_SETACL, ImapCommandSetAcl), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_DELETEACL, ImapCommandDeleteAcl), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_GETACL, ImapCommandGetAcl), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_LISTRIGHTS, ImapCommandListRights), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_MYRIGHTS, ImapCommandMyRights), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_SETQUOTA, ImapCommandSetQuota), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_GETQUOTA, ImapCommandGetQuota), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_GETQUOTAROOT, ImapCommandGetQuotaRoot), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_NAMESPACE, ImapCommandNameSpace), + { NULL, NULL, 0, NULL, NULL, NULL, NULL, BlackCommand } }; +#undef IMAP_PROTOCOL_COMMAND + #undef DEBUG #define ImapSessionGet() MemPrivatePoolGetEntryDirect(IMAPConnectionPool, __FILE__, __LINE__) @@ -93,7 +100,7 @@ static ProtocolCommand ImapProtocolCommands[] = { void *IMAPConnectionPool = NULL; static void -FreeReturnValueIndex(unsigned long *index) +FreeReturnValueIndex(long *index) { MemFree(index); } @@ -190,6 +197,7 @@ IMAPShutdown(unsigned char *Arguments, unsigned char **Response, BOOL *CloseConn static BOOL IMAPSessionAllocCB(void *Buffer, void *ClientData) { + UNUSED_PARAMETER(ClientData); register ImapSession *session = (ImapSession *)Buffer; memset(session, 0, sizeof(ImapSession)); session->client.state = STATE_FRESH; @@ -199,6 +207,7 @@ IMAPSessionAllocCB(void *Buffer, void *ClientData) static BOOL IMAPSessionFreeCB(void *Buffer, void *ClientData) { + UNUSED_PARAMETER(ClientData); register ImapSession *session = (ImapSession *)Buffer; if (session->command.buffer) { @@ -212,7 +221,7 @@ static void ImapSessionReturn(ImapSession *session) { register ImapSession *s = session; - register unsigned char *command; + register char *command; if (s->command.bufferLen == BUFSIZE) { command = s->command.buffer; @@ -221,7 +230,7 @@ ImapSessionReturn(ImapSession *session) MemFree(s->command.buffer); } - command = (unsigned char *)MemMalloc(BUFSIZE + 1); + command = MemMalloc(BUFSIZE + 1); } if (command != NULL) { @@ -307,7 +316,7 @@ FolderWatchAdd(Connection *storeConn, FolderInformation *folder, const char *eve { long ccode; - if ((ccode = CheckConnResultIsNot(-1, STATUS_NMAP_COMM_ERROR, NMAPSendCommandF(storeConn, "WATCH %llx %s\r\n", folder->guid, eventString))) == STATUS_CONTINUE) { + if ((ccode = CheckConnResultIsNot(-1, STATUS_NMAP_COMM_ERROR, NMAPSendCommandF(storeConn, "WATCH %" PRIx64 " %s\r\n", folder->guid, eventString))) == STATUS_CONTINUE) { ccode = CheckStoreResponseCode(1000, NMAPReadResponse(storeConn, NULL, 0, 0)); } return(ccode); @@ -318,7 +327,7 @@ FolderWatchRemove(Connection *storeConn, FolderInformation *folder) { long ccode; - if ((ccode = CheckConnResultIsNot(-1, STATUS_NMAP_COMM_ERROR, NMAPSendCommandF(storeConn, "WATCH %llx\r\n", folder->guid))) == STATUS_CONTINUE) { + if ((ccode = CheckConnResultIsNot(-1, STATUS_NMAP_COMM_ERROR, NMAPSendCommandF(storeConn, "WATCH %" PRIx64 "\r\n", folder->guid))) == STATUS_CONTINUE) { ccode = CheckStoreResponseCode(1000, NMAPReadResponse(storeConn, NULL, 0, 0)); } return(ccode); @@ -748,7 +757,7 @@ MessageListLoad(Connection *storeConn, OpenedFolder *folder) long headerSize; if ((ccode = MessageListReset(folder)) == STATUS_CONTINUE) { - if (NMAPSendCommandF(storeConn, "LIST %llx Pnmap.mail.headersize\r\n", folder->info->guid) != -1) { + if (NMAPSendCommandF(storeConn, "LIST %" PRIx64 " Pnmap.mail.headersize\r\n", folder->info->guid) != -1) { for (;;) { ccode = NMAPReadResponse(storeConn, reply, sizeof(reply), TRUE); if (ccode == 2001) { @@ -855,7 +864,13 @@ ImapCommandCapability(void *param) ImapSession *session = (ImapSession *)param; if ((ccode = EventsSend(session, STORE_EVENT_ALL)) == STATUS_CONTINUE) { - if (ConnWrite(session->client.conn, Imap.command.capability.ssl.message, Imap.command.capability.ssl.len) != -1) { + const char *capability = session->client.conn->ssl.enable + ? Imap.command.capability.message + : Imap.command.capability.ssl.message; + int capabilityLen = session->client.conn->ssl.enable + ? Imap.command.capability.len + : Imap.command.capability.ssl.len; + if (ConnWrite(session->client.conn, capability, capabilityLen) != -1) { return(SendOk(session, "CAPABILITY")); } return(STATUS_ABORT); @@ -1201,8 +1216,8 @@ FolderGetMessageGuids(ImapSession *session, char *collectionName, uint64_t **lis char buffer[CONN_BUFSIZE]; uint64_t *guid; uint64_t *tmpList; - long guidCount; - long allocCount; + unsigned long guidCount; + unsigned long allocCount; if (NMAPSendCommandF(session->store.conn, "LIST %s\r\n", collectionName) != -1) { @@ -1271,7 +1286,7 @@ FolderConvertToHierarchyOnly(ImapSession *session, FolderInformation *folder) long ccode; uint64_t *guid; uint64_t *guidList = NULL; - long guidCount; + unsigned long guidCount; if (NMAPSendCommandF(session->store.conn, "FLAG %s +%u\r\n", folder->name.utf8, STORE_COLLECTION_FLAG_HIERARCHY_ONLY) == -1) { return(STATUS_NMAP_COMM_ERROR); @@ -1291,7 +1306,7 @@ FolderConvertToHierarchyOnly(ImapSession *session, FolderInformation *folder) guid = &guidList[0]; while(guidCount > 0) { - if (NMAPSendCommandF(session->store.conn, "PURGE %llx\r\n", *guid) != -1) { + if (NMAPSendCommandF(session->store.conn, "PURGE %" PRIx64 "\r\n", *guid) != -1) { ccode = NMAPReadResponse(session->store.conn, NULL, 0, 0); if ((ccode == 1000) || (ccode == 4220)) { guid++; @@ -1390,7 +1405,7 @@ FolderMoveMessages(ImapSession *session, char *sourcePathUtf8, char *destination long ccode; uint64_t *guidList = NULL; uint64_t *guid; - long guidCount; + unsigned long guidCount; ccode = FolderGetMessageGuids(session, sourcePathUtf8, &guidList, &guidCount); if (ccode != STATUS_CONTINUE) { @@ -1400,7 +1415,7 @@ FolderMoveMessages(ImapSession *session, char *sourcePathUtf8, char *destination guid = &guidList[0]; while(guidCount > 0) { - if (NMAPSendCommandF(session->store.conn, "MOVE %llx /mail/%s\r\n", *guid, destinationPathUtf8) != -1) { + if (NMAPSendCommandF(session->store.conn, "MOVE %" PRIx64 " /mail/%s\r\n", *guid, destinationPathUtf8) != -1) { ccode = NMAPReadResponse(session->store.conn, NULL, 0, 0); if ((ccode == 1000) || (ccode == 4220)) { guid++; @@ -1820,10 +1835,10 @@ FolderSend(ImapSession *session, const char *command, char *pattern, unsigned lo if (*wildcard == '*') { *wildcard = '\0'; - ccode = SendMatchingFolders(session, command, pattern, FALSE, FALSE); + ccode = SendMatchingFolders(session, command, pattern, skipUnsubscribed, FALSE); } else if (*wildcard == '%') { *wildcard = '\0'; - ccode = SendMatchingFolders(session, command, pattern, FALSE, TRUE); + ccode = SendMatchingFolders(session, command, pattern, skipUnsubscribed, TRUE); } else { ccode = SendSingleFolder(session, command, pattern); } @@ -1931,7 +1946,7 @@ CheckUnseen(OpenedFolder *openFolder) typedef unsigned long (* CheckResultFunction)(OpenedFolder *openFolder); typedef struct { - unsigned char *string; + const char *string; unsigned long stringLen; CheckResultFunction function; } StatusCommandOption; @@ -2071,8 +2086,8 @@ ImapCommandStatus(void *param) ImapSession *session = (ImapSession *)param; long ccode; char *ptr; - unsigned char *ptr2; - unsigned char *items; + char *ptr2; + char *items; OpenedFolder openFolder; FolderInformation *folder; FolderPath folderPath; @@ -2080,7 +2095,7 @@ ImapCommandStatus(void *param) if ((ccode = CheckState(session, STATE_AUTH)) == STATUS_CONTINUE) { if ((ccode = EventsSend(session, STORE_EVENT_ALL)) == STATUS_CONTINUE) { if ((ccode = GetPathArgument(session, session->command.buffer + strlen("STATUS"), &ptr, &folderPath, FALSE)) == STATUS_CONTINUE) { - ptr2 = (unsigned char *)ptr; + ptr2 = ptr; if ((ccode = GrabArgument(session, &ptr2, &items)) == STATUS_CONTINUE) { if ((ccode = FolderListLoad(session)) == STATUS_CONTINUE) { if ((ccode = FolderGetByName(session, folderPath.name, &folder)) == STATUS_CONTINUE) { @@ -2264,7 +2279,7 @@ WriteMessageInMailbox(Connection *clientConn, Connection *storeConn, uint64_t fo char buffer[100]; ccode = STATUS_NMAP_COMM_ERROR; - if (NMAPSendCommandF(storeConn, "WRITE %llx %u %lu T%lu\r\n", folderGuid, STORE_DOCTYPE_MAIL, size, (unsigned long)creationTime) != -1) { + if (NMAPSendCommandF(storeConn, "WRITE %" PRIx64 " %u %lu T%lu\r\n", folderGuid, STORE_DOCTYPE_MAIL, size, (unsigned long)creationTime) != -1) { if ((ccode = CheckForNMAPCommError(NMAPReadResponse(storeConn, NULL, 0, 0))) == 2002) { ccode = STATUS_ABORT; if (ConnWrite(clientConn, "+ Ready for more data\r\n", sizeof("+ Ready for more data\r\n") - 1) != -1) { @@ -2374,7 +2389,7 @@ PurgeDeletedMessages(ImapSession *session, BOOL client_response, MessageInformat continue; } - if (NMAPSendCommandF(store, "PURGE %llx\r\n", message->guid) != -1) { + if (NMAPSendCommandF(store, "PURGE %" PRIx64 "\r\n", message->guid) != -1) { ccode = NMAPReadResponse(store, NULL, 0, 0); if (ccode == 1000) { if (client_response) { @@ -3061,8 +3076,8 @@ ImapCommandGetQuota(void *param) { ImapSession *session = (ImapSession *)param; long ccode; - unsigned char *ptr; - unsigned char Reply[1024]; + char *ptr; + char Reply[1024]; unsigned long quota = 0; unsigned long used = 0; @@ -3106,11 +3121,11 @@ ImapCommandGetQuotaRoot(void *param) { ImapSession *session = (ImapSession *)param; long ccode; - unsigned char *ptr; - unsigned char Reply[1024]; + char *ptr; + char Reply[1024]; unsigned long quota = 0; unsigned long used = 0; - unsigned char *mbox; + char *mbox; if ((ccode = CheckState(session, STATE_AUTH)) == STATUS_CONTINUE) { if ((ccode = EventsSend(session, STORE_EVENT_ALL)) == STATUS_CONTINUE) { @@ -3178,7 +3193,7 @@ HandleConnection(void *param) long ccode; long commandId; ImapSession *session=(ImapSession *)param; - unsigned char *ptr; + char *ptr; ProtocolCommand *localCommand; session->progress = NULL; @@ -3196,9 +3211,9 @@ HandleConnection(void *param) /* Grab ident from command */ ptr = strchr(session->command.buffer,' '); if (ptr) { - if ((unsigned long)(ptr - (unsigned char *)session->command.buffer) < sizeof(session->command.tag)) { + if ((unsigned long)(ptr - session->command.buffer) < sizeof(session->command.tag)) { ptr[0]='\0'; - memcpy(session->command.tag, session->command.buffer, ptr - (unsigned char *)session->command.buffer + 1); + memcpy(session->command.tag, session->command.buffer, ptr - session->command.buffer + 1); } else { memcpy(session->command.tag, session->command.buffer, sizeof(session->command.tag)); session->command.tag[sizeof(session->command.tag)-1]='\0'; @@ -3384,15 +3399,15 @@ InitializeImapGlobals() Imap.server.ssl.port = 993; Imap.server.ssl.config.options = 0; Imap.server.ssl.config.options |= SSL_ALLOW_CHAIN; - Imap.server.ssl.config.options |= SSL_ALLOW_SSL3; + Imap.server.allowLegacyTLS = FALSE; Imap.server.threadGroupId = XplGetThreadGroupID(); strcpy(Imap.server.postmaster, "admin"); /* Imap.command. */ Imap.command.capability.acl.enabled = TRUE; /* FIXME: ACL ?? */ - Imap.command.capability.len = sprintf(Imap.command.capability.message, "%s\r\n", "* CAPABILITY IMAP4 IMAP4rev1 AUTH=LOGIN NAMESPACE XSENDER"); - Imap.command.capability.ssl.len = sprintf(Imap.command.capability.ssl.message, "%s\r\n", "* CAPABILITY IMAP4 IMAP4rev1 AUTH=LOGIN NAMESPACE STARTTLS XSENDER LOGINDISABLED"); + Imap.command.capability.len = sprintf(Imap.command.capability.message, "%s\r\n", "* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=LOGIN NAMESPACE XSENDER"); + Imap.command.capability.ssl.len = sprintf(Imap.command.capability.ssl.message, "%s\r\n", "* CAPABILITY IMAP4 IMAP4rev1 NAMESPACE STARTTLS XSENDER LOGINDISABLED"); Imap.command.months[0] = "Jan"; Imap.command.months[1] = "Feb"; @@ -3462,6 +3477,8 @@ IMAPServer(void *unused) ImapSession *session; XplThreadID id = 0; + UNUSED_PARAMETER(unused); + XplRenameThread(XplGetThreadID(), "IMAP Server"); XplSafeIncrement(Imap.server.active); @@ -3530,6 +3547,7 @@ IMAPServer(void *unused) Imap.exiting = TRUE; oldTGID = XplSetThreadGroupID(Imap.server.threadGroupId); + UNUSED_PARAMETER(oldTGID); if (Imap.server.conn) { ConnClose(Imap.server.conn); @@ -3601,6 +3619,7 @@ IMAPServer(void *unused) static void BusyThread(void *unused) { + UNUSED_PARAMETER(unused); while (!Imap.exiting) { DoUpdate(); XplDelay(10000); @@ -3611,11 +3630,13 @@ static void IMAPSSLServer(void *unused) { ImapSession *session; - unsigned char *message; + char *message; unsigned long messageLen; XplThreadID id = 0; int ccode; Connection *conn; + + UNUSED_PARAMETER(unused); XplRenameThread(XplGetThreadID(), "IMAP SSL Server"); @@ -3716,6 +3737,7 @@ IMAPSSLServer(void *unused) static BongoConfigItem IMAPConfig[] = { { BONGO_JSON_INT, "o:port/i", &Imap.server.port }, { BONGO_JSON_INT, "o:port_ssl/i", &Imap.server.ssl.port }, + { BONGO_JSON_BOOL, "o:allow_legacy_tls/b", &Imap.server.allowLegacyTLS }, { BONGO_JSON_INT, "o:threads_max/i", &Imap.session.threads.max }, { BONGO_JSON_NULL, NULL, NULL } }; @@ -3754,6 +3776,9 @@ XplServiceMain(int argc, char *argv[]) int ccode; XplThreadID id=0; + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); + if (XplSetEffectiveUser(MsgGetUnprivilegedUser()) < 0) { XplConsolePrintf("bongoimap: Could not drop to unprivileged user '%s', exiting.\n", MsgGetUnprivilegedUser()); return 1; @@ -3777,6 +3802,10 @@ XplServiceMain(int argc, char *argv[]) MsgInit(); MsgAuthInit(); + if (!BongoSaslInitialize()) { + XplConsolePrintf("IMAPD: Unable to initialize Cyrus SASL.\r\n"); + return -1; + } NMAPInitialize(); LogOpen("bongoimap"); @@ -3791,6 +3820,9 @@ XplServiceMain(int argc, char *argv[]) } if (!ServerSocketSSLInit()) { + if (Imap.server.allowLegacyTLS) { + Imap.server.ssl.config.options |= SSL_ALLOW_LEGACY_PROTOCOLS; + } Imap.server.ssl.config.certificate.file = MsgGetFile(MSGAPI_FILE_PUBKEY, NULL, 0); Imap.server.ssl.config.key.file = MsgGetFile(MSGAPI_FILE_PRIVKEY, NULL, 0); diff --git a/src/agents/imap/imapd.h b/src/agents/imap/imapd.h index d59beee..88d1916 100755 --- a/src/agents/imap/imapd.h +++ b/src/agents/imap/imapd.h @@ -20,6 +20,7 @@ ****************************************************************************/ #include +#include #include #include #include @@ -235,7 +236,7 @@ typedef struct { } FolderInformation; typedef struct { - unsigned char *buffer; + char *buffer; char *name; unsigned long nameLen; FolderInformation *object; @@ -292,7 +293,7 @@ typedef struct { typedef struct { struct { Connection *conn; /* connection to the store agent */ - unsigned char response[1024]; /* used to read store responses */ + char response[1024]; /* used to read store responses */ } store; struct { @@ -377,6 +378,7 @@ typedef struct { unsigned short port; int threadGroupId; + BOOL allowLegacyTLS; BOOL disabled; XplSemaphore mainSemaphore; XplSemaphore shutdownSemaphore; diff --git a/src/agents/imap/inline.h b/src/agents/imap/inline.h index 14e51aa..abaca55 100644 --- a/src/agents/imap/inline.h +++ b/src/agents/imap/inline.h @@ -48,10 +48,10 @@ ReadCommandLine(Connection *conn, char **buffer, unsigned long *bufferLen) } __inline static long -GrabOctet(ImapSession *session, unsigned char **Input, unsigned char **Destination) +GrabOctet(ImapSession *session, char **Input, char **Destination) { - unsigned char *start; - unsigned char *end; + char *start; + char *end; long Size; long ccode; @@ -104,11 +104,12 @@ GrabOctet(ImapSession *session, unsigned char **Input, unsigned char **Destinati } __inline static long -GrabLiteral(ImapSession *session, unsigned char **Input, unsigned char **Destination) +GrabLiteral(ImapSession *session, char **Input, char **Destination) { - unsigned char *contentBegin; + char *contentBegin; unsigned long contentLen; - unsigned char *closeQuote; + char *closeQuote; + UNUSED_PARAMETER(session) /* skip opening quote */ contentBegin = *Input + 1; @@ -131,12 +132,13 @@ GrabLiteral(ImapSession *session, unsigned char **Input, unsigned char **Destina } __inline static unsigned char -GrabGroup(ImapSession *session, unsigned char **Input, unsigned char **Destination) +GrabGroup(ImapSession *session, char **Input, char **Destination) { - unsigned char *contentBegin; + char *contentBegin; unsigned long contentLen; - unsigned char *closeParen; + char *closeParen; int nesting = 1; + UNUSED_PARAMETER(session) /* We have to look for the proper closing paren, since they can be nested */ closeParen = *Input; @@ -178,11 +180,12 @@ GrabGroup(ImapSession *session, unsigned char **Input, unsigned char **Destinati } __inline static unsigned char -GrabWord(ImapSession *session, unsigned char **Input, unsigned char **Destination, BOOL lookForParen) +GrabWord(ImapSession *session, char **Input, char **Destination, BOOL lookForParen) { long len; - unsigned char *spacePtr; - unsigned char *parenPtr; + char *spacePtr; + char *parenPtr; + UNUSED_PARAMETER(session) spacePtr = strchr((*Input), ' '); if (lookForParen) { @@ -223,9 +226,9 @@ GrabWord(ImapSession *session, unsigned char **Input, unsigned char **Destinatio } __inline static long -GrabArgumentEx(ImapSession *session, unsigned char **Input, unsigned char **Destination, BOOL lookForParen) +GrabArgumentEx(ImapSession *session, char **Input, char **Destination, BOOL lookForParen) { - unsigned char *ptr = *Input; + char *ptr = *Input; while (IsWhiteSpace(*ptr)) { ptr++; @@ -258,7 +261,7 @@ GrabArgumentEx(ImapSession *session, unsigned char **Input, unsigned char **Dest } __inline static long -GrabArgument(ImapSession *session, unsigned char **Input, unsigned char **Destination) +GrabArgument(ImapSession *session, char **Input, char **Destination) { return(GrabArgumentEx(session, Input, Destination, FALSE)); } @@ -336,7 +339,7 @@ SendError(Connection *conn, char *prefix, char *command, long errorValue) } __inline static long -SendOk(ImapSession *session, unsigned char *command) +SendOk(ImapSession *session, const char *command) { if (ConnWriteF(session->client.conn, "%s OK %s completed\r\n", session->command.tag, command) != -1) { return(STATUS_CONTINUE); @@ -520,7 +523,7 @@ CheckPathLen(char *path) __inline static void AddMboxSpaces(char *mbox) { - unsigned char *ptr = mbox; + char *ptr = mbox; while((ptr = strchr(ptr, 127)) != NULL) { *ptr++ = ' '; @@ -528,10 +531,10 @@ AddMboxSpaces(char *mbox) } __inline static long -GrabTwoArguments(ImapSession *session, unsigned char *arguments, unsigned char **argument1, unsigned char **argument2) +GrabTwoArguments(ImapSession *session, char *arguments, char **argument1, char **argument2) { long ccode; - unsigned char *ptr = arguments; + char *ptr = arguments; do { if (!IsWhiteSpace(*ptr)) { @@ -564,8 +567,7 @@ __inline static long LeadCommandLine(ImapSession *session) { long ccode; - unsigned char *tmpCommand; - unsigned long bytesRead; + char *tmpCommand; ccode = ConnReadAnswer(session->client.conn, session->command.buffer, session->command.bufferLen); if (ccode > -1) { @@ -573,7 +575,6 @@ LeadCommandLine(ImapSession *session) return(STATUS_CONTINUE); } - bytesRead = ccode; do { tmpCommand = MemRealloc(session->command.buffer, session->command.bufferLen * 2); if (tmpCommand) { @@ -750,7 +751,7 @@ __inline static long GetPathArgument(ImapSession *session, char *start, char **next, FolderPath *path, BOOL nullStringValid) { long ccode; - unsigned char *ptr; + char *ptr; ptr = FindNextArgument(start); if (ptr) { @@ -870,7 +871,7 @@ StoreMessageFlag(Connection *storeConn, uint64_t guid, char *actionString, unsig char *ptr; char buffer[30]; - if (NMAPSendCommandF(storeConn, "FLAG %llx %s%lu\r\n", guid, actionString, flags) != -1) { + if (NMAPSendCommandF(storeConn, "FLAG %" PRIx64 " %s%lu\r\n", guid, actionString, flags) != -1) { if (newFlags) { if ((ccode = CheckForNMAPCommError(NMAPReadResponse(storeConn, buffer, sizeof(buffer), TRUE))) == 1000) { if ((ptr = strchr(buffer, ' ')) != NULL) { @@ -935,4 +936,3 @@ SearchKnownMessagesByGuid(MessageInformation *messageList, unsigned long message } return(NULL); } - diff --git a/src/agents/imap/rfc822parse.c b/src/agents/imap/rfc822parse.c index 7592155..e841e3f 100644 --- a/src/agents/imap/rfc822parse.c +++ b/src/agents/imap/rfc822parse.c @@ -26,7 +26,7 @@ /* RFC822 Address parsing */ #define BROKEN_ADDRESS_HOST ".Syntax-Error." #define BROKEN_ADDRESS_MAILBOX "UnexpectedDataAfterAddress" -const unsigned char *WSpecials = " ()<>@,;:\\\"[]\1\2\3\4\5\6\7\10\11\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\177"; +const char *WSpecials = " ()<>@,;:\\\"[]\1\2\3\4\5\6\7\10\11\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\177"; #define I2C_ESC 0x1b /* ESCape */ #define I2C_MULTI 0x24 /* multi-byte character set */ @@ -40,12 +40,12 @@ const unsigned char *WSpecials = " ()<>@,;:\\\"[]\1\2\3\4\5\6\7\10\11\12\13\14\1 #define I2CS_94_ASCII 0x42 /* 4/2 ISO 646 USA (ASCII) */ #define I2CS_ASCII (I2CS_94 | I2CS_94_ASCII) -static unsigned char -*RFC822SkipComment(unsigned char **String, unsigned long Trim) +static char +*RFC822SkipComment(char **String, unsigned long Trim) { - unsigned char *RetVal; - unsigned char *ptr = *String; - unsigned char *t = NULL; + char *RetVal; + char *ptr = *String; + char *t = NULL; /* Skip past whitespace */ for (RetVal = ++ptr; (*RetVal==' ' || *RetVal==0x09); RetVal++) { @@ -103,7 +103,7 @@ static unsigned char static void -RFC822SkipWhitespace(unsigned char **String) +RFC822SkipWhitespace(char **String) { while (TRUE) { if ((**String == ' ') || (**String == 0x09)) { @@ -115,13 +115,13 @@ RFC822SkipWhitespace(unsigned char **String) } -static unsigned char -*RFC822Quote(unsigned char *src) +static char +*RFC822Quote(char *src) { - unsigned char *ret = src; + char *ret = src; if (strpbrk(src,"\\\"")) { - unsigned char *dst = ret; + char *dst = ret; while (*src) { if (*src == '\"') { @@ -139,18 +139,18 @@ static unsigned char } -static unsigned char -*RFC822Copy(unsigned char *Source) +static char +*RFC822Copy(char *Source) { return(RFC822Quote(MemStrdup(Source))); } -static unsigned char -*RFC822ParseWord(unsigned char *s, const unsigned char *Delimiters) +static char +*RFC822ParseWord(char *s, const char *Delimiters) { - unsigned char *st; - unsigned char *str; + char *st; + char *str; if (!s) { return(NULL); @@ -232,6 +232,7 @@ static unsigned char str = st + 2; break; } + __attribute__((fallthrough)); } default: { @@ -243,10 +244,10 @@ static unsigned char } -static unsigned char -*RFC822ParsePhrase(unsigned char *s) +static char +*RFC822ParsePhrase(char *s) { - unsigned char *curpos; + char *curpos; if (!s) { return(NULL); @@ -269,14 +270,14 @@ static unsigned char static RFC822AddressStruct -*RFC822ParseAddrSpec(unsigned char *Address, unsigned char **ret, unsigned char *DefaultHost) +*RFC822ParseAddrSpec(char *Address, char **ret, char *DefaultHost) { RFC822AddressStruct *Adr; - unsigned char c; - unsigned char *s; - unsigned char *t; - unsigned char *v; - unsigned char *end; + char c; + char *s; + char *t; + char *v; + char *end; if (!Address) { return(NULL); @@ -360,21 +361,21 @@ static RFC822AddressStruct } -unsigned char -*RFC822ParseDomain(unsigned char *Address, unsigned char **end) +char +*RFC822ParseDomain(char *Address, char **end) { - unsigned char *ret = NULL; - unsigned char c; - unsigned char *s; - unsigned char *t; - unsigned char *v; + char *ret = NULL; + char c; + char *s; + char *t; + char *v; RFC822SkipWhitespace(&Address); if (*Address == '[') { if ((*end=RFC822ParseWord(Address + 1,"]\\"))!=NULL) { size_t len = ++*end - Address; - ret=(unsigned char *)MemMalloc(len + 1); + ret=(char *)MemMalloc(len + 1); strncpy(ret, Address, len); ret[len]='\0'; } @@ -395,7 +396,7 @@ unsigned char s=RFC822Copy(Address); *t=c; - v=(unsigned char *)MemMalloc(strlen(ret)+strlen(s)+2); + v=(char *)MemMalloc(strlen(ret)+strlen(s)+2); if (v) { sprintf (v, "%s.%s", ret, s); MemFree(ret); @@ -413,12 +414,12 @@ unsigned char static RFC822AddressStruct -*RFC822ParseRouteAddress(unsigned char *Address, unsigned char **ret, unsigned char *DefaultHost) +*RFC822ParseRouteAddress(char *Address, char **ret, char *DefaultHost) { RFC822AddressStruct *Adr; - unsigned char *s; - unsigned char *t; - unsigned char *adl; + char *s; + char *t; + char *adl; size_t adllen, i; if (!Address) { @@ -439,7 +440,7 @@ static RFC822AddressStruct for (adl = NULL, adllen = 0; (*t == '@') && ((s = RFC822ParseDomain(t + 1, &t)) != NULL); ) { /* parse possible A-D-L */ i = strlen (s) + 2; if (adl) { - unsigned char *tmp; + char *tmp; tmp = MemRealloc(adl, adllen + i); if (tmp) { @@ -503,11 +504,11 @@ static RFC822AddressStruct static RFC822AddressStruct -*RFC822ParseMailbox(unsigned char **Address, unsigned char *DefaultHost) +*RFC822ParseMailbox(char **Address, char *DefaultHost) { RFC822AddressStruct *Adr = NULL; - unsigned char *s; - unsigned char *end; + char *s; + char *end; if (!*Address) { return(NULL); @@ -539,10 +540,10 @@ static RFC822AddressStruct static RFC822AddressStruct -*RFC822ParseGroup(RFC822AddressStruct **List, RFC822AddressStruct *Last, unsigned char **Address, char *DefaultHost, unsigned long Depth) +*RFC822ParseGroup(RFC822AddressStruct **List, RFC822AddressStruct *Last, char **Address, char *DefaultHost, unsigned long Depth) { - unsigned char *p; - unsigned char *s; + char *p; + char *s; RFC822AddressStruct *Adr; if (Depth > 50) { @@ -635,7 +636,7 @@ static RFC822AddressStruct RFC822AddressStruct -*RFC822ParseAddress(RFC822AddressStruct **List, RFC822AddressStruct *Last, unsigned char **Address, unsigned char *DefaultHost, unsigned long Depth) +*RFC822ParseAddress(RFC822AddressStruct **List, RFC822AddressStruct *Last, char **Address, char *DefaultHost, unsigned long Depth) { RFC822AddressStruct *Adr; @@ -668,11 +669,11 @@ RFC822AddressStruct BOOL -RFC822ParseAddressList(RFC822AddressStruct **List, unsigned char *Address, unsigned char *DefaultHost) +RFC822ParseAddressList(RFC822AddressStruct **List, char *Address, char *DefaultHost) { RFC822AddressStruct *Last=*List; RFC822AddressStruct *Adr; - unsigned char c; + char c; if (!Address) { return(FALSE); diff --git a/src/agents/imap/rfc822parse.h b/src/agents/imap/rfc822parse.h index b57c9e7..094ef85 100644 --- a/src/agents/imap/rfc822parse.h +++ b/src/agents/imap/rfc822parse.h @@ -20,14 +20,14 @@ ****************************************************************************/ typedef struct _RFC822AddressStruct { - unsigned char *Personal; - unsigned char *Mailbox; - unsigned char *Host; - unsigned char *ADL; + char *Personal; + char *Mailbox; + char *Host; + char *ADL; struct _RFC822AddressStruct *Next; } RFC822AddressStruct; -unsigned char *RFC822ParseDomain(unsigned char *Address, unsigned char **end); -RFC822AddressStruct *RFC822ParseAddress(RFC822AddressStruct **List, RFC822AddressStruct *Last, unsigned char **Address, unsigned char *DefaultHost, unsigned long Depth); -BOOL RFC822ParseAddressList(RFC822AddressStruct **List, unsigned char *Address, unsigned char *DefaultHost); +char *RFC822ParseDomain(char *Address, char **end); +RFC822AddressStruct *RFC822ParseAddress(RFC822AddressStruct **List, RFC822AddressStruct *Last, char **Address, char *DefaultHost, unsigned long Depth); +BOOL RFC822ParseAddressList(RFC822AddressStruct **List, char *Address, char *DefaultHost); void RFC822FreeAddressList(RFC822AddressStruct *List); diff --git a/src/agents/imap/search.c b/src/agents/imap/search.c index 5793ce3..00ad810 100644 --- a/src/agents/imap/search.c +++ b/src/agents/imap/search.c @@ -25,9 +25,9 @@ #include "search.h" #include -long SearchHandleKey(ImapSession *session, char **keyString, SearchKey *key); -static long SearchHandleSubKey(ImapSession *session, char **keyString, SearchKey *key); -typedef long (* SearchRemainingMatch)(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *result); +long SearchHandleKey(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))); +static long SearchHandleSubKey(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))); +typedef long (* SearchRemainingMatch)(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *result); long ReadSourceFile(void *source, char *buffer, unsigned long maxRead); long ReadSourceConn(void *source, char *buffer, unsigned long maxRead); @@ -46,7 +46,7 @@ ReadSourceConn(void *source, char *buffer, unsigned long maxRead) } __inline static void -MarkedMatchesPreserve(SearchKey *key) +MarkedMatchesPreserve(SearchKey *key __attribute__((unused))) { long readNum = 0; long writeNum = 0; @@ -70,7 +70,7 @@ MarkedMatchesPreserve(SearchKey *key) } __inline static void -MarkedMatchesRemove(SearchKey *key) +MarkedMatchesRemove(SearchKey *key __attribute__((unused))) { unsigned long readNum = 0; unsigned long writeNum = 0; @@ -94,7 +94,7 @@ MarkedMatchesRemove(SearchKey *key) __inline static void -MarkMatch(SearchKey *key, unsigned long index) +MarkMatch(SearchKey *key __attribute__((unused)), unsigned long index) { /* make sure that it is not already marked */ if (key->matchList[index] > -1) { @@ -103,7 +103,7 @@ MarkMatch(SearchKey *key, unsigned long index) } __inline static void -MarkMatches(SearchKey *key, SearchKey *newKey) +MarkMatches(SearchKey *key __attribute__((unused)), SearchKey *newKey) { unsigned long readNum = 0; unsigned long writeNum = 0; @@ -128,7 +128,7 @@ MarkMatches(SearchKey *key, SearchKey *newKey) __inline static long -SearchKeyObjectDuplicate(SearchKey *key, SearchKey *newKey) +SearchKeyObjectDuplicate(SearchKey *key __attribute__((unused)), SearchKey *newKey) { memset(newKey, 0, sizeof(SearchKey)); @@ -157,7 +157,7 @@ SearchKeyObjectDuplicate(SearchKey *key, SearchKey *newKey) } __inline static long -SearchKeyObjectCreate(SearchKey *key, char *charset, unsigned long msgCount) +SearchKeyObjectCreate(SearchKey *key __attribute__((unused)), char *charset, unsigned long msgCount) { unsigned long i; unsigned long len; @@ -189,7 +189,7 @@ SearchKeyObjectCreate(SearchKey *key, char *charset, unsigned long msgCount) } __inline static void -SearchKeyObjectFree(SearchKey *key) +SearchKeyObjectFree(SearchKey *key __attribute__((unused))) { MemFree(key->matchList); @@ -199,7 +199,7 @@ SearchKeyObjectFree(SearchKey *key) } __inline static long -SearchStringToUtf8(SearchKey *key, char *in, char **out) +SearchStringToUtf8(SearchKey *key __attribute__((unused)), char *in, char **out) { char buffer[SEARCH_MAX_UTF8_STRING]; long len; @@ -224,12 +224,12 @@ SearchStringToUtf8(SearchKey *key, char *in, char **out) } __inline static long -GetSearchStringArg(ImapSession *session, SearchKey *key, char **keyString, char **utf8SearchString) +GetSearchStringArg(ImapSession *session __attribute__((unused)), SearchKey *key __attribute__((unused)), char **keyString __attribute__((unused)), char **utf8SearchString) { long ccode; char *searchString; - if((ccode = GrabArgumentEx(session, (unsigned char **)keyString, (unsigned char **)&(searchString), TRUE)) == STATUS_CONTINUE) { + if((ccode = GrabArgumentEx(session, keyString, &searchString, TRUE)) == STATUS_CONTINUE) { if (XplStrCaseCmp(key->charset, "utf-8") == 0) { *utf8SearchString = searchString; return(STATUS_CONTINUE); @@ -245,7 +245,7 @@ GetSearchStringArg(ImapSession *session, SearchKey *key, char **keyString, char } __inline static long -ImapDateArgToUtc(char *dateText, unsigned long *dateUtc) +ImapDateArgToUtc(char *dateText, time_t *dateUtc) { char *dayPtr; char *monthPtr; @@ -271,12 +271,12 @@ ImapDateArgToUtc(char *dateText, unsigned long *dateUtc) } __inline static long -GetDateArg(ImapSession *session, char **keyString, time_t *date) +GetDateArg(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), time_t *date) { long ccode; char *dateText; - if((ccode = GrabArgumentEx(session, (unsigned char **)keyString, (unsigned char **)&(dateText), TRUE)) == STATUS_CONTINUE) { + if((ccode = GrabArgumentEx(session, keyString, &dateText, TRUE)) == STATUS_CONTINUE) { ccode = ImapDateArgToUtc(dateText, date); MemFree(dateText); return(ccode); @@ -285,13 +285,13 @@ GetDateArg(ImapSession *session, char **keyString, time_t *date) } __inline static long -GetMessageSentDate(ImapSession *session, uint64_t guid, unsigned long headerLen, time_t *dateUtc) +GetMessageSentDate(ImapSession *session __attribute__((unused)), uint64_t guid, unsigned long headerLen, time_t *dateUtc) { long ccode; size_t len; char *dateString; - if ((ccode = NMAPSendCommandF(session->store.conn, "READ %llx 0 %lu\r\n", guid, headerLen)) != -1) { + if ((ccode = NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " 0 %lu\r\n", guid, headerLen)) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &len)) == 2001) { if ((dateString = BongoStreamGrabRfc822Header(ReadSourceConn, session->store.conn, (unsigned long)len, "Date")) != NULL) { if (NMAPReadCrLf(session->store.conn) == 2) { @@ -309,13 +309,13 @@ GetMessageSentDate(ImapSession *session, uint64_t guid, unsigned long headerLen, } __inline static long -GetNumericArg(ImapSession *session, char **keyString, unsigned long *num) +GetNumericArg(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), unsigned long *num) { long ccode; - unsigned char *numText; + char *numText; long longNum; - if((ccode = GrabArgumentEx(session, (unsigned char **)keyString, &numText, TRUE)) == STATUS_CONTINUE) { + if((ccode = GrabArgumentEx(session, keyString, &numText, TRUE)) == STATUS_CONTINUE) { longNum = atol(numText); MemFree(numText); if (longNum >= 0) { @@ -328,10 +328,10 @@ GetNumericArg(ImapSession *session, char **keyString, unsigned long *num) } __inline static long -GetMessageSetArg(ImapSession *session, char **keyString, long **messageSet) +GetMessageSetArg(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), long **messageSet) { long ccode; - unsigned char *setText; + char *setText; char *nextRange; unsigned long start; unsigned long end; @@ -339,7 +339,7 @@ GetMessageSetArg(ImapSession *session, char **keyString, long **messageSet) unsigned long messageCount = session->folder.selected.messageCount; long *workSet; - if((ccode = GrabArgumentEx(session, (unsigned char **)keyString, &setText, TRUE)) == STATUS_CONTINUE) { + if((ccode = GrabArgumentEx(session, keyString, &setText, TRUE)) == STATUS_CONTINUE) { nextRange = setText; if ((workSet = MemMalloc0(sizeof(long) * session->folder.selected.messageCount)) != NULL) { do { @@ -371,14 +371,14 @@ GetMessageSetArg(ImapSession *session, char **keyString, long **messageSet) __inline static long -FullListNoMatch(SearchKey *key) +FullListNoMatch(SearchKey *key __attribute__((unused))) { key->matchList[0] = -1; return(STATUS_CONTINUE); } __inline static long -FullListProcessMatch(ImapSession *session, SearchKey *key, char *response) +FullListProcessMatch(ImapSession *session __attribute__((unused)), SearchKey *key __attribute__((unused)), char *response) { long ccode; char *ptr; @@ -398,11 +398,11 @@ FullListProcessMatch(ImapSession *session, SearchKey *key, char *response) } __inline static long -FullListSubstringSearch(ImapSession *session, SearchKey *key, char *searchString, char *subcommand) +FullListSubstringSearch(ImapSession *session __attribute__((unused)), SearchKey *key __attribute__((unused)), char *searchString, char *subcommand) { long ccode; - if (NMAPSendCommandF(session->store.conn, "SEARCH %llx %s \"%s\"\r\n", session->folder.selected.info->guid, subcommand, searchString) != -1) { + if (NMAPSendCommandF(session->store.conn, "SEARCH %" PRIx64 " %s \"%s\"\r\n", session->folder.selected.info->guid, subcommand, searchString) != -1) { for (;;) { ccode = NMAPReadResponse(session->store.conn, session->store.response, sizeof(session->store.response), TRUE); if (ccode == 2001) { @@ -424,7 +424,7 @@ FullListSubstringSearch(ImapSession *session, SearchKey *key, char *searchString /* SearchRemainingMatch start */ static long -SearchRemainingMatchFlag(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchFlag(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long flag = (unsigned long)arg2; @@ -433,7 +433,7 @@ SearchRemainingMatchFlag(ImapSession *session, MessageInformation *message, char } static long -SearchRemainingMatchFlagNot(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchFlagNot(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long flag = (unsigned long)arg2; @@ -442,14 +442,14 @@ SearchRemainingMatchFlagNot(ImapSession *session, MessageInformation *message, c } static long -SearchRemainingMatchNew(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchNew(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { *found = (((message->flags & STORE_MSG_FLAG_RECENT) != 0) && ((message->flags & STORE_MSG_FLAG_SEEN) == 0)); return(STATUS_CONTINUE); } static long -SearchRemainingMatchBefore(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchBefore(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long date = *(unsigned long *)arg2; @@ -458,7 +458,7 @@ SearchRemainingMatchBefore(ImapSession *session, MessageInformation *message, ch } static long -SearchRemainingMatchOn(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchOn(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long date = *(unsigned long *)arg2; @@ -467,7 +467,7 @@ SearchRemainingMatchOn(ImapSession *session, MessageInformation *message, char * } static long -SearchRemainingMatchSince(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchSince(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long date = *(unsigned long *)arg2; @@ -476,7 +476,7 @@ SearchRemainingMatchSince(ImapSession *session, MessageInformation *message, cha } static long -SearchRemainingMatchSentBefore(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchSentBefore(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { long ccode; time_t date = *(unsigned long *)arg2; @@ -490,7 +490,7 @@ SearchRemainingMatchSentBefore(ImapSession *session, MessageInformation *message } static long -SearchRemainingMatchSentOn(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchSentOn(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { long ccode; time_t date = *(unsigned long *)arg2; @@ -504,7 +504,7 @@ SearchRemainingMatchSentOn(ImapSession *session, MessageInformation *message, ch } static long -SearchRemainingMatchSentSince(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchSentSince(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { long ccode; time_t date = *(unsigned long *)arg2; @@ -518,7 +518,7 @@ SearchRemainingMatchSentSince(ImapSession *session, MessageInformation *message, } static long -SearchRemainingMatchLarger(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchLarger(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long size = (unsigned long)arg2; @@ -527,7 +527,7 @@ SearchRemainingMatchLarger(ImapSession *session, MessageInformation *message, ch } static long -SearchRemainingMatchSmaller(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchSmaller(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long size = (unsigned long)arg2; @@ -536,12 +536,12 @@ SearchRemainingMatchSmaller(ImapSession *session, MessageInformation *message, c } static long -SearchRemainingMatchSubstring(ImapSession *session, MessageInformation *message, char *subcommand, void *arg2, BOOL *found) +SearchRemainingMatchSubstring(ImapSession *session __attribute__((unused)), MessageInformation *message, char *subcommand, void *arg2 __attribute__((unused)), BOOL *found) { char *searchString = (char *)arg2; long ccode; - if (NMAPSendCommandF(session->store.conn, "SEARCH %llx %s \"%s\"\r\n", message->guid, subcommand, searchString) != -1) { + if (NMAPSendCommandF(session->store.conn, "SEARCH %" PRIx64 " %s \"%s\"\r\n", message->guid, subcommand, searchString) != -1) { if ((ccode = NMAPReadResponse(session->store.conn, session->store.response, sizeof(session->store.response), TRUE)) == 1000) { *found = FALSE; return(STATUS_CONTINUE); @@ -558,7 +558,7 @@ SearchRemainingMatchSubstring(ImapSession *session, MessageInformation *message, __inline static long -SearchRemainingMatches(ImapSession *session, SearchKey *key, SearchRemainingMatch searchRemainingFunction, char *arg1, void *arg2) +SearchRemainingMatches(ImapSession *session __attribute__((unused)), SearchKey *key __attribute__((unused)), SearchRemainingMatch searchRemainingFunction, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused))) { MessageInformation *message = &session->folder.selected.message[0]; long ccode; @@ -597,7 +597,7 @@ SearchRemainingMatches(ImapSession *session, SearchKey *key, SearchRemainingMatc __inline static long -MessageSetAnd(ImapSession *session, SearchKey *key, long *messageSet) +MessageSetAnd(ImapSession *session __attribute__((unused)), SearchKey *key __attribute__((unused)), long *messageSet) { long readNum = 0; long writeNum = 0; @@ -624,7 +624,7 @@ MessageSetAnd(ImapSession *session, SearchKey *key, long *messageSet) __inline static long -SubStringSearch(ImapSession *session, char **keyString, SearchKey *key, char *subcommand) +SubStringSearch(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused)), char *subcommand) { long ccode; char *utf8SearchString; @@ -645,19 +645,19 @@ SubStringSearch(ImapSession *session, char **keyString, SearchKey *key, char *su /* key handlers begin */ static long -SearchKeyHandlerAll(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerAll(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(STATUS_CONTINUE); } static long -SearchKeyHandlerAnswered(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerAnswered(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlag, NULL, (void *)STORE_MSG_FLAG_ANSWERED)); } static long -SearchKeyHandlerBcc(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerBcc(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; char *utf8SearchString; @@ -671,7 +671,7 @@ SearchKeyHandlerBcc(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerBefore(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerBefore(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { time_t date; long ccode; @@ -684,49 +684,49 @@ SearchKeyHandlerBefore(ImapSession *session, char **keyString, SearchKey *key) static long -SearchKeyHandlerBody(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerBody(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SubStringSearch(session, keyString, key, "BODY")); } static long -SearchKeyHandlerCc(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerCc(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SubStringSearch(session, keyString, key, "HEADER CC")); } static long -SearchKeyHandlerDeleted(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerDeleted(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlag, NULL, (void *)STORE_MSG_FLAG_DELETED)); } static long -SearchKeyHandlerDraft(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerDraft(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlag, NULL, (void *)STORE_MSG_FLAG_DRAFT)); } static long -SearchKeyHandlerFlagged(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerFlagged(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlag, NULL, (void *)STORE_MSG_FLAG_FLAGGED)); } static long -SearchKeyHandlerFrom(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerFrom(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SubStringSearch(session, keyString, key, "HEADER FROM")); } static long -SearchKeyHandlerHeader(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerHeader(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; - unsigned char *headerName; + char *headerName; char substring[SEARCH_MAX_UTF8_STRING]; - if((ccode = GrabArgumentEx(session, (unsigned char **)keyString, &headerName, TRUE)) == STATUS_CONTINUE) { + if((ccode = GrabArgumentEx(session, keyString, &headerName, TRUE)) == STATUS_CONTINUE) { snprintf(substring, sizeof(substring), "HEADER %s", headerName); MemFree(headerName); return(SubStringSearch(session, keyString, key, substring)); @@ -735,7 +735,7 @@ SearchKeyHandlerHeader(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerKeyword(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerKeyword(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; char *utf8SearchString; @@ -749,7 +749,7 @@ SearchKeyHandlerKeyword(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerLarger(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerLarger(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { unsigned long size; long ccode; @@ -761,13 +761,13 @@ SearchKeyHandlerLarger(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerNew(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerNew(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchNew, NULL, NULL)); } static long -SearchKeyHandlerNot(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerNot(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; SearchKey newKey; @@ -783,13 +783,13 @@ SearchKeyHandlerNot(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerOld(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerOld(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlagNot, NULL, (void *)STORE_MSG_FLAG_RECENT)); } static long -SearchKeyHandlerOn(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerOn(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { time_t date; long ccode; @@ -801,7 +801,7 @@ SearchKeyHandlerOn(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerOr(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerOr(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; SearchKey leftKey; @@ -829,19 +829,19 @@ SearchKeyHandlerOr(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerRecent(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerRecent(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlag, NULL, (void *)STORE_MSG_FLAG_RECENT)); } static long -SearchKeyHandlerSeen(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSeen(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlag, NULL, (void *)STORE_MSG_FLAG_SEEN)); } static long -SearchKeyHandlerSentBefore(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSentBefore(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { time_t date; long ccode; @@ -853,7 +853,7 @@ SearchKeyHandlerSentBefore(ImapSession *session, char **keyString, SearchKey *ke } static long -SearchKeyHandlerSentOn(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSentOn(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { time_t date; long ccode; @@ -865,7 +865,7 @@ SearchKeyHandlerSentOn(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerSentSince(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSentSince(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { time_t date; long ccode; @@ -877,7 +877,7 @@ SearchKeyHandlerSentSince(ImapSession *session, char **keyString, SearchKey *key } static long -SearchKeyHandlerSince(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSince(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { time_t date; long ccode; @@ -889,7 +889,7 @@ SearchKeyHandlerSince(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerSmaller(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSmaller(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { unsigned long size; long ccode; @@ -901,25 +901,25 @@ SearchKeyHandlerSmaller(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerSubject(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSubject(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SubStringSearch(session, keyString, key, "HEADER SUBJECT")); } static long -SearchKeyHandlerText(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerText(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SubStringSearch(session, keyString, key, "TEXT")); } static long -SearchKeyHandlerTo(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerTo(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SubStringSearch(session, keyString, key, "HEADER TO")); } static long -SearchKeyHandlerUid(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUid(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; long *messageSet; @@ -932,31 +932,31 @@ SearchKeyHandlerUid(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerUnanswered(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUnanswered(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlagNot, NULL, (void *)STORE_MSG_FLAG_ANSWERED)); } static long -SearchKeyHandlerUndeleted(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUndeleted(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlagNot, NULL, (void *)STORE_MSG_FLAG_DELETED)); } static long -SearchKeyHandlerUndraft(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUndraft(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlagNot, NULL, (void *)STORE_MSG_FLAG_DRAFT)); } static long -SearchKeyHandlerUnflagged(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUnflagged(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlagNot, NULL, (void *)STORE_MSG_FLAG_FLAGGED)); } static long -SearchKeyHandlerUnkeyword(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUnkeyword(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; char *utf8SearchString; @@ -970,7 +970,7 @@ SearchKeyHandlerUnkeyword(ImapSession *session, char **keyString, SearchKey *key } static long -SearchKeyHandlerUnseen(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUnseen(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlagNot, NULL, (void *)STORE_MSG_FLAG_SEEN)); } @@ -1017,7 +1017,7 @@ static SearchKeyInfo SearchKeyList[] = { }; long -SearchHandleKey(ImapSession *session, char **keyString, SearchKey *key) +SearchHandleKey(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long keyId; @@ -1034,7 +1034,7 @@ SearchHandleKey(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchHandleSubKey(ImapSession *session, char **keyString, SearchKey *key) +SearchHandleSubKey(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; @@ -1058,7 +1058,7 @@ SearchHandleSubKey(ImapSession *session, char **keyString, SearchKey *key) static long -SearchHandleTopKey(ImapSession *session, char **keyString, SearchKey *key) +SearchHandleTopKey(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; @@ -1080,7 +1080,7 @@ SearchHandleTopKey(ImapSession *session, char **keyString, SearchKey *key) } __inline static long -SearchSendResults(ImapSession *session, long *matches, BOOL byUid) +SearchSendResults(ImapSession *session __attribute__((unused)), long *matches, BOOL byUid) { MessageInformation *message = NULL; if (byUid) { @@ -1120,7 +1120,7 @@ SearchSendResults(ImapSession *session, long *matches, BOOL byUid) static long -SearchHandleCommand(ImapSession *session, char *parameters, BOOL byUid) +SearchHandleCommand(ImapSession *session __attribute__((unused)), char *parameters, BOOL byUid) { long ccode; char *ptr = parameters; @@ -1165,7 +1165,7 @@ ImapCommandSearch(void *param) { long ccode; - ImapSession *session = (ImapSession *)param; + ImapSession *session __attribute__((unused)) = (ImapSession *)param; if ((ccode = SearchHandleCommand(session, session->command.buffer + strlen("SEARCH "), FALSE)) == STATUS_CONTINUE) { return(SendOk(session, "SEARCH")); @@ -1179,7 +1179,7 @@ ImapCommandUidSearch(void *param) { long ccode; - ImapSession *session = (ImapSession *)param; + ImapSession *session __attribute__((unused)) = (ImapSession *)param; if ((ccode = SearchHandleCommand(session, session->command.buffer + strlen("UID SEARCH "), TRUE)) == STATUS_CONTINUE) { return(SendOk(session, "UID SEARCH")); diff --git a/src/agents/imap/session.c b/src/agents/imap/session.c index 9a76e68..717d4ce 100644 --- a/src/agents/imap/session.c +++ b/src/agents/imap/session.c @@ -26,13 +26,15 @@ #include #include +#include #include "imapd.h" static long -UserAuthenticate(ImapSession *session, unsigned char **userName, unsigned char *password, struct sockaddr_in *storeAddress) +UserAuthenticate(ImapSession *session, char **userName, char *password, struct sockaddr_in *storeAddress) { + UNUSED_PARAMETER(session) if (MsgAuthFindUser(*userName) != 0) return(STATUS_USERNAME_NOT_FOUND); @@ -43,10 +45,29 @@ UserAuthenticate(ImapSession *session, unsigned char **userName, unsigned char * return(STATUS_CONTINUE); } +static int +ImapSaslVerify(void *context, const char *user, const char *password, + size_t passwordLength) +{ + char *copy; + int valid; + UNUSED_PARAMETER(context) + if (!user || !password || !passwordLength || strlen(user) > MAXEMAILNAMESIZE) + return 0; + copy = MemMalloc(passwordLength + 1); + if (!copy) return 0; + memcpy(copy, password, passwordLength); + copy[passwordLength] = '\0'; + valid = MsgAuthFindUser(user) == 0 && MsgAuthVerifyPassword(user, copy) == 0; + memset(copy, 0, passwordLength); + MemFree(copy); + return valid; +} + __inline static long UserStoreConnect(ImapSession *session, struct sockaddr_in *store) { - unsigned char buffer[CONN_BUFSIZE + 1]; + char buffer[CONN_BUFSIZE + 1]; if ((session->store.conn = NMAPConnectEx(NULL, store, session->client.conn->trace.destination)) != NULL) { if (NMAPAuthenticateToStore(session->store.conn, buffer, sizeof(buffer))) { @@ -67,7 +88,7 @@ UserStoreConnect(ImapSession *session, struct sockaddr_in *store) } __inline static long -UserStoreInitialize(ImapSession *session, unsigned char *username) +UserStoreInitialize(ImapSession *session, char *username) { long ccode; @@ -99,7 +120,7 @@ UserStoreInitialize(ImapSession *session, unsigned char *username) } __inline static long -UserLogin(ImapSession *session, unsigned char **username, unsigned char *password) +UserLogin(ImapSession *session, char **username, char *password) { long ccode; struct sockaddr_in store; @@ -121,9 +142,9 @@ UserLogin(ImapSession *session, unsigned char **username, unsigned char *passwor /* in X500 spaces and underscores are treated the same in distinguished names */ __inline static void -X500Encode(unsigned char *in) +X500Encode(char *in) { - unsigned char *spacePtr; + char *spacePtr; do { spacePtr = strchr(in, ' '); @@ -137,7 +158,7 @@ X500Encode(unsigned char *in) __inline static long -SendLoginFailure(ImapSession *session, unsigned char *username, unsigned char *command, long result) +SendLoginFailure(ImapSession *session, char *username, char *command, long result) { if ((result == STATUS_WRONG_PASSWORD) || (result == STATUS_USERNAME_NOT_FOUND)) { XplDelay(2000); @@ -158,7 +179,7 @@ SendLoginFailure(ImapSession *session, unsigned char *username, unsigned char *c __inline static long -GetLoginCredentials(ImapSession *session, unsigned char **username, unsigned char **password) +GetLoginCredentials(ImapSession *session, char **username, char **password) { long ccode; @@ -167,13 +188,13 @@ GetLoginCredentials(ImapSession *session, unsigned char **username, unsigned cha if (ccode == STATUS_CONTINUE) { *username = MemStrdup(session->command.buffer); if (*username) { - DecodeBase64(*username); + DecodeBase64((char *)*username); if ((ConnWrite(session->client.conn, "+ UGFzc3dvcmQ6AA==\r\n", 20) != -1) && (ConnFlush(session->client.conn) != -1)) { ccode = ReadCommandLine(session->client.conn, &(session->command.buffer), &(session->command.bufferLen)); if (ccode == STATUS_CONTINUE) { *password = MemStrdup(session->command.buffer); if (*password) { - DecodeBase64(*password); + DecodeBase64((char *)*password); return(STATUS_CONTINUE); } ccode = STATUS_MEMORY_ERROR; @@ -197,38 +218,83 @@ int ImapCommandAuthenticate(void *param) { ImapSession *session = (ImapSession *)param; - long ccode; - unsigned char *username = NULL; - unsigned char *password = NULL; + BongoSaslSession *sasl = NULL; + const char *saslOutput = NULL, *saslUser; + char *mechanism, *initial = NULL, *space, *username = NULL; + char decoded[CONN_BUFSIZE + 1], encoded[CONN_BUFSIZE * 2]; + size_t decodedLength = 0, encodedLength = 0, saslOutputLength = 0; + struct sockaddr_in store; + long ccode = STATUS_ABORT; + int saslResult; - if (session->client.state == STATE_FRESH) { - if (XplStrNCaseCmp(session->command.buffer + 13, "LOGIN", 5) == 0) { - ccode = GetLoginCredentials(session, &username, &password); - if (ccode == STATUS_CONTINUE) { - X500Encode(username); - ccode = UserLogin(session, &username, password); - MemFree(password); - if (ccode == STATUS_CONTINUE) { - session->user.name = username; - session->client.state = STATE_AUTH; - ccode = SendOk(session,"AUTHENTICATE"); - if (ccode == STATUS_CONTINUE) { - /* We are flushing here so that we can update the connection manager */ - /* while the client is working on the response and sending the next command */ - if (ConnFlush(session->client.conn) != -1) { - return(STATUS_CONTINUE); - } - } - } - ccode = SendLoginFailure(session, username, "AUTHENTICATE", ccode); - MemFree(username); - return(ccode); - } - return(SendError(session->client.conn, session->command.tag, "AUTHENTICATE", ccode)); - } - return(SendError(session->client.conn, session->command.tag, "AUTHENTICATE", STATUS_UNKNOWN_AUTH)); + if (session->client.state != STATE_FRESH || !session->client.conn->ssl.enable) + return SendError(session->client.conn, session->command.tag, + "AUTHENTICATE", STATUS_INVALID_STATE); + mechanism = session->command.buffer + strlen(IMAP_COMMAND_AUTHENTICATE); + while (*mechanism == ' ') mechanism++; + if (!*mechanism) return SendError(session->client.conn, session->command.tag, + "AUTHENTICATE", STATUS_INVALID_ARGUMENT); + space = strchr(mechanism, ' '); + if (space) { + *space++ = '\0'; + while (*space == ' ') space++; + if (*space) initial = space; } - return(SendError(session->client.conn, session->command.tag, "AUTHENTICATE", STATUS_INVALID_STATE)); + if (!BongoSaslSessionCreate(&sasl, "imap", BongoGlobals.hostname, + NULL, NULL, 1, ImapSaslVerify, session)) + return SendError(session->client.conn, session->command.tag, + "AUTHENTICATE", STATUS_NMAP_COMM_ERROR); + if (initial && strcmp(initial, "=")) { + if (!BongoSaslDecode64(initial, strlen(initial), decoded, sizeof(decoded), + &decodedLength)) goto authentication_failed; + } + saslResult = BongoSaslStart(sasl, mechanism, initial ? decoded : NULL, + decodedLength, (const void **)&saslOutput, + &saslOutputLength); + while (saslResult == BONGO_SASL_CONTINUE) { + if (saslOutputLength && + !BongoSaslEncode64(saslOutput, saslOutputLength, encoded, + sizeof(encoded), &encodedLength)) goto finish; + if (ConnWriteF(session->client.conn, "+ %.*s\r\n", (int)encodedLength, + saslOutputLength ? encoded : "") == -1 || + ConnFlush(session->client.conn) == -1 || + ReadCommandLine(session->client.conn, &session->command.buffer, + &session->command.bufferLen) != STATUS_CONTINUE) goto finish; + if (!strcmp(session->command.buffer, "*")) { + ccode = SendError(session->client.conn, session->command.tag, + "AUTHENTICATE", STATUS_ABORT); + goto finish; + } + if (!BongoSaslDecode64(session->command.buffer, session->command.bufferLen, + decoded, sizeof(decoded), &decodedLength)) + goto authentication_failed; + saslResult = BongoSaslStep(sasl, decoded, decodedLength, + (const void **)&saslOutput, &saslOutputLength); + } + if (saslResult != BONGO_SASL_COMPLETE || + !(saslUser = BongoSaslUsername(sasl)) || !(username = MemStrdup(saslUser))) + goto authentication_failed; + X500Encode(username); + MsgAuthGetUserStore(username, &store); + ccode = UserStoreConnect(session, &store); + if (ccode == STATUS_CONTINUE) ccode = UserStoreInitialize(session, username); + if (ccode == STATUS_CONTINUE) { + Log(LOG_INFO, "Successful SASL login for user %s", username); + session->user.name = username; + username = NULL; + session->client.state = STATE_AUTH; + ccode = SendOk(session, "AUTHENTICATE"); + if (ccode == STATUS_CONTINUE && ConnFlush(session->client.conn) == -1) + ccode = STATUS_ABORT; + goto finish; + } +authentication_failed: + ccode = SendLoginFailure(session, username ? username : "", "AUTHENTICATE", + STATUS_WRONG_PASSWORD); +finish: + MemFree(username); + BongoSaslSessionDestroy(&sasl); + return ccode; } int @@ -236,10 +302,10 @@ ImapCommandLogin(void *param) { ImapSession *session = (ImapSession *)param; long ccode; - unsigned char *username = NULL; - unsigned char *password = NULL; + char *username = NULL; + char *password = NULL; - if (session->client.state == STATE_FRESH) { + if (session->client.state == STATE_FRESH && session->client.conn->ssl.enable) { ccode = GrabTwoArguments(session, session->command.buffer + 6, &username, &password); if (ccode == STATUS_CONTINUE) { X500Encode(username); diff --git a/src/agents/imap/store.c b/src/agents/imap/store.c index e438d02..18d4186 100644 --- a/src/agents/imap/store.c +++ b/src/agents/imap/store.c @@ -44,9 +44,9 @@ static StoreCommandType StoreCommandTypes[] = { }; unsigned long -ParseStoreFlags(unsigned char *command, unsigned long *returnFlags) +ParseStoreFlags(char *command, unsigned long *returnFlags) { - unsigned char *ptr; + char *ptr; unsigned long flags; long ccode; @@ -172,12 +172,12 @@ DoStoreForMessageSet(ImapSession *session, char *messageSet, unsigned long Flags __inline static long HandleStore(ImapSession *session, BOOL ByUID, BOOL *purgedMessage) { - unsigned char *messageSet; + char *messageSet; long ccode; long keywordId; - unsigned char *ptr; - unsigned char *type; - unsigned char *flagString; + char *ptr; + char *type; + char *flagString; unsigned long flags; if ((ccode = CheckState(session, STATE_SELECTED)) == STATUS_CONTINUE) { diff --git a/src/agents/imap/store.h b/src/agents/imap/store.h index f01f861..66171e6 100644 --- a/src/agents/imap/store.h +++ b/src/agents/imap/store.h @@ -21,17 +21,16 @@ typedef struct { - unsigned char *string; + const char *string; unsigned long stringLen; unsigned long value; } StoreCommandFlag; typedef struct { - unsigned char *string; - unsigned char *nmapCommand; + const char *string; + char *nmapCommand; BOOL silent; } StoreCommandType; -unsigned long ParseStoreFlags(unsigned char *command, unsigned long *returnFlags); - +unsigned long ParseStoreFlags(char *command, unsigned long *returnFlags); diff --git a/src/agents/imap/uid.c b/src/agents/imap/uid.c index e10f034..3d1f2d8 100644 --- a/src/agents/imap/uid.c +++ b/src/agents/imap/uid.c @@ -88,7 +88,7 @@ UidToSequenceNum(MessageInformation *message, unsigned long messageCount, unsign __inline static long -UidToSequenceRange(MessageInformation *message, unsigned long messageCount, unsigned long requestUidStart, unsigned long requestUidEnd, long *startNum, long *endNum) +UidToSequenceRange(MessageInformation *message, unsigned long messageCount, unsigned long requestUidStart, unsigned long requestUidEnd, unsigned long *startNum, unsigned long *endNum) { if (messageCount != 0) { unsigned long lastMessageId; @@ -289,5 +289,14 @@ GetMessageRange(MessageInformation *message, unsigned long messageCount, char ** long TestUidToSequenceRange(MessageInformation *message, unsigned long messageCount, unsigned long requestUidStart, unsigned long requestUidEnd, long *startNum, long *endNum) { - return(UidToSequenceRange(message, messageCount, requestUidStart, requestUidEnd, startNum, endNum)); + unsigned long unsignedStart; + unsigned long unsignedEnd; + long result = UidToSequenceRange(message, messageCount, requestUidStart, + requestUidEnd, &unsignedStart, &unsignedEnd); + + if (result == STATUS_CONTINUE) { + *startNum = (long)unsignedStart; + *endNum = (long)unsignedEnd; + } + return result; } diff --git a/src/agents/pop/CMakeLists.txt b/src/agents/pop/CMakeLists.txt index 210c5dd..b24f100 100644 --- a/src/agents/pop/CMakeLists.txt +++ b/src/agents/pop/CMakeLists.txt @@ -8,6 +8,7 @@ target_link_libraries(bongopop3 bongoutil bongojson bongomsgapi + bongosasl ) install(TARGETS bongopop3 DESTINATION ${SBIN_INSTALL_DIR}) diff --git a/src/agents/pop/pop3.c b/src/agents/pop/pop3.c index a4073e1..645bfcc 100644 --- a/src/agents/pop/pop3.c +++ b/src/agents/pop/pop3.c @@ -36,6 +36,7 @@ #include #include #include +#include #include @@ -98,7 +99,7 @@ static void SignalHandler(int sigtype); } else if (XplSafeRead(POP3.client.worker.active) < XplSafeRead(POP3.client.worker.maximum)) { \ XplSafeIncrement(POP3.client.worker.active); \ XplSignalBlock(); \ - XplBeginThread(&(id), HandleConnection, POP_STACK_SPACE, (char *)(XplSafeRead(POP3.client.worker.active)), (r)); \ + XplBeginThread(&(id), HandleConnection, POP_STACK_SPACE, (void *)(intptr_t)XplSafeRead(POP3.client.worker.active), (r)); \ XplSignalHandler(SignalHandler); \ if (!(r)) { \ (c)->queue.previous = NULL; \ @@ -166,10 +167,10 @@ typedef struct _POP3Client { ProtocolCommand *handler; - unsigned char user[MAXEMAILNAMESIZE + 1]; + char user[MAXEMAILNAMESIZE + 1]; - unsigned char command[CONN_BUFSIZE + 1]; - unsigned char buffer[CONN_BUFSIZE + 1]; + char command[CONN_BUFSIZE + 1]; + char buffer[CONN_BUFSIZE + 1]; unsigned long messageCount; unsigned long messagesDeleted; @@ -182,6 +183,11 @@ typedef struct _POP3Client { int loginCount; } POP3Client; +static int POP3OpenMailbox(POP3Client *client); +static int POP3SaslVerify(void *context, const char *user, + const char *password, size_t password_length); +static BOOL LoadIDList(POP3Client *client); + struct { enum POP3States state; @@ -200,6 +206,7 @@ struct { struct { struct { BOOL enable; + BOOL allowLegacyTLS; ConnSSLConfiguration config; @@ -245,8 +252,8 @@ struct { Connection *conn; } ssl; - unsigned char address[80]; - unsigned char hash[NMAP_HASH_SIZE]; + char address[80]; + char hash[NMAP_HASH_SIZE]; } nmap; struct { @@ -257,7 +264,7 @@ struct { XplAtomic badPasswords; } stats; - unsigned char managementURL[256]; + char managementURL[256]; void *loggingHandle; @@ -285,29 +292,31 @@ static int POP3CommandUser(void *param); static int POP3CommandUIDL(void *param); static int POP3CommandXTND(void *param); +#define POP_COMMAND(name, callback) \ + { name, NULL, sizeof(name) - 1, callback, NULL, NULL, NULL, 0 } static ProtocolCommand POP3CommandEntries[] = { - { POP3_COMMAND_APOP, NULL, sizeof(POP3_COMMAND_APOP) - 1, POP3CommandAPOP, NULL, NULL }, - { POP3_COMMAND_AUTH, NULL, sizeof(POP3_COMMAND_AUTH) - 1, POP3CommandAuth, NULL, NULL }, - { POP3_COMMAND_CAPA, NULL, sizeof(POP3_COMMAND_CAPA) - 1, POP3CommandCapa, NULL, NULL }, - { POP3_COMMAND_DELE, NULL, sizeof(POP3_COMMAND_DELE) - 1, POP3CommandDele, NULL, NULL }, - { POP3_COMMAND_GURL, NULL, sizeof(POP3_COMMAND_GURL) - 1, POP3CommandGURL, NULL, NULL }, - { POP3_COMMAND_LAST, NULL, sizeof(POP3_COMMAND_LAST) - 1, POP3CommandLast, NULL, NULL }, - { POP3_COMMAND_LIST, NULL, sizeof(POP3_COMMAND_LIST) - 1, POP3CommandList, NULL, NULL }, - { POP3_COMMAND_NOOP, NULL, sizeof(POP3_COMMAND_NOOP) - 1, POP3CommandNoop, NULL, NULL }, - { POP3_COMMAND_PASS, NULL, sizeof(POP3_COMMAND_PASS) - 1, POP3CommandPass, NULL, NULL }, - { POP3_COMMAND_QUIT, NULL, sizeof(POP3_COMMAND_QUIT) - 1, POP3CommandQuit, NULL, NULL }, - { POP3_COMMAND_RETR, NULL, sizeof(POP3_COMMAND_RETR) - 1, POP3CommandRetr, NULL, NULL }, - { POP3_COMMAND_RSET, NULL, sizeof(POP3_COMMAND_RSET) - 1, POP3CommandRSet, NULL, NULL }, - { POP3_COMMAND_STARTTLS, NULL, sizeof(POP3_COMMAND_STARTTLS) - 1, POP3CommandSTLS, NULL, NULL }, - { POP3_COMMAND_STAT, NULL, sizeof(POP3_COMMAND_STAT) - 1, POP3CommandStat, NULL, NULL }, - { POP3_COMMAND_STLS, NULL, sizeof(POP3_COMMAND_STLS) - 1, POP3CommandSTLS, NULL, NULL }, - { POP3_COMMAND_TOP, NULL, sizeof(POP3_COMMAND_TOP) - 1, POP3CommandTop, NULL, NULL }, - { POP3_COMMAND_USER, NULL, sizeof(POP3_COMMAND_USER) - 1, POP3CommandUser, NULL, NULL }, - { POP3_COMMAND_UIDL, NULL, sizeof(POP3_COMMAND_UIDL) - 1, POP3CommandUIDL, NULL, NULL }, - { POP3_COMMAND_XTND, NULL, sizeof(POP3_COMMAND_XTND) - 1, POP3CommandXTND, NULL, NULL }, - - { NULL, NULL, 0, NULL, NULL, NULL } + POP_COMMAND(POP3_COMMAND_APOP, POP3CommandAPOP), + POP_COMMAND(POP3_COMMAND_AUTH, POP3CommandAuth), + POP_COMMAND(POP3_COMMAND_CAPA, POP3CommandCapa), + POP_COMMAND(POP3_COMMAND_DELE, POP3CommandDele), + POP_COMMAND(POP3_COMMAND_GURL, POP3CommandGURL), + POP_COMMAND(POP3_COMMAND_LAST, POP3CommandLast), + POP_COMMAND(POP3_COMMAND_LIST, POP3CommandList), + POP_COMMAND(POP3_COMMAND_NOOP, POP3CommandNoop), + POP_COMMAND(POP3_COMMAND_PASS, POP3CommandPass), + POP_COMMAND(POP3_COMMAND_QUIT, POP3CommandQuit), + POP_COMMAND(POP3_COMMAND_RETR, POP3CommandRetr), + POP_COMMAND(POP3_COMMAND_RSET, POP3CommandRSet), + POP_COMMAND(POP3_COMMAND_STARTTLS, POP3CommandSTLS), + POP_COMMAND(POP3_COMMAND_STAT, POP3CommandStat), + POP_COMMAND(POP3_COMMAND_STLS, POP3CommandSTLS), + POP_COMMAND(POP3_COMMAND_TOP, POP3CommandTop), + POP_COMMAND(POP3_COMMAND_USER, POP3CommandUser), + POP_COMMAND(POP3_COMMAND_UIDL, POP3CommandUIDL), + POP_COMMAND(POP3_COMMAND_XTND, POP3CommandXTND), + { NULL, NULL, 0, NULL, NULL, NULL, NULL, 0 } }; +#undef POP_COMMAND unsigned long POP3ServerPort = POP3_PORT; unsigned long POP3ServerPortSSL = POP3_PORT_SSL; @@ -427,6 +436,7 @@ static BOOL POP3ClientAllocCB(void *buffer, void *clientData) { POP3Client *c = (POP3Client *)buffer; + UNUSED_PARAMETER(clientData) memset(c, 0, sizeof(POP3Client)); @@ -450,7 +460,7 @@ POP3ReturnClientPoolEntry(POP3Client *client) } static int -ConnectUserToNMAPServer(POP3Client *client, unsigned char *username, unsigned char *password) +ConnectUserToNMAPServer(POP3Client *client, char *username, char *password) { BOOL result; struct sockaddr_in nmap; @@ -487,6 +497,55 @@ ConnectUserToNMAPServer(POP3Client *client, unsigned char *username, unsigned ch return(POP3_NMAP_SERVER_DOWN); } +static int +POP3SaslVerify(void *context, const char *user, const char *password, + size_t password_length) +{ + POP3Client *client = context; + char *copy; + int valid; + if (!client || !user || !password || !password_length || + strlen(user) >= sizeof(client->user)) return 0; + copy = MemMalloc(password_length + 1); + if (!copy) return 0; + memcpy(copy, password, password_length); + copy[password_length] = '\0'; + valid = MsgAuthFindUser(user) == 0 && MsgAuthVerifyPassword(user, copy) == 0; + memset(copy, 0, password_length); + MemFree(copy); + return valid; +} + +static int +POP3OpenMailbox(POP3Client *client) +{ + BOOL authenticated; + if (!client->store) { + client->store = NMAPConnectEx("127.0.0.1", NULL, + client->conn->trace.destination); + if (!client->store) return POP3_NMAP_SERVER_DOWN; + authenticated = NMAPAuthenticateToStore(client->store, (char *)client->buffer, + CONN_BUFSIZE); + if (!authenticated) { + NMAPQuit(client->store); + ConnFree(client->store); + client->store = NULL; + return POP3_NMAP_SERVER_DOWN; + } + } + if (NMAPSendCommandF(client->store, "USER %s\r\n", client->user) == -1 || + NMAPReadAnswer(client->store, (char *)client->buffer, CONN_BUFSIZE, TRUE) != NMAP_OK || + NMAPSendCommandF(client->store, "STORE %s\r\n", client->user) == -1 || + NMAPReadAnswer(client->store, (char *)client->buffer, CONN_BUFSIZE, TRUE) != NMAP_OK || + !LoadIDList(client)) { + NMAPQuit(client->store); + ConnFree(client->store); + client->store = NULL; + return POP3_NMAP_SERVER_DOWN; + } + return 0; +} + static BOOL LoadIDList(POP3Client *client) { @@ -540,7 +599,7 @@ LoadIDList(POP3Client *client) } } - result = sscanf(client->buffer, "%*u %llx %u %u %*x %*u %lu %*s", &guid, &type, &flags, &size); + result = sscanf(client->buffer, "%*u %" SCNx64 " %u %u %*x %*u %lu %*s", &guid, &type, &flags, &size); if(result != 4) { /* sscanf failed - must have been passed a bad LIST string - bail out! */ ccode = -1; @@ -598,8 +657,111 @@ static int POP3CommandAuth(void *param) { POP3Client *client = (POP3Client *)param; + BongoSaslSession *session = NULL; + const char *mechanisms = NULL, *username, *output = NULL; + char *mechanism, *initial = NULL, *space; + char decoded[CONN_BUFSIZE + 1]; + char encoded[CONN_BUFSIZE * 2]; + size_t decodedLength = 0, encodedLength = 0, outputLength = 0; + int result = BONGO_SASL_FAIL, ccode = -1; - return(ConnWrite(client->conn, MSGERRNOTSUPPORTED, sizeof(MSGERRNOTSUPPORTED) - 1)); + if (client->state != POP3_CLIENT_FRESH) + return ConnWrite(client->conn, MSGERRBADSTATE, sizeof(MSGERRBADSTATE) - 1); + /* Password-bearing mechanisms are deliberately unavailable before TLS. */ + if (!client->conn->ssl.enable) + return ConnWrite(client->conn, "-ERR [AUTH] Encryption required\r\n", + sizeof("-ERR [AUTH] Encryption required\r\n") - 1); + if (!BongoSaslSessionCreate(&session, "pop", BongoGlobals.hostname, + NULL, NULL, 1, POP3SaslVerify, client)) + return ConnWrite(client->conn, "-ERR [SYS/TEMP] Authentication unavailable\r\n", + sizeof("-ERR [SYS/TEMP] Authentication unavailable\r\n") - 1); + if (client->command[4] == '\0') { + char *list, *token, *save = NULL; + if (!BongoSaslMechanisms(session, &mechanisms) || + !(list = MemStrdup(mechanisms))) goto finish; + ccode = ConnWrite(client->conn, "+OK Supported SASL mechanisms\r\n", + sizeof("+OK Supported SASL mechanisms\r\n") - 1); + for (token = strtok_r(list, " ", &save); ccode != -1 && token; + token = strtok_r(NULL, " ", &save)) + ccode = ConnWriteF(client->conn, "%s\r\n", token); + MemFree(list); + if (ccode != -1) ccode = ConnWrite(client->conn, ".\r\n", 3); + goto finish; + } + if (client->command[4] != ' ') { + ccode = ConnWrite(client->conn, MSGERRSYNTAX, sizeof(MSGERRSYNTAX) - 1); + goto finish; + } + mechanism = client->command + 5; + while (*mechanism == ' ') mechanism++; + space = strchr(mechanism, ' '); + if (space) { + *space++ = '\0'; + while (*space == ' ') space++; + initial = space; + } + if (!*mechanism || strchr((char *)mechanism, '\r') || strchr((char *)mechanism, '\n')) { + ccode = ConnWrite(client->conn, MSGERRSYNTAX, sizeof(MSGERRSYNTAX) - 1); + goto finish; + } + if (initial && strcmp(initial, "=")) { + if (!BongoSaslDecode64(initial, strlen(initial), decoded, + sizeof(decoded), &decodedLength)) goto authentication_failed; + } + result = BongoSaslStart(session, mechanism, + initial ? decoded : NULL, decodedLength, + (const void **)&output, &outputLength); + while (result == BONGO_SASL_CONTINUE) { + if (outputLength && !BongoSaslEncode64(output, outputLength, encoded, + sizeof(encoded), &encodedLength)) + goto authentication_failed; + if (ConnWriteF(client->conn, "+ %.*s\r\n", (int)encodedLength, + outputLength ? encoded : "") == -1 || + ConnFlush(client->conn) == -1 || + ConnReadAnswer(client->conn, client->buffer, CONN_BUFSIZE) == -1) goto finish; + if (!strcmp(client->buffer, "*")) { + ccode = ConnWrite(client->conn, "-ERR [AUTH] Authentication cancelled\r\n", + sizeof("-ERR [AUTH] Authentication cancelled\r\n") - 1); + goto finish; + } + if (!BongoSaslDecode64(client->buffer, strlen(client->buffer), + decoded, sizeof(decoded), &decodedLength)) + goto authentication_failed; + result = BongoSaslStep(session, decoded, decodedLength, + (const void **)&output, &outputLength); + } + if (result != BONGO_SASL_COMPLETE || !(username = BongoSaslUsername(session)) || + strlen(username) >= sizeof(client->user)) goto authentication_failed; + strcpy(client->user, username); + if (!POP3OpenMailbox(client)) { + client->state = POP3_CLIENT_TRANSACTION; + Log(LOG_INFO, "User %s logged in with SASL from host %s", client->user, + LOGIP(client->conn->socketAddress)); + ccode = ConnWrite(client->conn, MSGOK, sizeof(MSGOK) - 1); + goto finish; + } + ccode = ConnWrite(client->conn, "-ERR [SYS/TEMP] Mailbox unavailable\r\n", + sizeof("-ERR [SYS/TEMP] Mailbox unavailable\r\n") - 1); + goto finish; + +authentication_failed: + XplSafeIncrement(POP3.stats.badPasswords); + client->loginCount++; + XplDelay(2000); + if (client->loginCount >= MAX_LOGIN_FAILURES) { + ccode = ConnWrite(client->conn, MSGERRNOPERMISSIONCLOSING, + sizeof(MSGERRNOPERMISSIONCLOSING) - 1); + ConnFlush(client->conn); + ConnClose(client->conn); + ConnFree(client->conn); + client->conn = NULL; + } else { + ccode = ConnWrite(client->conn, MSGERRNOPERMISSION, + sizeof(MSGERRNOPERMISSION) - 1); + } +finish: + BongoSaslSessionDestroy(&session); + return ccode; } #if 0 @@ -633,7 +795,7 @@ POP3CommandAuth(void *param) if (ccode == -1) break; /* fixme - using static salts? */ - DecodeBase64(client->buffer); + DecodeBase64((char *)client->buffer); ccode = ConnWrite(client->conn, "+ UGFzc3dvcmQ6AA==\r\n", 20); if (ccode == -1) break; @@ -644,7 +806,7 @@ POP3CommandAuth(void *param) ccode = ConnReadAnswer(client->conn, client->command, CONN_BUFSIZE); if (ccode == -1) break; - DecodeBase64(client->command); + DecodeBase64((char *)client->command); strcpy(client->user, client->buffer); @@ -744,12 +906,22 @@ POP3CommandCapa(void *param) { int ccode; POP3Client *client = (POP3Client *)param; + BongoSaslSession *session = NULL; + const char *mechanisms = NULL; - if (POP3.server.ssl.enable) { - ccode = ConnWrite(client->conn, MSGCAPALIST, sizeof(MSGCAPALIST) - 1); - } else { - ccode = ConnWrite(client->conn, MSGCAPALIST_NOTLS, sizeof(MSGCAPALIST_NOTLS) - 1); - } + ccode = ConnWrite(client->conn, "+OK\r\nTOP\r\nRESP-CODES\r\nAUTH-RESP-CODE\r\n" + "PIPELINING\r\nEXPIRE NEVER\r\nUIDL\r\n", + sizeof("+OK\r\nTOP\r\nRESP-CODES\r\nAUTH-RESP-CODE\r\n" + "PIPELINING\r\nEXPIRE NEVER\r\nUIDL\r\n") - 1); + if (ccode != -1 && client->conn->ssl.enable && + BongoSaslSessionCreate(&session, "pop", BongoGlobals.hostname, + NULL, NULL, 1, POP3SaslVerify, client) && + BongoSaslMechanisms(session, &mechanisms)) + ccode = ConnWriteF(client->conn, "SASL %s\r\nUSER\r\n", mechanisms); + if (ccode != -1 && !client->conn->ssl.enable) + ccode = ConnWrite(client->conn, "STLS\r\n", 6); + if (ccode != -1) ccode = ConnWrite(client->conn, ".\r\n", 3); + BongoSaslSessionDestroy(&session); return(ccode); } @@ -891,7 +1063,7 @@ static int POP3CommandPass(void *param) { int ccode; - unsigned char *ptr; + char *ptr; POP3Client *client = (POP3Client *)param; if (client->state == POP3_CLIENT_AUTHORIZATION) { @@ -933,69 +1105,8 @@ POP3CommandPass(void *param) return(-1); } - do - { - ccode = NMAPSendCommandF(client->store, "USER %s\r\n", client->user); - if (ccode == -1) { - ConnWriteF(client->conn, "%s %d\r\n", MSGERRNMAP, ccode); - break; - } - - ccode = NMAPReadAnswer(client->store, client->buffer, CONN_BUFSIZE, TRUE); - switch(ccode) { - case 3010: - case 3241: { - ConnWrite(client->conn, MSGERRNOBOX, sizeof(MSGERRNOBOX) - 1); - break; - } - - case 1000: { - ccode = 0; - break; - } - - default: { - ConnWriteF(client->conn, "-ERR [SYS/PERM] %s %s\r\n", BongoGlobals.hostname, MSGERRNONMAP); - break; - } - } - - if (ccode) break; - - ccode = NMAPSendCommandF(client->store, "STORE %s\r\n", client->user); - if (ccode == -1) { - ccode = ConnWriteF(client->conn, "%s %d\r\n", MSGERRNMAP, ccode); - break; - } - - ccode = NMAPReadAnswer(client->store, client->buffer, CONN_BUFSIZE, TRUE); - switch(ccode) { - case 3010: - case 3241: { - ConnWrite(client->conn, MSGERRNOBOX, sizeof(MSGERRNOBOX) - 1); - break; - } - - case 1000: { - ccode = 0; - break; - } - - default: { - ConnWriteF(client->conn, "-ERR [SYS/PERM] %s %s\r\n", BongoGlobals.hostname, MSGERRNONMAP); - break; - } - } - - if (ccode) break; - - if (!LoadIDList(client)) { - ccode = -1; - ConnWriteF(client->conn, "%s %d\r\n", MSGERRNMAP, ccode); - break; - } - ccode = 0; - } while (0); + ccode = POP3OpenMailbox(client); + if (ccode) ConnWriteF(client->conn, "%s %d\r\n", MSGERRNMAP, ccode); if (ccode == 0) { client->state = POP3_CLIENT_TRANSACTION; @@ -1025,7 +1136,7 @@ POP3CommandQuit(void *param) if(client->message[id].deleted) { if (ccode != -1) { - ccode = NMAPSendCommandF(client->store, "DELETE %llx\r\n", client->message[id].guid); + ccode = NMAPSendCommandF(client->store, "DELETE %" PRIx64 "\r\n", client->message[id].guid); } if (ccode != -1) { @@ -1037,6 +1148,7 @@ POP3CommandQuit(void *param) lastccode = ccode; } } + break; } } } @@ -1062,7 +1174,7 @@ POP3CommandRetr(void *param) long length; long tmp; unsigned long id; - unsigned char *ptr; + char *ptr; POP3Client *client = (POP3Client *)param; if (client->state == POP3_CLIENT_TRANSACTION) { @@ -1079,7 +1191,7 @@ POP3CommandRetr(void *param) return(ConnWrite(client->conn, MSGERRNOMSG, sizeof(MSGERRNOMSG) - 1)); } - ccode = NMAPSendCommandF(client->store, "READ %llx\r\n", client->message[id].guid); + ccode = NMAPSendCommandF(client->store, "READ %" PRIx64 "\r\n", client->message[id].guid); if (ccode != -1) { ccode = NMAPReadAnswer(client->store, client->buffer, CONN_BUFSIZE, TRUE); @@ -1133,7 +1245,7 @@ POP3CommandRetr(void *param) if (ccode != -1) { /* Mark as read */ - ccode = NMAPSendCommandF(client->store, "FLAG %llx +%lu\r\n", client->message[id].guid, (long unsigned int)STORE_MSG_FLAG_SEEN); + ccode = NMAPSendCommandF(client->store, "FLAG %" PRIx64 " +%lu\r\n", client->message[id].guid, (long unsigned int)STORE_MSG_FLAG_SEEN); } if (ccode != -1) { @@ -1142,6 +1254,7 @@ POP3CommandRetr(void *param) break; } + break; } case 4220: { @@ -1210,7 +1323,7 @@ POP3CommandSTLS(void *param) BOOL result; POP3Client *client = (POP3Client *)param; - if (client->state == POP3_CLIENT_FRESH) { + if (client->state == POP3_CLIENT_FRESH && !client->conn->ssl.enable) { if (POP3.server.ssl.enable) { ccode = ConnWrite(client->conn, MSGOKSTARTTLS, sizeof(MSGOKSTARTTLS) - 1); } else { @@ -1248,8 +1361,8 @@ POP3CommandTop(void *param) long length; long lines; unsigned long id; - unsigned long headerSize; - unsigned char *ptr; + long headerSize; + char *ptr; POP3Client *client = (POP3Client *)param; if (client->state == POP3_CLIENT_TRANSACTION) { @@ -1271,11 +1384,11 @@ POP3CommandTop(void *param) /* Read message header size */ ccode = NMAPGetDecimalProperty(client->store, client->message[id].guid, "nmap.mail.headersize", &headerSize); - if (ccode == -1) + if (ccode == -1 || headerSize < 0 || (unsigned long)headerSize > client->message[id].size) return (ConnWriteF(client->conn, "%s %d\r\n", MSGERRNMAP, ccode)); /* Read the message header */ - ccode = NMAPSendCommandF(client->store, "READ %llx 0 %lu\r\n", client->message[id].guid, headerSize); + ccode = NMAPSendCommandF(client->store, "READ %" PRIx64 " 0 %ld\r\n", client->message[id].guid, headerSize); if (ccode != -1) { ccode = NMAPReadPropertyValueLength(client->store, "nmap.document", &count); switch(ccode) { @@ -1306,7 +1419,9 @@ POP3CommandTop(void *param) } if (lines) { - ccode = NMAPSendCommandF(client->store, "READ %llx %lu %lu\r\n", client->message[id].guid, headerSize, (client->message[id].size - headerSize)); + ccode = NMAPSendCommandF(client->store, "READ %" PRIx64 " %ld %lu\r\n", + client->message[id].guid, headerSize, + client->message[id].size - (unsigned long)headerSize); if (ccode != -1) { ccode = NMAPReadPropertyValueLength(client->store, "nmap.document", &count); switch(ccode) { @@ -1382,7 +1497,7 @@ POP3CommandUIDL(void *param) return(ConnWrite(client->conn, MSGERRNOMSG, sizeof(MSGERRNOMSG) - 1)); } - ccode = ConnWriteF(client->conn, "+OK %ld %llx\r\n", id + 1, client->message[id].guid); + ccode = ConnWriteF(client->conn, "+OK %ld %" PRIx64 "\r\n", id + 1, client->message[id].guid); return(ccode); } @@ -1394,7 +1509,7 @@ POP3CommandUIDL(void *param) if(client->message[id].deleted) { continue; } - ccode = ConnWriteF(client->conn, "%lu %llx\r\n", id + 1, client->message[id].guid); + ccode = ConnWriteF(client->conn, "%lu %" PRIx64 "\r\n", id + 1, client->message[id].guid); } ccode = ConnWrite(client->conn, ".\r\n", 3); } @@ -1407,12 +1522,12 @@ static int POP3CommandUser(void *param) { int ccode; - unsigned char *ptr; - unsigned char *dest; - unsigned char *limit; + char *ptr; + char *dest; + char *limit; POP3Client *client = (POP3Client *)param; - if (client->state == POP3_CLIENT_FRESH) { + if (client->state == POP3_CLIENT_FRESH && client->conn->ssl.enable) { ptr = client->command + 5; while (isspace(*ptr)) { ptr++; @@ -1461,7 +1576,7 @@ static BOOL HandleConnection(void *param) { int ccode; - long threadNumber = (long)param; + long threadNumber = (long)(intptr_t)param; BOOL result; time_t last = time(NULL); time_t current; @@ -1678,6 +1793,7 @@ static void POP3Server(void *ignored) { int ccode; + UNUSED_PARAMETER(ignored) XplThreadID id; Connection *conn; @@ -1867,6 +1983,7 @@ static void POP3SSLServer(void *ignored) { int ccode; + UNUSED_PARAMETER(ignored) XplThreadID id; Connection *conn; @@ -1997,6 +2114,7 @@ client->CSSL = NULL; static BongoConfigItem POP3Config[] = { { BONGO_JSON_INT, "o:port/i", &POP3ServerPort }, { BONGO_JSON_INT, "o:port_ssl/i", &POP3ServerPortSSL }, + { BONGO_JSON_BOOL, "o:allow_legacy_tls/b", &POP3.server.ssl.allowLegacyTLS }, { BONGO_JSON_NULL, NULL, NULL } }; @@ -2004,7 +2122,6 @@ static BOOL ReadConfiguration(void) { POP3.server.ssl.config.options |= SSL_ALLOW_CHAIN; - POP3.server.ssl.config.options |= SSL_ALLOW_SSL3; POP3.server.ssl.enable = TRUE; POP3.nmap.ssl.enable = FALSE; // FIXME: why? @@ -2029,6 +2146,8 @@ XplServiceMain(int argc, char *argv[]) { int ccode; XplThreadID id; + UNUSED_PARAMETER(argc) + UNUSED_PARAMETER(argv) if (XplSetEffectiveUser(MsgGetUnprivilegedUser()) < 0) { Log(LOG_ERROR, "Could not drop to unprivileged user '%s'.", MsgGetUnprivilegedUser()); @@ -2054,6 +2173,7 @@ XplServiceMain(int argc, char *argv[]) POP3.server.ssl.enable = FALSE; POP3.server.ssl.context = NULL; POP3.server.ssl.config.options = 0; + POP3.server.ssl.allowLegacyTLS = FALSE; POP3.loggingHandle = NULL; @@ -2086,6 +2206,10 @@ XplServiceMain(int argc, char *argv[]) MsgInit(); MsgAuthInit(); + if (!BongoSaslInitialize()) { + Log(LOG_ERROR, "Unable to initialize Cyrus SASL"); + return -1; + } NMAPInitialize(); LogOpen("bongopop3"); @@ -2103,6 +2227,9 @@ XplServiceMain(int argc, char *argv[]) if (POP3.server.ssl.enable) { if (!ServerSocketSSLInit()) { + if (POP3.server.ssl.allowLegacyTLS) { + POP3.server.ssl.config.options |= SSL_ALLOW_LEGACY_PROTOCOLS; + } POP3.server.ssl.config.certificate.file = MsgGetFile(MSGAPI_FILE_PUBKEY, NULL, 0); POP3.server.ssl.config.key.file = MsgGetFile(MSGAPI_FILE_PRIVKEY, NULL, 0); POP3.server.ssl.config.key.type = GNUTLS_X509_FMT_PEM; diff --git a/src/agents/pop/pop3.h b/src/agents/pop/pop3.h index e6a2f18..b436017 100644 --- a/src/agents/pop/pop3.h +++ b/src/agents/pop/pop3.h @@ -19,6 +19,8 @@ * ****************************************************************************/ +#include + /* Port to listen */ #define POP3_PORT 110 #define POP3_PORT_SSL 995 @@ -35,8 +37,8 @@ #define MSGOK "+OK\r\n" #define MSGOKHELLO "Bongo POP3 server ready\r\n" #define MSGOKBYE "signing off" -#define MSGCAPALIST "+OK\r\nTOP\r\nUSER\r\nSASL LOGIN\r\nRESP-CODES\r\nAUTH-RESP-CODE\r\nPIPELINING\r\nEXPIRE NEVER\r\nUIDL\r\nSTLS\r\n.\r\n" -#define MSGCAPALIST_NOTLS "+OK\r\nTOP\r\nUSER\r\nSASL LOGIN\r\nRESP-CODES\r\nAUTH-RESP-CODE\r\nPIPELINING\r\nEXPIRE NEVER\r\nUIDL\r\n.\r\n" +#define MSGCAPALIST_TLS "+OK\r\nTOP\r\nUSER\r\nRESP-CODES\r\nAUTH-RESP-CODE\r\nPIPELINING\r\nEXPIRE NEVER\r\nUIDL\r\n.\r\n" +#define MSGCAPALIST_STARTTLS "+OK\r\nTOP\r\nRESP-CODES\r\nAUTH-RESP-CODE\r\nPIPELINING\r\nEXPIRE NEVER\r\nUIDL\r\nSTLS\r\n.\r\n" #define MSGWRONGMETHOD "-ERR [AUTH] Unsupported method\r\n" #define MSGOKPASSWD "+OK Password required\r\n" #define MSGOKSTARTTLS "+OK Begin TLS negotiations\r\n" diff --git a/src/agents/queue/conf.c b/src/agents/queue/conf.c index 1c2a8e0..055dc9d 100644 --- a/src/agents/queue/conf.c +++ b/src/agents/queue/conf.c @@ -49,7 +49,7 @@ int aliasCmpFunc (const void *lft, const void *rgt); -QueueConfiguration Conf = { 0, {0, }, }; +QueueConfiguration Conf = {0}; long CalculateCheckQueueLimit(unsigned long concurrent, unsigned long sequential) @@ -205,11 +205,11 @@ ReadConfiguration (BOOL *recover) unsigned int x; for(x=0;xlen;x++) { char *aconfig; - unsigned char path[100]; + char path[100]; BongoJsonNode *node; struct _AliasStruct a; - a.original = MemStrdup(g_array_index(Conf.domains, unsigned char *, x)); + a.original = MemStrdup(g_array_index(Conf.domains, char *, x)); a.to = NULL; a.aliases = NULL; a.mapping_type = 0; @@ -262,6 +262,7 @@ ReadConfiguration (BOOL *recover) void CheckConfig(BongoAgent *agent) { + UNUSED_PARAMETER(agent); /* TODO: Figure out how we are going to handle this type of situation */ return; } diff --git a/src/agents/queue/conf.h b/src/agents/queue/conf.h index ffa0792..78749c7 100644 --- a/src/agents/queue/conf.h +++ b/src/agents/queue/conf.h @@ -108,9 +108,9 @@ typedef struct _QueueConfiguration { } QueueConfiguration; struct _AliasStruct{ - unsigned char* original; - unsigned char* to; - unsigned int mapping_type; + char *original; + char *to; + int mapping_type; GArray *aliases; }; diff --git a/src/agents/queue/domain.c b/src/agents/queue/domain.c index 6b64b1d..32d56d5 100644 --- a/src/agents/queue/domain.c +++ b/src/agents/queue/domain.c @@ -26,7 +26,7 @@ #include "domain.h" int -QDBStartup(int minimum, int maximum) +QDBStartup(int minimum __attribute__((unused)), int maximum __attribute__((unused))) { return 0; } @@ -43,48 +43,48 @@ QDBHandleAlloc(void) } void -QDBHandleRelease(void *handle) +QDBHandleRelease(void *handle __attribute__((unused))) { } int -QDBAdd(void *handle, unsigned char *domain, unsigned long queueID, int queue) +QDBAdd(void *handle __attribute__((unused)), char *domain __attribute__((unused)), unsigned long queueID __attribute__((unused)), int queue __attribute__((unused))) { return -1; } int -QDBRemoveID(void *handle, unsigned long queueID) +QDBRemoveID(void *handle __attribute__((unused)), unsigned long queueID __attribute__((unused))) { return -1; } int -QDBRemoveDomain(void *handle, unsigned char *domain) +QDBRemoveDomain(void *handle __attribute__((unused)), char *domain __attribute__((unused))) { return -1; } int -QDBSearchID(void *handle, unsigned long queueID) +QDBSearchID(void *handle __attribute__((unused)), unsigned long queueID __attribute__((unused))) { return -1; } int -QDBSearchDomain(void *handle, unsigned char *domain) +QDBSearchDomain(void *handle __attribute__((unused)), char *domain __attribute__((unused))) { return -1; } int -QDBQuery(void *handle, unsigned char *query, QDBQueryResults *results) +QDBQuery(void *handle __attribute__((unused)), char *query __attribute__((unused)), QDBQueryResults *results __attribute__((unused))) { return -1; } int -QDBDump(unsigned char *domain) +QDBDump(char *domain __attribute__((unused))) { return -1; } @@ -93,4 +93,3 @@ void QDBSummarizeQueue(void) { } - diff --git a/src/agents/queue/domain.h b/src/agents/queue/domain.h index 82edd8f..d58c0d2 100644 --- a/src/agents/queue/domain.h +++ b/src/agents/queue/domain.h @@ -11,13 +11,13 @@ int QDBStartup(int minimum, int maximum); void QDBShutdown(void); void *QDBHandleAlloc(void); void QDBHandleRelease(void *handle); -int QDBAdd(void *handle, unsigned char *domain, unsigned long queueID, int queue); +int QDBAdd(void *handle, char *domain, unsigned long queueID, int queue); int QDBRemoveID(void *handle, unsigned long queueID); -int QDBRemoveDomain(void *handle, unsigned char *domain); +int QDBRemoveDomain(void *handle, char *domain); int QDBSearchID(void *handle, unsigned long queueID); -int QDBSearchDomain(void *handle, unsigned char *domain); -int QDBQuery(void *handle, unsigned char *query, QDBQueryResults *results); -int QDBDump(unsigned char *domain); +int QDBSearchDomain(void *handle, char *domain); +int QDBQuery(void *handle, char *query, QDBQueryResults *results); +int QDBDump(char *domain); void QDBSummarizeQueue(void); #endif diff --git a/src/agents/queue/mime.c b/src/agents/queue/mime.c index 853ba51..0fd77af 100644 --- a/src/agents/queue/mime.c +++ b/src/agents/queue/mime.c @@ -30,7 +30,7 @@ #include static BOOL -ParseDispositionParameters(unsigned char *ptr, MIMEPartStruct *part) +ParseDispositionParameters(char *ptr, MIMEPartStruct *part) { RFC2231ParamStruct *rfc2231Name = NULL; @@ -67,7 +67,7 @@ ParseDispositionParameters(unsigned char *ptr, MIMEPartStruct *part) } __inline static BOOL -ParseTypeParameters(unsigned char *ptr, MIMEPartStruct *part) +ParseTypeParameters(char *ptr, MIMEPartStruct *part) { RFC2231ParamStruct *rfc2231Name = NULL; @@ -289,7 +289,7 @@ FreeMIME(MIMEReportStruct *report) } MIMEReportStruct * -ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bodyLines, unsigned char *line, unsigned long lineSize) +ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bodyLines, char *line, unsigned long lineSize) { int i; int j; @@ -305,12 +305,12 @@ ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bo unsigned long lastHeaderStart = 0; unsigned long lastHeaderLen = 0; unsigned long contentHeaderLen = 0; - unsigned char *ptr; - unsigned char *start; - unsigned char *end; - unsigned char *header = NULL; - unsigned char *currentSeparator = NULL; - unsigned char *allocatedSeparator = NULL; + char *ptr; + char *start; + char *end; + char *header = NULL; + char *currentSeparator = NULL; + char *allocatedSeparator = NULL; BOOL lastInHeader = TRUE; BOOL inHeader = TRUE; BOOL skip = FALSE; @@ -322,7 +322,7 @@ ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bo partsAllocated = MIME_STRUCT_ALLOC; part = (MIMEPartStruct *)MemMalloc((sizeof(MIMEPartStruct) * partsAllocated) + sizeof(MIMEReportStruct)); - header = (unsigned char *)MemMalloc(sizeof(unsigned char)*MAX_HEADER); + header = (char *)MemMalloc(sizeof(char)*MAX_HEADER); if (!part || !header) { if (part) { MemFree(part); @@ -462,7 +462,7 @@ ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bo doingDigest = TRUE; } ParseTypeParameters(end + 1, &part[partNo]); - if (part[partNo].separator) { + if (part[partNo].separator[0] != '\0') { currentSeparator = part[partNo].separator; } partNo++; diff --git a/src/agents/queue/mime.h b/src/agents/queue/mime.h index 1d5da02..893186c 100644 --- a/src/agents/queue/mime.h +++ b/src/agents/queue/mime.h @@ -29,12 +29,12 @@ #include "connio.h" typedef struct _MIMEPartStruct { - unsigned char separator[MIME_SEPARATOR_LEN+1]; - unsigned char type[MIME_TYPE_LEN+1]; - unsigned char subtype[MIME_SUBTYPE_LEN+1]; - unsigned char charset[MIME_CHARSET_LEN+1]; - unsigned char encoding[MIME_ENCODING_LEN+1]; - unsigned char name[MIME_NAME_LEN+1]; + char separator[MIME_SEPARATOR_LEN+1]; + char type[MIME_TYPE_LEN+1]; + char subtype[MIME_SUBTYPE_LEN+1]; + char charset[MIME_CHARSET_LEN+1]; + char encoding[MIME_ENCODING_LEN+1]; + char name[MIME_NAME_LEN+1]; long headerStart; unsigned long headerLen; unsigned long start; @@ -60,6 +60,6 @@ typedef struct _MIMEReportStruct { int SendMIME(Connection *conn, MIMEReportStruct *report); int SendMimeDetails(Connection *conn, MIMEReportStruct *report); void FreeMIME(MIMEReportStruct *report); -MIMEReportStruct *ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bodyLines, unsigned char *line, unsigned long lineSize); +MIMEReportStruct *ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bodyLines, char *line, unsigned long lineSize); #endif diff --git a/src/agents/queue/queue.c b/src/agents/queue/queue.c index c0585bd..f51dba6 100644 --- a/src/agents/queue/queue.c +++ b/src/agents/queue/queue.c @@ -25,6 +25,33 @@ #include "queue.h" #include "mime.h" #include "messages.h" + +static BOOL +QueueFormatBuffer(char *destination, size_t destinationSize, + const char *format, ...) +{ + int written; + va_list arguments; + + if (!destination || destinationSize == 0) { + return FALSE; + } + + va_start(arguments, format); + written = vsnprintf(destination, destinationSize, format, arguments); + va_end(arguments); + + if (written < 0 || (size_t)written >= destinationSize) { + destination[0] = '\0'; + Log(LOG_ERROR, "queue: Refusing an overlong formatted path or identifier"); + return FALSE; + } + + return TRUE; +} + +#define QueueFormat(destination, ...) \ + QueueFormatBuffer((destination), sizeof(destination), __VA_ARGS__) #include "domain.h" #include @@ -139,9 +166,9 @@ int IntCmp (const unsigned long *s1, const unsigned long *s2) { static int PDBSearch(char *doc, char *searchString) { - unsigned char *w[MAX_CHARS_IN_PDBSEARCH], *textStart, *textEnd, *text, *end; + char *w[MAX_CHARS_IN_PDBSEARCH], *textStart, *textEnd, *text, *end; int wl[MAX_CHARS_IN_PDBSEARCH]; - unsigned char expression[MAX_CHARS_IN_PDBSEARCH]; + char expression[MAX_CHARS_IN_PDBSEARCH]; long word, words = 0, i,len; int found = 0; @@ -295,7 +322,7 @@ AddPushAgent(QueueClient *client, char *address, int port, int queue, - unsigned char *identifier) + char *identifier) { /* since the sorting doesn't always pass in the int or the struct the same every time i need to put the int in a struct */ QueueList tempQueue; @@ -467,9 +494,15 @@ DeliverToStore(NMAPConnections *list, { NMAPConnection *nmap = NULL; int ccode = 0; - unsigned char line[CONN_BUFSIZE + 1]; + char line[CONN_BUFSIZE + 1]; BOOL new; + UNUSED_PARAMETER(type); + UNUSED_PARAMETER(from); + UNUSED_PARAMETER(authFrom); + UNUSED_PARAMETER(filename); + UNUSED_PARAMETER(messageFlags); + if ((ccode = GetNMAPConnection(list, address, &nmap, &new)) < 0) { return DELIVER_TRY_LATER; } @@ -501,7 +534,7 @@ EndStoreDelivery(NMAPConnections *list) { int ccode; long index; - unsigned char line[CONN_BUFSIZE + 1]; + char line[CONN_BUFSIZE + 1]; for (index = 0; index < list->used; index++) { NMAPConnection *nmap = &list->connections[index]; @@ -536,7 +569,7 @@ ProcessQueueEntryCleanUp(unsigned long id, MIMEReportStruct *report) } static BOOL -ProcessQueueEntry(unsigned char *entryIn) +ProcessQueueEntry(char *entryIn) { int i; int queue; @@ -554,21 +587,21 @@ ProcessQueueEntry(unsigned char *entryIn) unsigned int iter_p=0; unsigned long dSize = 0; unsigned long entryID = 0; - unsigned char *ptr; - unsigned char *ptr2 = NULL; - unsigned char *cur; - unsigned char *next; - unsigned char *limit; - unsigned char *qDate; - unsigned char *qFlags; - unsigned char *qID; - unsigned char *qAddress; - unsigned char *qFrom; - unsigned char *qEnvelope = NULL; - unsigned char path[XPL_MAX_PATH + 1] = ""; - unsigned char path2[XPL_MAX_PATH + 1]; - unsigned char line[CONN_BUFSIZE + 1] = ""; - unsigned char entry[15]; + char *ptr; + char *ptr2 = NULL; + char *cur; + char *next; + char *limit; + char *qDate; + char *qFlags; + char *qID; + char *qAddress; + char *qFrom; + char *qEnvelope = NULL; + char path[XPL_MAX_PATH + 1] = ""; + char path2[XPL_MAX_PATH + 1]; + char line[CONN_BUFSIZE + 1] = ""; + char entry[15]; BOOL keep = TRUE; BOOL bounce = FALSE; time_t date; @@ -603,7 +636,7 @@ StartOver: Log(LOG_DEBUG, "Processing entry %ld on queue %d", entryID, queue); if (SpoolEntryIDLock(entryID)) { - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(fh, path, "r+b"); } else { ProcessQueueEntryCleanUp(0, report); @@ -630,17 +663,17 @@ StartOver: qAddress = NULL; qFrom = NULL; - sprintf(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(newFH, path, "wb"); - sprintf(path, "%s/c%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d",Conf.spoolPath, entry, queue); FOPEN_CHECK(temp, path, "rb"); if (newFH && (stat(path, &sb) == 0) && (sb.st_size > 8) - && ((qEnvelope = (unsigned char *)MemMalloc(sb.st_size + 1)) != NULL) + && ((qEnvelope = (char *)MemMalloc(sb.st_size + 1)) != NULL) && (temp) - && (fread(qEnvelope, sizeof(unsigned char), sb.st_size, temp) == (size_t)sb.st_size)) { + && (fread(qEnvelope, sizeof(char), sb.st_size, temp) == (size_t)sb.st_size)) { /* Sort the control file as follows: QUEUE_DATE QUEUE_FLAGS @@ -731,23 +764,23 @@ StartOver: } if (qDate && qFrom && count) { - fwrite(qDate, sizeof(unsigned char), qDateLength, newFH); + fwrite(qDate, sizeof(char), qDateLength, newFH); if (qFlags) { - fwrite(qFlags, sizeof(unsigned char), qFlagsLength, newFH); + fwrite(qFlags, sizeof(char), qFlagsLength, newFH); } else { - fwrite(QUEUES_FLAGS"0\r\n", sizeof(unsigned char), 4, newFH); + fwrite(QUEUES_FLAGS"0\r\n", sizeof(char), 4, newFH); } if (qID) { - fwrite(qID, sizeof(unsigned char), qIDLength, newFH); + fwrite(qID, sizeof(char), qIDLength, newFH); } if (qAddress) { - fwrite(qAddress, sizeof(unsigned char), qAddressLength, newFH); + fwrite(qAddress, sizeof(char), qAddressLength, newFH); } - fwrite(qFrom, sizeof(unsigned char), qFromLength, newFH); + fwrite(qFrom, sizeof(char), qFromLength, newFH); } else { /* fixme - if a new queue entry has at least QUEUE_FROM but no recipients we should bounce the message rather @@ -756,10 +789,10 @@ StartOver: UNLINK_CHECK(path); - sprintf(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); UNLINK_CHECK(path); - sprintf(path, "%s/d%s.msg",Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg",Conf.spoolPath, entry); UNLINK_CHECK(path); MemFree(qEnvelope); @@ -790,7 +823,7 @@ StartOver: switch (*cur) { case QUEUE_BOUNCE: case QUEUE_CALENDAR_LOCAL: { - fwrite(cur, sizeof(unsigned char), next - cur, newFH); + fwrite(cur, sizeof(char), next - cur, newFH); break; } @@ -807,9 +840,9 @@ StartOver: } /* alias this user */ - unsigned char buffer[1024]; /* FIXME: is this too big? */ - unsigned char *addrptr; - unsigned char *flags = strchr(ptr2+1,' '); /* the original flags */ + char buffer[1024]; /* FIXME: is this too big? */ + char *addrptr; + char *flags = strchr(ptr2+1,' '); /* the original flags */ int cnt=0; aliasing(cur+1, &cnt, buffer); @@ -857,7 +890,7 @@ StartOver: case QUEUE_THIRD_PARTY: default: { - fwrite(cur, sizeof(unsigned char), next - cur, newFH); + fwrite(cur, sizeof(char), next - cur, newFH); break; } } @@ -871,7 +904,7 @@ StartOver: UNLINK_CHECK(path); - sprintf(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); RENAME_CHECK(path2, path); break; @@ -882,7 +915,7 @@ StartOver: } if (!newFH) { - sprintf(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); LogFailureF("File open failure. Entry %ld, path %s", entryID, path); } else if (!qEnvelope) { LogFailureF("Out of memory. Entry %ld, size %ld", entryID, sb.st_size); @@ -906,7 +939,7 @@ StartOver: Log(LOG_WARNING, "Write error in queue"); - sprintf(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); UNLINK_CHECK(path); ProcessQueueEntryCleanUp(entryID, report); @@ -918,11 +951,11 @@ StartOver: case Q_OUTGOING: { if (date < time(NULL) - Conf.maxLinger) { /* We move it to the Q_RTS queue and the linger code there will bounce it for us */ - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); - sprintf(path2, "%s/c%s.%03d", Conf.spoolPath, entry, Q_RTS); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path2, "%s/c%s.%03d", Conf.spoolPath, entry, Q_RTS); RENAME_CHECK(path, path2); - sprintf(path, "%03d%s", Q_RTS, entry); + QueueFormat(path, "%03d%s", Q_RTS, entry); SpoolEntryIDUnlock(entryID); entryIn = MemStrdup(path); goto StartOver; @@ -934,29 +967,30 @@ StartOver: } /* Fall-through to Q_DELIVER */ + __attribute__((fallthrough)); } case Q_DELIVER: { int status; unsigned long messageFlags; unsigned long flags = DSN_FAILURE | DSN_HEADER | DSN_BODY; - unsigned char *mailbox; - unsigned char *preMailboxDelim = NULL; - unsigned char *postMailboxDelim = NULL; - unsigned char recipient[MAXEMAILNAMESIZE + 1]; - unsigned char sender[MAXEMAILNAMESIZE + 1]; - unsigned char authenticatedSender[MAXEMAILNAMESIZE + 1]; - unsigned char messageID[MAXEMAILNAMESIZE + 1]; + char *mailbox; + char *preMailboxDelim = NULL; + char *postMailboxDelim = NULL; + char recipient[MAXEMAILNAMESIZE + 1]; + char sender[MAXEMAILNAMESIZE + 1]; + char authenticatedSender[MAXEMAILNAMESIZE + 1]; + char messageID[MAXEMAILNAMESIZE + 1]; char dataFilename[XPL_MAX_PATH]; NMAPConnections list = { 0, }; data = NULL; saddr.sin_addr.s_addr = 0; - sprintf(dataFilename, "d%s.msg", entry); + QueueFormat(dataFilename, "d%s.msg", entry); if (!dSize) { - sprintf(path, "%s/%s", Conf.spoolPath, dataFilename); + QueueFormat(path, "%s/%s", Conf.spoolPath, dataFilename); if (stat(path, &sb)) { ProcessQueueEntryCleanUp(entryID, report); return(TRUE); @@ -968,10 +1002,10 @@ StartOver: keep = FALSE; bounce = FALSE; - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(fh, path, "rb"); - sprintf(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(newFH, path, "wb"); if (!fh || !newFH) { @@ -983,7 +1017,7 @@ StartOver: FCLOSE_CHECK(newFH); } - sprintf(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); UNLINK_CHECK(path); ProcessQueueEntryCleanUp(entryID, report); @@ -1034,7 +1068,7 @@ StartOver: FOPEN_CHECK(data, path, "rb"); if (!data) { FCLOSE_CHECK(fh); - sprintf(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); FCLOSE_CHECK(newFH); UNLINK_CHECK(path); ProcessQueueEntryCleanUp(entryID, report); @@ -1129,18 +1163,18 @@ StartOver: ptr++; } } - /**** FALLTHROUGH ****/ + __attribute__((fallthrough)); case QUEUE_RECIP_LOCAL: { /* Local */ struct sockaddr_in siaddr; if (!data) { - sprintf(path, "%s/d%s.msg", Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg", Conf.spoolPath, entry); FOPEN_CHECK(data, path, "rb"); if (!data) { FCLOSE_CHECK(fh); FCLOSE_CHECK(newFH); - sprintf(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); UNLINK_CHECK(path); ProcessQueueEntryCleanUp(entryID, report); @@ -1316,29 +1350,29 @@ StartOver: FCLOSE_CHECK(newFH); FCLOSE_CHECK(fh); if (bounce) { - unsigned char Path2[XPL_MAX_PATH+1]; + char Path2[XPL_MAX_PATH+1]; /* First, rename the work file into a control file */ - sprintf(path, "%s/c%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d",Conf.spoolPath, entry, queue); UNLINK_CHECK(path); - sprintf(Path2, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(Path2, "%s/w%s.%03d",Conf.spoolPath, entry, queue); RENAME_CHECK(Path2, path); if (!data) { - sprintf(path, "%s/d%s.msg",Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg",Conf.spoolPath, entry); FOPEN_CHECK(data, path, "rb"); } else { fseek(data, 0, SEEK_SET); } - sprintf(path, "%s/c%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d",Conf.spoolPath, entry, queue); FOPEN_CHECK(fh, path, "rb"); if (fh && data && 0 == HandleDSN(data, fh)) { /* Now bounce the thing */ fseek(fh, 0, SEEK_SET); - sprintf(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); FOPEN_CHECK(newFH, path, "wb"); if (newFH) { /* Now remove the bounced entries */ @@ -1374,21 +1408,21 @@ StartOver: FCLOSE_CHECK(data); } if (keep) { - unsigned char Path2[XPL_MAX_PATH+1]; + char Path2[XPL_MAX_PATH+1]; - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); UNLINK_CHECK(path); - sprintf(Path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(Path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); RENAME_CHECK(Path2, path); } else { - sprintf(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); UNLINK_CHECK(path); - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); UNLINK_CHECK(path); - sprintf(path, "%s/d%s.msg", Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg", Conf.spoolPath, entry); UNLINK_CHECK(path); XplSafeDecrement(Queue.queuedLocal); @@ -1402,7 +1436,7 @@ StartOver: } if (Agent.agent.state < BONGO_AGENT_STATE_STOPPING) { - sprintf(path, "%s/d%s.msg", Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg", Conf.spoolPath, entry); } else { ProcessQueueEntryCleanUp(entryID, report); return(TRUE); @@ -1423,7 +1457,7 @@ StartOver: for (iter_p=0; iter_p < CurrentQueue->pools->len; iter_p++) { QueuePoolList *CurrentPool = g_array_index(CurrentQueue->pools, QueuePoolList *, iter_p); - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); stat(path, &sb); FOPEN_CHECK(fh, path, "rb"); if (!fh) { @@ -1474,7 +1508,7 @@ StartOver: FCLOSE_CHECK(fh); fh=NULL; - sprintf(client->entry.workQueue, "%03d-%s", queue, entry); + QueueFormat(client->entry.workQueue, "%03d-%s", queue, entry); client->entry.workQueue[3] = '\0'; ConnWriteStr(client->conn, "6021 Get busy!\r\n"); ConnFlush(client->conn); @@ -1489,7 +1523,7 @@ StartOver: Log(LOG_DEBUG, "Ran into queue resend [C%s.%03d] Last Command: %s", entry, queue, client->buffer); FCLOSE_CHECK(client->entry.work); client->entry.work = NULL; - sprintf(client->path,"%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(client->path,"%s/w%s.%03d", Conf.spoolPath, entry, queue); remove(client->path); } @@ -1560,11 +1594,11 @@ StartOver: } Log(LOG_DEBUG, "Moving %ld from queue %d to queue %d", entryID, queue, i); - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); - sprintf(path2, "%s/c%s.%03d", Conf.spoolPath, entry, i); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path2, "%s/c%s.%03d", Conf.spoolPath, entry, i); RENAME_CHECK(path, path2); - sprintf(path, "%03d%s", i, entry); + QueueFormat(path, "%03d%s", i, entry); SpoolEntryIDUnlock(entryID); entryIn = MemStrdup(path); @@ -1574,7 +1608,7 @@ StartOver: case Q_OUTGOING: { /* Check if there are any recipients left */ - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); if ((handle = QDBHandleAlloc()) != NULL) { QDBRemoveID(handle, entryID); @@ -1636,7 +1670,7 @@ StartOver: /* Note that we make sure to keep the content of path pointing to the control file; this way we only need one sprintf */ if (bounce) { /* Call the bouncing code */ - sprintf(path2, "%s/d%s.msg", Conf.spoolPath, entry); + QueueFormat(path2, "%s/d%s.msg", Conf.spoolPath, entry); FOPEN_CHECK(data, path2, "rb"); FOPEN_CHECK(fh, path, "rb"); @@ -1645,7 +1679,7 @@ StartOver: /* If we're not keeping the file, we can ignore its contents */ if (keep) { - sprintf(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(newFH, path2, "wb"); if (newFH) { /* Now remove the bounced entries */ @@ -1696,7 +1730,7 @@ StartOver: UNLINK_CHECK(path); - sprintf(path, "%s/d%s.msg",Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg",Conf.spoolPath, entry); UNLINK_CHECK(path); break; @@ -1713,13 +1747,13 @@ StartOver: QDBHandleRelease(handle); } - sprintf(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(newFH, path, "wb"); - sprintf(path, "%s/d%s.msg", Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg", Conf.spoolPath, entry); FOPEN_CHECK(data, path, "rb"); - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(fh, path, "rb"); if (fh && newFH && data) { @@ -1770,7 +1804,7 @@ StartOver: /* Still got path from above */ UNLINK_CHECK(path); - sprintf(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); RENAME_CHECK(path2, path); FOPEN_CHECK(fh, path, "rb"); @@ -1783,10 +1817,10 @@ StartOver: FCLOSE_CHECK(data); - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); UNLINK_CHECK(path); - sprintf(path, "%s/d%s.msg", Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg", Conf.spoolPath, entry); UNLINK_CHECK(path); XplSafeDecrement(Queue.queuedLocal); @@ -1808,11 +1842,11 @@ StartOver: return(TRUE); } else { count = 0; - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, Q_RTS); - sprintf(path2, "%s/c%s.%03d", Conf.spoolPath, entry, Q_INCOMING); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, Q_RTS); + QueueFormat(path2, "%s/c%s.%03d", Conf.spoolPath, entry, Q_INCOMING); RENAME_CHECK(path, path2); - sprintf(path, "%03d%s", Q_INCOMING, entry); + QueueFormat(path, "%03d%s", Q_INCOMING, entry); } SpoolEntryIDUnlock(entryID); @@ -1846,17 +1880,17 @@ CreateDSNMessage(FILE *data, FILE *control, FILE *rtsData, FILE *rtsControl, BOO unsigned long maxBodySize; unsigned long received = 0; unsigned long handling; - unsigned char *ptr; - unsigned char *ptr2; - unsigned char *transcript; - unsigned char timeLine[80]; - unsigned char line[CONN_BUFSIZE + 1]; - unsigned char postmaster[MAXEMAILNAMESIZE + 1]; - unsigned char sender[MAXEMAILNAMESIZE + 1]; - unsigned char aSender[MAXEMAILNAMESIZE + 1] = ""; - unsigned char recipient[MAXEMAILNAMESIZE + 1] = ""; - unsigned char oRecipient[MAXEMAILNAMESIZE + 1] = ""; - unsigned char envID[128] = ""; + char *ptr; + char *ptr2; + char *transcript; + char timeLine[80]; + char line[CONN_BUFSIZE + 1]; + char postmaster[MAXEMAILNAMESIZE + 1]; + char sender[MAXEMAILNAMESIZE + 1]; + char aSender[MAXEMAILNAMESIZE + 1] = ""; + char recipient[MAXEMAILNAMESIZE + 1] = ""; + char oRecipient[MAXEMAILNAMESIZE + 1] = ""; + char envID[128] = ""; BOOL mBounce=FALSE; BOOL header; time_t now; @@ -1932,7 +1966,7 @@ CreateDSNMessage(FILE *data, FILE *control, FILE *rtsData, FILE *rtsControl, BOO return(FALSE); } - sprintf(postmaster, "%s@%s", BongoGlobals.postmaster, BongoGlobals.hostname); + QueueFormat(postmaster, "%s@%s", BongoGlobals.postmaster, BongoGlobals.hostname); handling = Conf.bounceHandling; /* We're guaranteed to have a recipient */ @@ -1947,7 +1981,7 @@ CreateDSNMessage(FILE *data, FILE *control, FILE *rtsData, FILE *rtsControl, BOO if (MDBRead(dn, MSGSRV_A_POSTMASTER, vs) > 0) { ptr2 = strrchr(vs->Value[0], '\\'); if (ptr2) { - sprintf(postmaster, "%s@%s", ptr2 + 1, ptr + 1); + QueueFormat(postmaster, "%s@%s", ptr2 + 1, ptr + 1); } MDBFreeValues(vs); @@ -2259,14 +2293,14 @@ CreateDSNMessage(FILE *data, FILE *control, FILE *rtsData, FILE *rtsControl, BOO if (flags & DSN_HEADER) { if ((line[0] == 'F') && (line[1] == 'r') && (line[4] == ' ')) { - fwrite(">", sizeof(unsigned char), 1, rtsData); + fwrite(">", sizeof(char), 1, rtsData); } - bodySize += fwrite(line, sizeof(unsigned char), strlen(line), rtsData); + bodySize += fwrite(line, sizeof(char), strlen(line), rtsData); } } else { if (flags & DSN_BODY) { - bodySize += fwrite(line, sizeof(unsigned char), strlen(line), rtsData); + bodySize += fwrite(line, sizeof(char), strlen(line), rtsData); if (bodySize > maxBodySize) { break; } @@ -2288,11 +2322,11 @@ CreateDSNMessage(FILE *data, FILE *control, FILE *rtsData, FILE *rtsControl, BOO XplRWReadLockAcquire(&Conf.lock); - if (Conf.bounceHandling & BOUNCE_RETURN) { + if (handling & BOUNCE_RETURN) { fprintf(rtsControl, QUEUES_RECIP_REMOTE"%s %s 0\r\n", sender, sender); } - if (Conf.bounceHandling & BOUNCE_CC_POSTMASTER) { + if (handling & BOUNCE_CC_POSTMASTER) { fprintf(rtsControl, QUEUES_RECIP_REMOTE"%s %s 0\r\n", postmaster, postmaster); } @@ -2305,7 +2339,7 @@ static int HandleDSN(FILE *data, FILE *control) { unsigned long id; - unsigned char path[XPL_MAX_PATH + 1]; + char path[XPL_MAX_PATH + 1]; FILE *rtsControl = NULL; FILE *rtsData = NULL; XplThreadID threadID; @@ -2314,7 +2348,7 @@ HandleDSN(FILE *data, FILE *control) id = Queue.queueID++ & ((1 << 28) - 1); XplMutexUnlock(Queue.queueIDLock); - sprintf(path, "%s/c%07lx.%03d", Conf.spoolPath, id, Q_INCOMING); + QueueFormat(path, "%s/c%07lx.%03d", Conf.spoolPath, id, Q_INCOMING); /* Create control file */ FOPEN_CHECK(rtsControl, path,"wb"); @@ -2326,12 +2360,12 @@ HandleDSN(FILE *data, FILE *control) SpoolEntryIDLock(id); /* Create data file */ - sprintf(path, "%s/d%07lx.msg", Conf.spoolPath, id); + QueueFormat(path, "%s/d%07lx.msg", Conf.spoolPath, id); FOPEN_CHECK(rtsData, path, "wb"); if (!rtsData) { fprintf (stderr, "could not open rtsData\n"); FCLOSE_CHECK(rtsControl); - sprintf(path, "%s/d%07lx.msg", Conf.spoolPath, id); + QueueFormat(path, "%s/d%07lx.msg", Conf.spoolPath, id); UNLINK_CHECK(path); return -1; } @@ -2340,10 +2374,10 @@ HandleDSN(FILE *data, FILE *control) FCLOSE_CHECK(rtsControl); FCLOSE_CHECK(rtsData); - sprintf(path, "%s/d%07lx.msg", Conf.spoolPath, id); + QueueFormat(path, "%s/d%07lx.msg", Conf.spoolPath, id); UNLINK_CHECK(path); - sprintf(path, "%s/c%07lx.%03d", Conf.spoolPath, id, Q_INCOMING); + QueueFormat(path, "%s/c%07lx.%03d", Conf.spoolPath, id, Q_INCOMING); UNLINK_CHECK(path); SpoolEntryIDUnlock(id); @@ -2356,7 +2390,7 @@ HandleDSN(FILE *data, FILE *control) FCLOSE_CHECK(rtsData); SpoolEntryIDUnlock(id); - sprintf(path, "%03d%lx",Q_INCOMING, id); + QueueFormat(path, "%03d%lx",Q_INCOMING, id); if (XplSafeRead(Queue.activeWorkers) < Conf.maxConcurrentWorkers) { XplBeginCountedThread(&threadID, ProcessQueueEntry, STACKSPACE_Q, MemStrdup(path), id, Queue.activeWorkers); @@ -2368,12 +2402,13 @@ HandleDSN(FILE *data, FILE *control) static void CheckQueue(void *queueIn) { + UNUSED_PARAMETER(queueIn); int ccode; int burst; int count; unsigned long found; unsigned long handled; - unsigned char path[XPL_MAX_PATH + 1]; + char path[XPL_MAX_PATH + 1]; time_t now; XplDir *dirP; XplDir *dirEntry; @@ -2421,7 +2456,7 @@ CheckQueue(void *queueIn) } found++; - sprintf(path, "%.3s%.7s", dirEntry->d_nameDOS + 9, dirEntry->d_nameDOS + 1); + QueueFormat(path, "%.3s%.7s", dirEntry->d_nameDOS + 9, dirEntry->d_nameDOS + 1); /* skip recently-touched messages that are in the outgoing * queue (queue 7) */ @@ -2514,7 +2549,7 @@ CreateQueueThreads(BOOL failed) unsigned long count; unsigned long total; unsigned long current; - unsigned char path[XPL_MAX_PATH + 1]; + char path[XPL_MAX_PATH + 1]; FILE *control = NULL; FILE *killFile = NULL; XplDir *dirP; @@ -2531,11 +2566,11 @@ CreateQueueThreads(BOOL failed) Log(LOG_DEBUG, "Verifying queue integrity, quick mode."); } - sprintf(path, "%s", Conf.spoolPath); + QueueFormat(path, "%s", Conf.spoolPath); dirP = XplOpenDir(path); - sprintf(path, "%s/fragfile", MsgGetDir(MSGAPI_DIR_DBF, NULL, 0)); + QueueFormat(path, "%s/fragfile", MsgGetDir(MSGAPI_DIR_DBF, NULL, 0)); FOPEN_CHECK(killFile, path, "wb"); if (!killFile) { if (dirP) { @@ -2570,8 +2605,15 @@ CreateQueueThreads(BOOL failed) count++; } else if (failed) { - sprintf(path, "%s/d%s", Conf.spoolPath, dirEntry->d_nameDOS + 1); - sprintf(path + strlen(path) - 3, "msg"); + QueueFormat(path, "%s/d%s", Conf.spoolPath, dirEntry->d_nameDOS + 1); + if (strlen(path) >= 3) { + size_t extensionOffset = strlen(path) - 3; + QueueFormatBuffer(path + extensionOffset, + sizeof(path) - extensionOffset, + "msg"); + } else { + path[0] = '\0'; + } if (access(path, 0) == 0) { /* Check for matching D file */ if (dirEntry->d_size <= 0) { @@ -2611,9 +2653,16 @@ CreateQueueThreads(BOOL failed) count += 2; } else if (failed) { - sprintf(path, "%s/c%s", Conf.spoolPath, dirEntry->d_nameDOS + 1); + QueueFormat(path, "%s/c%s", Conf.spoolPath, dirEntry->d_nameDOS + 1); for (i = 0; i < 10; i++) { - sprintf(path + strlen(path) - 3, "%03d", i); + if (strlen(path) >= 3) { + size_t extensionOffset = strlen(path) - 3; + QueueFormatBuffer(path + extensionOffset, + sizeof(path) - extensionOffset, + "%03d", i); + } else { + path[0] = '\0'; + } if (access(path, 0)==0) { break; } @@ -2650,7 +2699,7 @@ CreateQueueThreads(BOOL failed) XplCloseDir(dirP); } - sprintf(path, "%s", Conf.spoolPath); + QueueFormat(path, "%s", Conf.spoolPath); dirP = XplOpenDir(path); if (killFile) { @@ -2660,7 +2709,7 @@ CreateQueueThreads(BOOL failed) Log(LOG_DEBUG, "Queue integrity check complete, now cleaning irrelevant entries."); - sprintf(path, "%s/fragfile", MsgGetDir(MSGAPI_DIR_DBF, NULL, 0)); + QueueFormat(path, "%s/fragfile", MsgGetDir(MSGAPI_DIR_DBF, NULL, 0)); FOPEN_CHECK(killFile, path, "rb"); if (!killFile) { Log(LOG_ERROR, "Could not re-open killfile."); @@ -2678,13 +2727,13 @@ CreateQueueThreads(BOOL failed) FCLOSE_CHECK(killFile); } - sprintf(path, "%s/killfile", MsgGetDir(MSGAPI_DIR_DBF, NULL, 0)); + QueueFormat(path, "%s/killfile", MsgGetDir(MSGAPI_DIR_DBF, NULL, 0)); UNLINK_CHECK(path); XplCloseDir(dirP); if (failed) { - sprintf(path, "%s", Conf.spoolPath); + QueueFormat(path, "%s", Conf.spoolPath); dirP = XplOpenDir(path); if (dirP) { @@ -2701,7 +2750,7 @@ CreateQueueThreads(BOOL failed) current++; if ((dirEntry->d_nameDOS[0] & 0xDF) == 'C' ) { - sprintf(path, "%s/c%s", Conf.spoolPath, dirEntry->d_nameDOS + 1); + QueueFormat(path, "%s/c%s", Conf.spoolPath, dirEntry->d_nameDOS + 1); FOPEN_CHECK(control, path, "r+b"); if (!control) { continue; @@ -2734,13 +2783,12 @@ CommandQaddm(void *param) int ccode; int result; unsigned long flags = 0; - unsigned char *sender; - unsigned char *authSender; - unsigned char *recipient; - unsigned char *mailbox; - unsigned char *queue; - unsigned char *ptr; - unsigned char *ptr2; + char *sender; + char *authSender; + char *recipient; + char *mailbox; + char *ptr; + char *ptr2; struct stat sb; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -2771,8 +2819,6 @@ CommandQaddm(void *param) *mailbox++ = '\0'; *ptr2++ = '\0'; - queue = ptr; - ptr += 4; flags = atol(ptr2); @@ -2782,7 +2828,7 @@ CommandQaddm(void *param) result = DELIVER_FAILURE; - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr); if (stat(client->path, &sb) == 0) { FOPEN_CHECK(data, client->path, "rb"); } @@ -2791,8 +2837,10 @@ CommandQaddm(void *param) struct sockaddr_in saddr; if (MsgAuthFindUser(recipient) == 0) { - if (MsgAuthGetUserStore(recipient, &saddr) == TRUE) { - result = DeliverToStore(&list, &saddr, NMAP_DOCTYPE_MAIL, sender, authSender, client->path, data, sb.st_size, recipient, mailbox, 0); + if (MsgAuthGetUserStore(recipient, &saddr) == 0) { + result = DeliverToStore(&list, &saddr, NMAP_DOCTYPE_MAIL, + sender, authSender, client->path, data, + sb.st_size, recipient, mailbox, flags); EndStoreDelivery(&list); } } @@ -2820,9 +2868,9 @@ CommandQaddq(void *param) unsigned long len; unsigned long start; unsigned long length; - unsigned char *ptr; - unsigned char *ptr2; - unsigned char *ptr3; + char *ptr; + char *ptr2; + char *ptr3; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -2864,7 +2912,7 @@ CommandQaddq(void *param) /* fixme - LockQueueEntry? LockQueueEntry(ptr+4, atoi(ptr)); */ - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); FOPEN_CHECK(data, client->path, "rb"); if (data) { fseek(data, start, SEEK_SET); @@ -2876,13 +2924,13 @@ CommandQaddq(void *param) while (!feof(data) && !ferror(data) && (length != 0)) { if (length > CONN_BUFSIZE) { - len = fread(client->line, sizeof(unsigned char), CONN_BUFSIZE, data); + len = fread(client->line, sizeof(char), CONN_BUFSIZE, data); } else { - len = fread(client->line, sizeof(unsigned char), length, data); + len = fread(client->line, sizeof(char), length, data); } if (len) { - fwrite(client->line, sizeof(unsigned char), len, client->entry.data); + fwrite(client->line, sizeof(char), len, client->entry.data); length -= len; } } @@ -2905,7 +2953,7 @@ CommandQabrt(void *param) FCLOSE_CHECK(client->entry.control); client->entry.control = NULL; - sprintf(client->path,"%s/c%07lx.in", Conf.spoolPath, client->entry.id); + QueueFormat(client->path,"%s/c%07lx.in", Conf.spoolPath, client->entry.id); UNLINK_CHECK(client->path); } @@ -2913,7 +2961,7 @@ CommandQabrt(void *param) FCLOSE_CHECK(client->entry.data); client->entry.data = NULL; - sprintf(client->path,"%s/d%07lx.msg",Conf.spoolPath, client->entry.id); + QueueFormat(client->path,"%s/d%07lx.msg",Conf.spoolPath, client->entry.id); UNLINK_CHECK(client->path); } if (client->entry.work) { @@ -2921,7 +2969,7 @@ CommandQabrt(void *param) client->entry.work = NULL; if (client->entry.workQueue[0] != '\0') { - sprintf(client->path,"%s/w%s.%s",Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path,"%s/w%s.%s",Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); UNLINK_CHECK(client->path); } } @@ -2937,7 +2985,7 @@ CommandQbody(void *param) { int ccode; unsigned long count = 0; - unsigned char *ptr; + char *ptr; struct stat sb; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -2957,7 +3005,7 @@ CommandQbody(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path,"%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path,"%s/d%s.msg", Conf.spoolPath, ptr + 4); if (stat(client->path, &sb) == 0) { FOPEN_CHECK(data, client->path, "rb"); } @@ -2994,9 +3042,9 @@ CommandQbraw(void *param) unsigned long start; unsigned long size; unsigned long count = 0; - unsigned char *ptr; - unsigned char *ptr2; - unsigned char *ptr3; + char *ptr; + char *ptr2; + char *ptr3; struct stat sb; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -3024,7 +3072,7 @@ CommandQbraw(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); if (stat(client->path, &sb) == 0) { FOPEN_CHECK(data, client->path, "rb"); } @@ -3067,8 +3115,8 @@ CommandQcopy(void *param) unsigned long id; unsigned long len; unsigned long target; - unsigned char *ptr; - unsigned char *ptr2; + char *ptr; + char *ptr2; FILE *source = NULL; QueueClient *client = (QueueClient *)param; @@ -3118,7 +3166,7 @@ CommandQcopy(void *param) /* fixme - LockQueueEntry? LockQueueEntry(ptr+4, atoi(ptr)); */ - sprintf(client->path, "%s/d%s.msg",Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg",Conf.spoolPath, ptr + 4); FOPEN_CHECK(source, client->path, "rb"); if (source) { XplMutexLock(Queue.queueIDLock); @@ -3133,7 +3181,7 @@ CommandQcopy(void *param) client->entry.id = id; - sprintf(client->path, "%s/c%07lx.in", Conf.spoolPath, id); + QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, id); FOPEN_CHECK(client->entry.control, client->path, "wb"); if (client->entry.control) { fprintf(client->entry.control, QUEUES_DATE"%lu\r\n", time(NULL)); @@ -3143,13 +3191,13 @@ CommandQcopy(void *param) return(ConnWrite(client->conn, MSG5221SPACELOW, sizeof(MSG5221SPACELOW) - 1)); } - sprintf(client->path, "%s/d%07lx.msg", Conf.spoolPath, id); + QueueFormat(client->path, "%s/d%07lx.msg", Conf.spoolPath, id); FOPEN_CHECK(client->entry.data, client->path, "wb"); while (!feof(source) && !ferror(source)) { - len = fread(client->line, sizeof(unsigned char), CONN_BUFSIZE, source); + len = fread(client->line, sizeof(char), CONN_BUFSIZE, source); if (len) { - fwrite(client->line, sizeof(unsigned char), len, client->entry.data); + fwrite(client->line, sizeof(char), len, client->entry.data); } } @@ -3166,7 +3214,7 @@ CommandQcrea(void *param) { unsigned long id; unsigned long target; - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (Agent.flags & QUEUE_AGENT_DISK_SPACE_LOW) { @@ -3198,7 +3246,7 @@ CommandQcrea(void *param) id = Queue.queueID++ & ((1 << 28) - 1); XplMutexUnlock(Queue.queueIDLock); - sprintf(client->path, "%s/c%07lx.in", Conf.spoolPath, id); + QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, id); FOPEN_CHECK(client->entry.control, client->path, "wb"); if (client->entry.control) { fprintf(client->entry.control, QUEUES_DATE"%lu\r\n", time(NULL)); @@ -3206,13 +3254,13 @@ CommandQcrea(void *param) return(ConnWrite(client->conn, MSG5221SPACELOW, sizeof(MSG5221SPACELOW) - 1)); } - sprintf(client->path, "%s/d%07lx.msg", Conf.spoolPath, id); + QueueFormat(client->path, "%s/d%07lx.msg", Conf.spoolPath, id); FOPEN_CHECK(client->entry.data, client->path, "wb"); if (client->entry.data) { client->entry.id = id; } else { FCLOSE_CHECK(client->entry.control); - sprintf(client->path, "%s/c%07lx.in", Conf.spoolPath, id); + QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, id); UNLINK_CHECK(client->path); return(ConnWrite(client->conn, MSG5221SPACELOW, sizeof(MSG5221SPACELOW) - 1)); @@ -3225,7 +3273,7 @@ CommandQcrea(void *param) int CommandQdele(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 5; @@ -3247,10 +3295,10 @@ CommandQdele(void *param) /* fixme - LockQueueEntry? LockQueueEntry(ptr+4, atoi(ptr)); */ - sprintf(client->path, "%s/c%s.%s", Conf.spoolPath, ptr + 4, ptr); + QueueFormat(client->path, "%s/c%s.%s", Conf.spoolPath, ptr + 4, ptr); UNLINK_CHECK(client->path); - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); UNLINK_CHECK(client->path); /* FIXME: close out the file handles? */ @@ -3265,7 +3313,7 @@ int CommandQdone(void *param) { int ccode; - unsigned char path[XPL_MAX_PATH + 1]; + char path[XPL_MAX_PATH + 1]; struct stat sb; struct stat sb1; struct stat sb2; @@ -3277,8 +3325,8 @@ CommandQdone(void *param) FCLOSE_CHECK(client->entry.work); client->entry.work = NULL; if (client->entry.workQueue[0] != '\0') { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); - sprintf(path, "%s/c%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(path, "%s/c%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); if ((stat(client->path, &sb1) == 0) && (stat(path, &sb2) == 0)) { UNLINK_CHECK(path); RENAME_CHECK(client->path, path); @@ -3331,9 +3379,9 @@ int CommandQgrep(void *param) { int ccode; - int length; + size_t length; char *field; - unsigned char *ptr; + char *ptr; BOOL found = FALSE; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -3356,7 +3404,7 @@ CommandQgrep(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); FOPEN_CHECK(data, client->path, "rb"); if (data) { length = strlen(field); @@ -3404,7 +3452,7 @@ CommandQhead(void *param) { int ccode; unsigned long count = 0; - unsigned char *ptr; + char *ptr; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -3424,7 +3472,7 @@ CommandQhead(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path,"%s/d%s.msg", Conf.spoolPath, ptr+4); + QueueFormat(client->path,"%s/d%s.msg", Conf.spoolPath, ptr+4); FOPEN_CHECK(data, client->path, "rb"); if (data) { while (!feof(data) && !ferror(data)) { @@ -3457,7 +3505,7 @@ int CommandQinfo(void *param) { unsigned long count = 0; - unsigned char *ptr; + char *ptr; struct stat sb; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -3478,7 +3526,7 @@ CommandQinfo(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); if (stat(client->path, &sb) == 0) { FOPEN_CHECK(data, client->path, "rb"); } @@ -3509,13 +3557,13 @@ CommandQinfo(void *param) int CommandQmodFrom(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 9; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -3534,13 +3582,13 @@ CommandQmodFrom(void *param) int CommandQmodFlags(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 10; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -3559,13 +3607,13 @@ CommandQmodFlags(void *param) int CommandQmodLocal(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 10; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -3584,13 +3632,13 @@ CommandQmodLocal(void *param) int CommandQmodMailbox(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 12; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -3609,13 +3657,13 @@ CommandQmodMailbox(void *param) int CommandQmodRaw(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 8; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -3634,13 +3682,13 @@ CommandQmodRaw(void *param) int CommandQmodTo(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 7; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -3662,7 +3710,7 @@ CommandQmime(void *param) int ccode; long size; unsigned long i; - unsigned char *ptr; + char *ptr; struct stat sb; FILE *data = NULL; MIMEReportStruct *report; @@ -3686,7 +3734,7 @@ CommandQmime(void *param) if (!client->entry.report) { /* Find the message size */ - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); if (stat(client->path, &sb) == 0) { FOPEN_CHECK(data, client->path, "rb"); } @@ -3741,9 +3789,9 @@ int CommandQmove(void *param) { int ccode; - unsigned char *ptr; - unsigned char *ptr2; - unsigned char path[XPL_MAX_PATH + 1]; + char *ptr; + char *ptr2; + char path[XPL_MAX_PATH + 1]; struct stat sb; QueueClient *client = (QueueClient *)param; @@ -3765,15 +3813,15 @@ CommandQmove(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/c%s.%s", Conf.spoolPath, ptr + 4, ptr); + QueueFormat(client->path, "%s/c%s.%s", Conf.spoolPath, ptr + 4, ptr); if (stat(client->path, &sb) == 0) { - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, ptr + 4, atoi(ptr)); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, ptr + 4, atoi(ptr)); RENAME_CHECK(client->path, path); ccode = ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1); } else { - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); UNLINK_CHECK(client->path); ccode = ConnWrite(client->conn, MSG4224NOENTRY, sizeof(MSG4224NOENTRY) - 1); @@ -3787,8 +3835,8 @@ CommandQrcp(void *param) { int ccode; unsigned long id; - unsigned char *ptr; - unsigned char path[XPL_MAX_PATH + 1]; + char *ptr; + char path[XPL_MAX_PATH + 1]; FILE *source = NULL; QueueClient *client = (QueueClient *)param; @@ -3817,31 +3865,31 @@ CommandQrcp(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/c%07lx.in", Conf.spoolPath, id); + QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, id); FOPEN_CHECK(client->entry.control, client->path,"wb"); if (client->entry.control) { fprintf(client->entry.control, QUEUES_DATE"%lu\r\n", time(NULL)); - sprintf(client->path, "%s/d%07lx.msg", Conf.spoolPath, id); + QueueFormat(client->path, "%s/d%07lx.msg", Conf.spoolPath, id); FOPEN_CHECK(client->entry.data, client->path, "wb"); if (client->entry.data) { - sprintf(client->path, "%s/d%07lx.msg", Conf.spoolPath, client->entry.id); + QueueFormat(client->path, "%s/d%07lx.msg", Conf.spoolPath, client->entry.id); FOPEN_CHECK(source, client->path, "rb"); while (!feof(source) && !ferror(source)) { - ccode = fread(client->line, sizeof(unsigned char), CONN_BUFSIZE, source); + ccode = fread(client->line, sizeof(char), CONN_BUFSIZE, source); if (ccode) { - fwrite(client->line, sizeof(unsigned char), ccode, client->entry.data); + fwrite(client->line, sizeof(char), ccode, client->entry.data); } } FCLOSE_CHECK(source); - sprintf(client->path,"%s/c%07lx.in",Conf.spoolPath, client->entry.id); - sprintf(path, "%s/c%07lx.%03ld", Conf.spoolPath, client->entry.id, client->entry.target); + QueueFormat(client->path,"%s/c%07lx.in",Conf.spoolPath, client->entry.id); + QueueFormat(path, "%s/c%07lx.%03ld", Conf.spoolPath, client->entry.id, client->entry.target); RENAME_CHECK(client->path, path); - sprintf(client->path, "%03ld%lx", client->entry.target, client->entry.id); + QueueFormat(client->path, "%03ld%lx", client->entry.target, client->entry.id); if (XplSafeRead(Queue.activeWorkers) < Conf.maxConcurrentWorkers) { XplThreadID threadID; @@ -3867,14 +3915,14 @@ CommandQrcp(void *param) FCLOSE_CHECK(client->entry.control); client->entry.control = NULL; - sprintf(client->path, "%s/c%07lx.in", Conf.spoolPath, id); + QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, id); UNLINK_CHECK(client->path); } - sprintf(client->path, "%s/c%07lx.in", Conf.spoolPath, client->entry.id); + QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, client->entry.id); FOPEN_CHECK(client->entry.control, path, "a+b"); - sprintf(client->path,"%s/d%07lx.msg", Conf.spoolPath, client->entry.id); + QueueFormat(client->path,"%s/d%07lx.msg", Conf.spoolPath, client->entry.id); FOPEN_CHECK(client->entry.data, path, "a+b"); if (client->entry.data && client->entry.control) { @@ -3900,8 +3948,8 @@ int CommandQretr(void *param) { int ccode; - unsigned char *ptr; - unsigned char *ptr2; + char *ptr; + char *ptr2; struct stat sb; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -3927,7 +3975,7 @@ CommandQretr(void *param) } if (XplStrCaseCmp(ptr2, "INFO") == 0) { - sprintf(client->path, "%s/c07%s.%s", Conf.spoolPath, ptr + 4, ptr); + QueueFormat(client->path, "%s/c07%s.%s", Conf.spoolPath, ptr + 4, ptr); if ((stat(client->path, &sb) == 0) && sb.st_size) { FOPEN_CHECK(data, client->path, "rb"); } @@ -3938,7 +3986,7 @@ CommandQretr(void *param) return(ConnWrite(client->conn, MSG4224CANTREAD, sizeof(MSG4224CANTREAD) - 1)); } } else if (XplStrCaseCmp(ptr2, "MESSAGE") == 0) { - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); if (stat(client->path, &sb) == 0) { FOPEN_CHECK(data, client->path, "rb"); } @@ -3964,9 +4012,9 @@ CommandQretr(void *param) int CommandQrts(void *param) { - unsigned char *recipient; - unsigned char *oRecipient; - unsigned char *flags; + char *recipient; + char *oRecipient; + char *flags; QueueClient *client = (QueueClient *)param; @@ -3978,7 +4026,7 @@ CommandQrts(void *param) recipient = client->buffer + 4; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -4006,8 +4054,8 @@ CommandQrun(void *param) int ccode; unsigned long target; unsigned long id; - unsigned char *ptr; - unsigned char path[XPL_MAX_PATH + 1]; + char *ptr; + char path[XPL_MAX_PATH + 1]; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 4; @@ -4024,15 +4072,15 @@ CommandQrun(void *param) return(ConnWrite(client->conn, MSG4000CANTUNLOCKENTRY, sizeof(MSG4000CANTUNLOCKENTRY) - 1)); } - sprintf(client->path,"%s/c%07lx.in",Conf.spoolPath, client->entry.id); - sprintf(path, "%s/c%07lx.%03ld", Conf.spoolPath, client->entry.id, client->entry.target); + QueueFormat(client->path,"%s/c%07lx.in",Conf.spoolPath, client->entry.id); + QueueFormat(path, "%s/c%07lx.%03ld", Conf.spoolPath, client->entry.id, client->entry.target); RENAME_CHECK(client->path, path); XplSafeIncrement(Queue.queuedLocal); ccode = ConnWriteF(client->conn, "1000 %03ld-%lx OK\r\n", client->entry.target, client->entry.id); - sprintf(client->path, "%03ld%07lx", client->entry.target, client->entry.id); + QueueFormat(client->path, "%03ld%07lx", client->entry.target, client->entry.id); client->entry.id = 0; } else if ((*ptr++ == ' ') && (!isspace(*ptr)) @@ -4049,7 +4097,7 @@ CommandQrun(void *param) ccode = ConnWriteF(client->conn, "1000 %03ld-%lx OK\r\n", target, id); - sprintf(client->path, "%03ld%07lx", target, id); + QueueFormat(client->path, "%03ld%07lx", target, id); } else { return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } @@ -4074,7 +4122,7 @@ int CommandQsrchDomain(void *param) { int ccode; - unsigned char *ptr; + char *ptr; void *handle = NULL; QueueClient *client = (QueueClient *)param; @@ -4114,7 +4162,7 @@ CommandQsrchHeader(void *param) int length; char *field; char *content; - unsigned char *ptr; + char *ptr; BOOL fieldFound=FALSE; BOOL contentFound=FALSE; FILE *data = NULL; @@ -4148,7 +4196,7 @@ CommandQsrchHeader(void *param) } - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); FOPEN_CHECK(data, client->path, "rb"); if (data) { while (!feof(data) && !ferror(data)) { @@ -4203,9 +4251,8 @@ int CommandQsrchBody(void *param) { int ccode; - int length; char *content; - unsigned char *ptr; + char *ptr; BOOL found = FALSE; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -4226,12 +4273,11 @@ CommandQsrchBody(void *param) ptr[3] = '\0'; *content++ = '\0'; - length = strlen(content); } else { return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); FOPEN_CHECK(data, client->path, "rb"); if (data) { while (!feof(data) && !ferror(data)) { @@ -4246,7 +4292,7 @@ CommandQsrchBody(void *param) /* fixme - this doesn't search across buffer bondaries! */ while (!feof(data) && !ferror(data)) { - if (fread(client->line, sizeof(unsigned char), CONN_BUFSIZE, data) > 0) { + if (fread(client->line, sizeof(char), CONN_BUFSIZE, data) > 0) { if (PDBSearch(client->line, content) == FALSE) { continue; } @@ -4275,13 +4321,12 @@ CommandQsrchBraw(void *param) { int ccode; int read; - int length; unsigned long start; unsigned long end; - unsigned char *ptr; - unsigned char *startPtr; - unsigned char *endPtr; - unsigned char *content; + char *ptr; + char *startPtr; + char *endPtr; + char *content; BOOL found = FALSE; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -4312,12 +4357,11 @@ CommandQsrchBraw(void *param) start = atol(startPtr); end = atol(endPtr); - length = strlen(content); } else { return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); FOPEN_CHECK(data, client->path, "rb"); if (data) { while (!feof(data) && !ferror(data)) { @@ -4336,9 +4380,9 @@ CommandQsrchBraw(void *param) /* fixme - this doesn't search across buffer bondaries! */ while ((end > 0) && !feof(data) && !ferror(data)) { if (end >= CONN_BUFSIZE) { - read = fread(client->line, sizeof(unsigned char), CONN_BUFSIZE, data); + read = fread(client->line, sizeof(char), CONN_BUFSIZE, data); } else { - read = fread(client->line, sizeof(unsigned char), end, data); + read = fread(client->line, sizeof(char), end, data); } if (read) { @@ -4371,7 +4415,7 @@ CommandQsrchBraw(void *param) int CommandQstorAddress(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4394,7 +4438,7 @@ CommandQstorAddress(void *param) int CommandQstorCal(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4417,7 +4461,7 @@ CommandQstorCal(void *param) int CommandQstorFlags(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4440,7 +4484,7 @@ CommandQstorFlags(void *param) int CommandQstorFrom(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4462,7 +4506,7 @@ CommandQstorFrom(void *param) int CommandQstorLocal(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4487,9 +4531,9 @@ CommandQstorMessage(void *param) { int ccode; long count; - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; - unsigned char TimeBuf[80]; + char TimeBuf[80]; if (!client->entry.data || !client->entry.control) { return(ConnWrite(client->conn, MSG4260NOQENTRY, sizeof(MSG4260NOQENTRY) - 1)); @@ -4549,7 +4593,7 @@ CommandQstorMessage(void *param) int CommandQstorRaw(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4572,7 +4616,7 @@ CommandQstorRaw(void *param) int CommandQstorTo(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4598,8 +4642,8 @@ CommandQwait(void *param) int ccode; int port; int queue; - unsigned char *ptr; - unsigned char *identifier; + char *ptr; + char *identifier; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 5; diff --git a/src/agents/queue/queue.h b/src/agents/queue/queue.h index 5067f6d..b3f471b 100644 --- a/src/agents/queue/queue.h +++ b/src/agents/queue/queue.h @@ -36,7 +36,7 @@ typedef struct _QueuePushClient { unsigned long usageCount; unsigned long errorCount; - unsigned char identifier[101]; + char identifier[101]; } QueuePushClient; typedef struct _QueueList { @@ -46,7 +46,7 @@ typedef struct _QueueList { } QueueList; typedef struct _QueuePoolList { - unsigned char identifier[101]; /* each queue agent should have a unique identifier so that pooling will work correctly */ + char identifier[101]; /* each queue agent should have a unique identifier so that pooling will work correctly */ AddressPool pool; } QueuePoolList; diff --git a/src/agents/queue/queued.c b/src/agents/queue/queued.c index f92608f..99690b0 100644 --- a/src/agents/queue/queued.c +++ b/src/agents/queue/queued.c @@ -36,7 +36,7 @@ #include "domain.h" #include "messages.h" -QueueAgent Agent = { {0, }, }; +QueueAgent Agent = {0}; static int CommandAuth(void *param); static int CommandPass(void *param); @@ -47,60 +47,60 @@ static int CommandDomainLocation(void *param); int aliasFindFunc(const void *str, const void *node); static ProtocolCommand authCommands[] = { - { NMAP_AUTH_COMMAND, NMAP_AUTH_HELP, sizeof(NMAP_AUTH_COMMAND) - 1, CommandAuth, NULL, NULL }, - { NMAP_HELP_COMMAND, NMAP_HELP_HELP, sizeof(NMAP_HELP_COMMAND) - 1, CommandHelp, NULL, NULL }, - { NMAP_PASS_COMMAND, NMAP_PASS_HELP, sizeof(NMAP_PASS_COMMAND) - 1, CommandPass, NULL, NULL }, - { NMAP_QUIT_COMMAND, NMAP_QUIT_HELP, sizeof(NMAP_QUIT_COMMAND) - 1, CommandQuit, NULL, NULL }, - { NMAP_ADDRESS_RESOLVE_COMMAND, NMAP_ADDRESS_RESOLVE_HELP, sizeof(NMAP_ADDRESS_RESOLVE_COMMAND) -1, CommandAddressResolve, NULL, NULL }, - { NMAP_DOMAIN_LOCATION_COMMAND, NMAP_DOMAIN_LOCATION_HELP, sizeof(NMAP_DOMAIN_LOCATION_COMMAND) -1, CommandDomainLocation, NULL, NULL }, - { NULL, NULL, 0, NULL, NULL, NULL } + { NMAP_AUTH_COMMAND, NMAP_AUTH_HELP, sizeof(NMAP_AUTH_COMMAND) - 1, CommandAuth, NULL, NULL, NULL, BlackCommand }, + { NMAP_HELP_COMMAND, NMAP_HELP_HELP, sizeof(NMAP_HELP_COMMAND) - 1, CommandHelp, NULL, NULL, NULL, BlackCommand }, + { NMAP_PASS_COMMAND, NMAP_PASS_HELP, sizeof(NMAP_PASS_COMMAND) - 1, CommandPass, NULL, NULL, NULL, BlackCommand }, + { NMAP_QUIT_COMMAND, NMAP_QUIT_HELP, sizeof(NMAP_QUIT_COMMAND) - 1, CommandQuit, NULL, NULL, NULL, BlackCommand }, + { NMAP_ADDRESS_RESOLVE_COMMAND, NMAP_ADDRESS_RESOLVE_HELP, sizeof(NMAP_ADDRESS_RESOLVE_COMMAND) -1, CommandAddressResolve, NULL, NULL, NULL, BlackCommand }, + { NMAP_DOMAIN_LOCATION_COMMAND, NMAP_DOMAIN_LOCATION_HELP, sizeof(NMAP_DOMAIN_LOCATION_COMMAND) -1, CommandDomainLocation, NULL, NULL, NULL, BlackCommand }, + { NULL, NULL, 0, NULL, NULL, NULL, NULL, BlackCommand } }; static ProtocolCommand commands[] = { - { NMAP_HELP_COMMAND, NMAP_HELP_HELP, sizeof(NMAP_HELP_COMMAND) - 1, CommandHelp, NULL, NULL }, - { NMAP_QADDM_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QADDM_COMMAND) - 1, CommandQaddm, NULL, NULL }, - { NMAP_QADDQ_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QADDQ_COMMAND) - 1, CommandQaddq, NULL, NULL }, - { NMAP_QABRT_COMMAND, NMAP_QABRT_HELP, sizeof(NMAP_QABRT_COMMAND) - 1, CommandQabrt, NULL, NULL }, - { NMAP_QBODY_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QBODY_COMMAND) - 1, CommandQbody, NULL, NULL }, - { NMAP_QBRAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QBRAW_COMMAND) - 1, CommandQbraw, NULL, NULL }, - { NMAP_QCOPY_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QCOPY_COMMAND) - 1, CommandQcopy, NULL, NULL }, - { NMAP_QCREA_COMMAND, NMAP_QCREA_HELP, sizeof(NMAP_QCREA_COMMAND) - 1, CommandQcrea, NULL, NULL }, - { NMAP_QDELE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QDELE_COMMAND) - 1, CommandQdele, NULL, NULL }, - { NMAP_QDONE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QDONE_COMMAND) - 1, CommandQdone, NULL, NULL }, - { NMAP_QDSPC_COMMAND, NMAP_QDSPC_HELP, sizeof(NMAP_QDSPC_COMMAND) - 1, CommandQdspc, NULL, NULL }, - { NMAP_QGREP_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QGREP_COMMAND) - 1, CommandQgrep, NULL, NULL }, - { NMAP_QHEAD_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QHEAD_COMMAND) - 1, CommandQhead, NULL, NULL }, - { NMAP_QINFO_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QINFO_COMMAND) - 1, CommandQinfo, NULL, NULL }, - { NMAP_QMOD_FROM_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_FROM_COMMAND) - 1, CommandQmodFrom, NULL, NULL }, - { NMAP_QMOD_FLAGS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_FLAGS_COMMAND) - 1, CommandQmodFlags, NULL, NULL }, - { NMAP_QMOD_LOCAL_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_LOCAL_COMMAND) - 1, CommandQmodLocal, NULL, NULL }, - { NMAP_QMOD_MAILBOX_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_MAILBOX_COMMAND) - 1, CommandQmodMailbox, NULL, NULL }, - { NMAP_QMOD_RAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_RAW_COMMAND) - 1, CommandQmodRaw, NULL, NULL }, - { NMAP_QMOD_TO_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_TO_COMMAND) - 1, CommandQmodTo, NULL, NULL }, - { NMAP_QMIME_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMIME_COMMAND) - 1, CommandQmime, NULL, NULL }, - { NMAP_QMOVE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOVE_COMMAND) - 1, CommandQmove, NULL, NULL }, - { NMAP_QRCP_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QRCP_COMMAND) - 1, CommandQrcp, NULL, NULL }, - { NMAP_QRETR_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QRETR_COMMAND) - 1, CommandQretr, NULL, NULL }, - { NMAP_QRTS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QRTS_COMMAND) - 1, CommandQrts, NULL, NULL }, - { NMAP_QRUN_COMMAND, NMAP_QRUN_HELP, sizeof(NMAP_QRUN_COMMAND) - 1, CommandQrun, NULL, NULL }, - { NMAP_QSRCH_DOMAIN_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_DOMAIN_COMMAND) - 1, CommandQsrchDomain, NULL, NULL }, - { NMAP_QSRCH_HEADER_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_HEADER_COMMAND) - 1, CommandQsrchHeader, NULL, NULL }, - { NMAP_QSRCH_BODY_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_BODY_COMMAND) - 1, CommandQsrchBody, NULL, NULL }, - { NMAP_QSRCH_BRAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_BRAW_COMMAND) - 1, CommandQsrchBraw, NULL, NULL }, - { NMAP_QSTOR_ADDRESS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_ADDRESS_COMMAND) - 1, CommandQstorAddress, NULL, NULL }, - { NMAP_QSTOR_CAL_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_CAL_COMMAND) - 1, CommandQstorCal, NULL, NULL }, - { NMAP_QSTOR_FLAGS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_FLAGS_COMMAND) - 1, CommandQstorFlags, NULL, NULL }, - { NMAP_QSTOR_FROM_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_FROM_COMMAND) - 1, CommandQstorFrom, NULL, NULL }, - { NMAP_QSTOR_LOCAL_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_LOCAL_COMMAND) - 1, CommandQstorLocal, NULL, NULL }, - { NMAP_QSTOR_MESSAGE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_MESSAGE_COMMAND) - 1, CommandQstorMessage, NULL, NULL }, - { NMAP_QSTOR_RAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_RAW_COMMAND) - 1, CommandQstorRaw, NULL, NULL }, - { NMAP_QSTOR_TO_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_TO_COMMAND) - 1, CommandQstorTo, NULL, NULL }, - { NMAP_QUIT_COMMAND, NMAP_QUIT_HELP, sizeof(NMAP_QUIT_COMMAND) - 1, CommandQuit, NULL, NULL }, - { NMAP_QWAIT_COMMAND, NMAP_QWAIT_HELP, sizeof(NMAP_QWAIT_COMMAND) - 1, CommandQwait, NULL, NULL }, - { NMAP_QFLUSH_COMMAND, NMAP_QFLUSH_HELP, sizeof(NMAP_QFLUSH_COMMAND) -1, CommandQflush, NULL, NULL }, - { NMAP_ADDRESS_RESOLVE_COMMAND, NMAP_ADDRESS_RESOLVE_HELP, sizeof(NMAP_ADDRESS_RESOLVE_COMMAND) -1, CommandAddressResolve, NULL, NULL }, - { NMAP_DOMAIN_LOCATION_COMMAND, NMAP_DOMAIN_LOCATION_HELP, sizeof(NMAP_DOMAIN_LOCATION_COMMAND) -1, CommandDomainLocation, NULL, NULL }, - { NULL, NULL, 0, NULL, NULL, NULL } + { NMAP_HELP_COMMAND, NMAP_HELP_HELP, sizeof(NMAP_HELP_COMMAND) - 1, CommandHelp, NULL, NULL, NULL, BlackCommand }, + { NMAP_QADDM_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QADDM_COMMAND) - 1, CommandQaddm, NULL, NULL, NULL, BlackCommand }, + { NMAP_QADDQ_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QADDQ_COMMAND) - 1, CommandQaddq, NULL, NULL, NULL, BlackCommand }, + { NMAP_QABRT_COMMAND, NMAP_QABRT_HELP, sizeof(NMAP_QABRT_COMMAND) - 1, CommandQabrt, NULL, NULL, NULL, BlackCommand }, + { NMAP_QBODY_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QBODY_COMMAND) - 1, CommandQbody, NULL, NULL, NULL, BlackCommand }, + { NMAP_QBRAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QBRAW_COMMAND) - 1, CommandQbraw, NULL, NULL, NULL, BlackCommand }, + { NMAP_QCOPY_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QCOPY_COMMAND) - 1, CommandQcopy, NULL, NULL, NULL, BlackCommand }, + { NMAP_QCREA_COMMAND, NMAP_QCREA_HELP, sizeof(NMAP_QCREA_COMMAND) - 1, CommandQcrea, NULL, NULL, NULL, BlackCommand }, + { NMAP_QDELE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QDELE_COMMAND) - 1, CommandQdele, NULL, NULL, NULL, BlackCommand }, + { NMAP_QDONE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QDONE_COMMAND) - 1, CommandQdone, NULL, NULL, NULL, BlackCommand }, + { NMAP_QDSPC_COMMAND, NMAP_QDSPC_HELP, sizeof(NMAP_QDSPC_COMMAND) - 1, CommandQdspc, NULL, NULL, NULL, BlackCommand }, + { NMAP_QGREP_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QGREP_COMMAND) - 1, CommandQgrep, NULL, NULL, NULL, BlackCommand }, + { NMAP_QHEAD_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QHEAD_COMMAND) - 1, CommandQhead, NULL, NULL, NULL, BlackCommand }, + { NMAP_QINFO_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QINFO_COMMAND) - 1, CommandQinfo, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOD_FROM_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_FROM_COMMAND) - 1, CommandQmodFrom, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOD_FLAGS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_FLAGS_COMMAND) - 1, CommandQmodFlags, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOD_LOCAL_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_LOCAL_COMMAND) - 1, CommandQmodLocal, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOD_MAILBOX_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_MAILBOX_COMMAND) - 1, CommandQmodMailbox, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOD_RAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_RAW_COMMAND) - 1, CommandQmodRaw, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOD_TO_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_TO_COMMAND) - 1, CommandQmodTo, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMIME_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMIME_COMMAND) - 1, CommandQmime, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOVE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOVE_COMMAND) - 1, CommandQmove, NULL, NULL, NULL, BlackCommand }, + { NMAP_QRCP_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QRCP_COMMAND) - 1, CommandQrcp, NULL, NULL, NULL, BlackCommand }, + { NMAP_QRETR_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QRETR_COMMAND) - 1, CommandQretr, NULL, NULL, NULL, BlackCommand }, + { NMAP_QRTS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QRTS_COMMAND) - 1, CommandQrts, NULL, NULL, NULL, BlackCommand }, + { NMAP_QRUN_COMMAND, NMAP_QRUN_HELP, sizeof(NMAP_QRUN_COMMAND) - 1, CommandQrun, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSRCH_DOMAIN_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_DOMAIN_COMMAND) - 1, CommandQsrchDomain, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSRCH_HEADER_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_HEADER_COMMAND) - 1, CommandQsrchHeader, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSRCH_BODY_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_BODY_COMMAND) - 1, CommandQsrchBody, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSRCH_BRAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_BRAW_COMMAND) - 1, CommandQsrchBraw, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_ADDRESS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_ADDRESS_COMMAND) - 1, CommandQstorAddress, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_CAL_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_CAL_COMMAND) - 1, CommandQstorCal, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_FLAGS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_FLAGS_COMMAND) - 1, CommandQstorFlags, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_FROM_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_FROM_COMMAND) - 1, CommandQstorFrom, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_LOCAL_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_LOCAL_COMMAND) - 1, CommandQstorLocal, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_MESSAGE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_MESSAGE_COMMAND) - 1, CommandQstorMessage, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_RAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_RAW_COMMAND) - 1, CommandQstorRaw, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_TO_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_TO_COMMAND) - 1, CommandQstorTo, NULL, NULL, NULL, BlackCommand }, + { NMAP_QUIT_COMMAND, NMAP_QUIT_HELP, sizeof(NMAP_QUIT_COMMAND) - 1, CommandQuit, NULL, NULL, NULL, BlackCommand }, + { NMAP_QWAIT_COMMAND, NMAP_QWAIT_HELP, sizeof(NMAP_QWAIT_COMMAND) - 1, CommandQwait, NULL, NULL, NULL, BlackCommand }, + { NMAP_QFLUSH_COMMAND, NMAP_QFLUSH_HELP, sizeof(NMAP_QFLUSH_COMMAND) -1, CommandQflush, NULL, NULL, NULL, BlackCommand }, + { NMAP_ADDRESS_RESOLVE_COMMAND, NMAP_ADDRESS_RESOLVE_HELP, sizeof(NMAP_ADDRESS_RESOLVE_COMMAND) -1, CommandAddressResolve, NULL, NULL, NULL, BlackCommand }, + { NMAP_DOMAIN_LOCATION_COMMAND, NMAP_DOMAIN_LOCATION_HELP, sizeof(NMAP_DOMAIN_LOCATION_COMMAND) -1, CommandDomainLocation, NULL, NULL, NULL, BlackCommand }, + { NULL, NULL, 0, NULL, NULL, NULL, NULL, BlackCommand } }; int aliasFindFunc(const void *str, const void *node) { @@ -120,9 +120,9 @@ int hostedFindFunc(const void *str1, const void *str2) { * 2: username == domain * 3: username == auth subsystem */ -BOOL aliasing(char *addr, int *cnt, unsigned char *buffer) { - unsigned char *local = NULL; - unsigned char *domain = NULL; +BOOL aliasing(char *addr, int *cnt, char *buffer) { + char *local = NULL; + char *domain = NULL; int i=-1; BOOL result=FALSE; @@ -239,7 +239,7 @@ BOOL aliasing(char *addr, int *cnt, unsigned char *buffer) { int CommandAddressResolve(void *param) { BOOL handled=FALSE; int cnt=0; - unsigned char buffer[1024]; /* FIXME: is this too big? */ + char buffer[1024]; /* FIXME: is this too big? */ QueueClient *client = (QueueClient *)param; handled = aliasing(client->buffer + 16, &cnt, buffer); @@ -255,8 +255,8 @@ int CommandDomainLocation(void *param) { QueueClient *client = (QueueClient *)param; // FIXME: is this actually used at all with our current aliasing stuff? /* first find the domain in the request */ - unsigned char *domain = client->buffer + 16; - + char *domain = client->buffer + 16; + #if 0 /* now search for it */ int idx = GArrayFindSorted(Conf.hostedDomains, domain, (ArrayCompareFunc)hostedFindFunc); @@ -271,11 +271,11 @@ int CommandDomainLocation(void *param) { return 0; } -int +int CommandAuth(void *param) { - unsigned char *ptr; - unsigned char credential[XPLHASH_MD5_LENGTH]; + char *ptr; + char credential[XPLHASH_MD5_LENGTH]; QueueClient *client = (QueueClient *)param; xpl_hash_context context; @@ -284,7 +284,7 @@ CommandAuth(void *param) return ConnWriteStr(client->conn, MSG3000UNKNOWN); } ptr += 7; - + XplHashNew(&context, XPLHASH_MD5); XplHashWrite(&context, client->authChallenge, strlen(client->authChallenge)); XplHashWrite(&context, Conf.serverHash, NMAP_HASH_SIZE); @@ -304,11 +304,11 @@ CommandAuth(void *param) return(-1); } -int +int CommandPass(void *param) { - unsigned char *ptr; - unsigned char *pass; + char *ptr; + char *pass; int result; struct sockaddr_in nmap; QueueClient *client = (QueueClient *)param; @@ -316,32 +316,33 @@ CommandPass(void *param) ptr = client->buffer + 4; if (*ptr++ == ' ') { - if ( (toupper(ptr[0]) == 'U') - && (toupper(ptr[1]) == 'S') - && (toupper(ptr[2]) == 'E') - && (toupper(ptr[3]) == 'R') - && (ptr[4] == ' ') - && (ptr[5] != '\0') - && (!isspace(ptr[5])) + if ( (toupper(ptr[0]) == 'U') + && (toupper(ptr[1]) == 'S') + && (toupper(ptr[2]) == 'E') + && (toupper(ptr[3]) == 'R') + && (ptr[4] == ' ') + && (ptr[5] != '\0') + && (!isspace(ptr[5])) && ((pass = strchr(ptr + 5, ' ')) != NULL)) { ptr += 5; *pass++ = '\0'; result = MsgAuthVerifyPassword(ptr, pass); - if ((result==0) && (nmap.sin_addr.s_addr == MsgGetHostIPAddress())) { + if ((result == 0) && (MsgAuthGetUserStore(ptr, &nmap) == 0) && + (nmap.sin_addr.s_addr == MsgGetHostIPAddress())) { return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1)); } else { return(ConnWrite(client->conn, MSG4120USERLOCKED, sizeof(MSG4120USERLOCKED) - 1)); } Log(LOG_NOTICE, "PASS USER failed for user from host %s", LOGIP(client->conn->socketAddress)); - } else if ( (toupper(ptr[0]) == 'S') - && (toupper(ptr[1]) == 'Y') - && (toupper(ptr[2]) == 'S') - && (toupper(ptr[3]) == 'T') - && (toupper(ptr[4]) == 'E') - && (toupper(ptr[5]) == 'M') + } else if ( (toupper(ptr[0]) == 'S') + && (toupper(ptr[1]) == 'Y') + && (toupper(ptr[2]) == 'S') + && (toupper(ptr[3]) == 'T') + && (toupper(ptr[4]) == 'E') + && (toupper(ptr[5]) == 'M') && (ptr[6] == ' ')) { ptr += 7; @@ -362,25 +363,25 @@ CommandPass(void *param) ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1); client->done = TRUE; - + return(-1); } -static int +static int CommandQuit(void *param) { QueueClient *client = param; - + client->done = TRUE; return -1; } -static int +static int CommandHelp(void *param) { int ccode; - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 4; @@ -394,7 +395,7 @@ CommandHelp(void *param) } else { command = ProtocolCommandTreeSearch(&Agent.authCommands, ptr); } - + if (command) { if (command->help) { ccode = ConnWrite(client->conn, command->help, strlen(command->help)); @@ -415,7 +416,7 @@ CommandHelp(void *param) return(ccode); } -void +void QueueClientFree(void *clientp) { QueueClient *client = clientp; @@ -423,11 +424,11 @@ QueueClientFree(void *clientp) MemPrivatePoolReturnEntry(Agent.clientMemPool, client); } -static BOOL +static BOOL CheckTrustedHost(QueueClient *client) { int i = 0; - + if (Conf.trustedHosts) { /* TODO: this can be optimized a little bit */ XplRWReadLockAcquire(&Conf.lock); @@ -441,22 +442,22 @@ int HandleCommand(QueueClient *client) { int ccode = 0; - + while (Agent.agent.state < BONGO_AGENT_STATE_STOPPING && client && client->conn && !client->done) { client->buffer[0] = '\0'; ccode = ConnReadAnswer(client->conn, client->buffer, CONN_BUFSIZE); - if ((ccode != -1) && (ccode < CONN_BUFSIZE)) { + if ((ccode != -1) && (ccode < CONN_BUFSIZE)) { ProtocolCommand *command; if (client->authorized) { command = ProtocolCommandTreeSearch(&Agent.commands, client->buffer); } else { - command = ProtocolCommandTreeSearch(&Agent.authCommands, client->buffer); + command = ProtocolCommandTreeSearch(&Agent.authCommands, client->buffer); } - + if (command) { ccode = command->handler(client); } else { @@ -465,7 +466,7 @@ HandleCommand(QueueClient *client) } else { ccode = ConnWrite(client->conn, MSG3240NOAUTH, sizeof(MSG3240NOAUTH) - 1); } - + Log(LOG_DEBUG, "Handled command from %s", LOGIP(client->conn->socketAddress)); } } else if (ccode == -1) { @@ -477,11 +478,11 @@ HandleCommand(QueueClient *client) } } - return(ccode); + return(ccode); } static int -ProcessClient(void *clientp, Connection *conn) +ProcessClient(void *clientp, Connection *conn) { QueueClient *client = clientp; client->conn = conn; @@ -495,11 +496,11 @@ ProcessClient(void *clientp, Connection *conn) } else { ccode = ConnWriteF(client->conn, "1000 %s %s\r\n", BongoGlobals.hostname, MSG1000READY); } - + if (ccode == -1) { return -1; } - + if ((ccode = ConnFlush(client->conn)) == -1) { return -1; } @@ -507,9 +508,10 @@ ProcessClient(void *clientp, Connection *conn) return HandleCommand(client); } -static void +static void QueueServer(void *ignored) { + UNUSED_PARAMETER(ignored); XplRenameThread(XplGetThreadID(), AGENT_DN " Server"); XplSafeIncrement(Agent.activeThreads); @@ -556,7 +558,7 @@ ServerSocketInit(int port) conn->socketAddress.sin_port = htons(port); conn->socketAddress.sin_addr.s_addr = MsgGetAgentBindIPAddress(); - /* Get root privs back for the bind. It's ok if this fails - + /* Get root privs back for the bind. It's ok if this fails - * the user might not need to be root to bind to the port */ XplSetEffectiveUserId(0); @@ -589,8 +591,10 @@ CheckDiskspace(BongoAgent *agent) unsigned long bytesPerBlock; unsigned long wantFree; + UNUSED_PARAMETER(agent); + bytesPerBlock = XplGetDiskBlocksize(Conf.spoolPath); - freeBlocks = XplGetDiskspaceFree(Conf.spoolPath); + freeBlocks = XplGetDiskspaceFree(Conf.spoolPath); wantFree = (Conf.minimumFree / bytesPerBlock) + 1; if (freeBlocks != 0x7f000000) { @@ -600,7 +604,7 @@ CheckDiskspace(BongoAgent *agent) /* overflow if a volume had more than 16 TB. */ Agent.flags |= QUEUE_AGENT_DISK_SPACE_LOW; - + Log(LOG_ERROR, "Disk space for %s low: have %ld free, want %ld.", Conf.spoolPath, freeBlocks, wantFree); } else { Agent.flags &= ~QUEUE_AGENT_DISK_SPACE_LOW; @@ -623,19 +627,19 @@ CheckLoad(BongoAgent *agent) if (!Conf.loadMonitorDisabled) { current = XplGetServerUtilization(&upTime, &idleTime); - + if (current <= Conf.loadMonitorLow) { if (state) { if (Conf.defaultConcurrentWorkers > Conf.maxConcurrentWorkers) { Conf.maxConcurrentWorkers *= 2; } - + state = 0; } else { if (Conf.defaultConcurrentWorkers > Conf.maxConcurrentWorkers) { Conf.maxConcurrentWorkers *= 2; } - + if (Conf.defaultSequentialWorkers > Conf.maxSequentialWorkers) { Conf.maxSequentialWorkers *= 2; } @@ -653,12 +657,12 @@ CheckLoad(BongoAgent *agent) } Conf.queueCheck = CalculateCheckQueueLimit(Conf.maxConcurrentWorkers, Conf.maxSequentialWorkers); } - } + } } #endif -static void -SignalHandler(int sigtype) +static void +SignalHandler(int sigtype) { BongoAgentHandleSignal(&Agent.agent, sigtype); } @@ -672,13 +676,16 @@ XplServiceMain(int argc, char *argv[]) int ccode; int startupOpts; + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); + LogStart(); if (XplSetRealUser(MsgGetUnprivilegedUser()) < 0) { Log(LOG_ERROR, "Could not drop to unprivileged user '%s'", MsgGetUnprivilegedUser()); return -1; } - + XplInit(); /* Set the default port */ @@ -691,7 +698,7 @@ XplServiceMain(int argc, char *argv[]) Log(LOG_ERROR, "Initialization failed exiting."); return -1; } - + XplRWLockInit(&Conf.lock); XplSafeWrite(Agent.activeThreads, 0); @@ -733,9 +740,8 @@ XplServiceMain(int argc, char *argv[]) XplStartMainThread(AGENT_NAME, &id, QueueServer, 8192, NULL, ccode); LogShutdown(); - + XplUnloadApp(XplGetThreadID()); - + return 0; } - diff --git a/src/agents/queue/queued.h b/src/agents/queue/queued.h index fe33c9b..21030ae 100644 --- a/src/agents/queue/queued.h +++ b/src/agents/queue/queued.h @@ -46,10 +46,10 @@ typedef struct { BOOL done; /* Buffers */ - unsigned char authChallenge[MAXEMAILNAMESIZE + 1]; - unsigned char buffer[CONN_BUFSIZE + 1]; - unsigned char line[CONN_BUFSIZE + 1]; - unsigned char path[XPL_MAX_PATH + 1]; + char authChallenge[MAXEMAILNAMESIZE + 1]; + char buffer[CONN_BUFSIZE + 1]; + char line[CONN_BUFSIZE + 1]; + char path[XPL_MAX_PATH + 1]; /* Active queue entry */ struct { @@ -60,7 +60,7 @@ typedef struct { unsigned long id; unsigned long target; - unsigned char workQueue[12]; + char workQueue[12]; MIMEReportStruct *report; } entry; @@ -95,7 +95,7 @@ extern QueueAgent Agent; void QueueClientFree(void *clientp); int HandleCommand(QueueClient *client); -BOOL aliasing(char *addr, int *cnt, unsigned char *buffer); +BOOL aliasing(char *addr, int *cnt, char *buffer); #define STACKSPACE_Q (1024*80) diff --git a/src/agents/rules/CMakeLists.txt b/src/agents/rules/CMakeLists.txt index 762ef7c..80576d6 100644 --- a/src/agents/rules/CMakeLists.txt +++ b/src/agents/rules/CMakeLists.txt @@ -9,6 +9,7 @@ target_link_libraries(bongorules bongoutil bongojson bongomsgapi + libbongosieve ) install(TARGETS bongorules DESTINATION ${SBIN_INSTALL_DIR}) diff --git a/src/agents/rules/rules.c b/src/agents/rules/rules.c index f03199e..43c23f4 100644 --- a/src/agents/rules/rules.c +++ b/src/agents/rules/rules.c @@ -30,11 +30,373 @@ #include #include #include +#include #include #include "rules.h" -RulesAgentGlobals RulesAgent = {{0,}, }; +RulesAgentGlobals RulesAgent = {0}; + +typedef struct { + RulesClient *client; + const char *message; + int result; + BOOL written; +} SieveExecution; + +static int SieveAddress(const char *input, char output[1025]); + +static int +SieveAutomatedMessage(const char *message) +{ + const char *value; + if (!message) return 1; + value = strcasestr(message, "\nAuto-Submitted:"); + if (value && strncasecmp(value + 16, " no", 3)) return 1; + if (strcasestr(message, "\nList-Id:") || strcasestr(message, "\nList-Post:") || + strcasestr(message, "\nX-Auto-Response-Suppress:")) return 1; + value = strcasestr(message, "\nPrecedence:"); + if (value && (strcasestr(value, "bulk") == value + 12 || + strcasestr(value, "list") == value + 12 || + strcasestr(value, "junk") == value + 12)) return 1; + return 0; +} + +static void +SieveVacationHandle(const BongoSieveResult *result, char handle[33]) +{ + unsigned long hash = 2166136261UL; + const unsigned char *cursor; + if (result->handle && *result->handle && strlen(result->handle) <= 32) { + strcpy(handle, result->handle); + return; + } + for (cursor = (const unsigned char *)result->argument; *cursor; cursor++) { + hash = (hash ^ *cursor) * 16777619UL; + } + snprintf(handle, 33, "%08lx", hash & 0xffffffffUL); +} + +static int +SieveVacationMimeSafe(const char *text) +{ + const char *line = text; + int contentType = 0; + if (!text || !*text) return 0; + while (*line) { + const char *end = strchr(line, '\n'); + size_t length = end ? (size_t)(end - line) : strlen(line); + if (length && line[length - 1] == '\r') length--; + if (!length) return contentType; + if (length > 998) return 0; + if (*line != ' ' && *line != '\t') { + const char *colon = memchr(line, ':', length); + size_t nameLength; + if (!colon) return 0; + nameLength = (size_t)(colon - line); + if (nameLength == 12 && !strncasecmp(line, "Content-Type", 12)) { + contentType = 1; + } else if (!((nameLength == 25 && !strncasecmp(line, "Content-Transfer-Encoding", 25)) || + (nameLength == 19 && !strncasecmp(line, "Content-Disposition", 19)) || + (nameLength == 16 && !strncasecmp(line, "Content-Language", 16)))) { + return 0; + } + } + if (!end) return 0; + line = end + 1; + } + return 0; +} + +static char * +SieveNormalizeCRLF(const char *input) +{ + size_t length = strlen(input), index, output = 0; + char *normalized = malloc(length * 2 + 3); + if (!normalized) return NULL; + for (index = 0; index < length; index++) { + if (input[index] == '\r') { + if (index + 1 >= length || input[index + 1] != '\n') { + free(normalized); + return NULL; + } + normalized[output++] = '\r'; + normalized[output++] = '\n'; + index++; + } else if (input[index] == '\n') { + normalized[output++] = '\r'; + normalized[output++] = '\n'; + } else { + normalized[output++] = input[index]; + } + } + normalized[output] = '\0'; + return normalized; +} + +static int +SieveSendVacation(SieveExecution *execution, const BongoSieveResult *result) +{ + RulesClient *client = execution->client; + const char *sender = client->sender; + const char *from = result->from ? result->from : client->recipient; + const char *subject = result->subject ? result->subject : "Automatic reply"; + char safeSender[1025], safeFrom[1025], handle[33]; + char safeAlias[1025]; + char *reply, *reason; + size_t replySize; + size_t alias; + unsigned long interval; + int code; + if (!sender || !strcmp(sender, "-") || SieveAutomatedMessage(execution->message) || + !SieveAddress(sender, safeSender) || !SieveAddress(from, safeFrom) || + strcasecmp(safeFrom, client->recipient) || strchr(subject, '\r') || + strchr(subject, '\n') || strlen(subject) > 998 || + (result->mime && !SieveVacationMimeSafe(result->argument))) return 1; + for (alias = 0; alias < result->address_count; alias++) { + if (!SieveAddress(result->addresses[alias], safeAlias)) return 0; + } + reason = SieveNormalizeCRLF(result->argument); + if (!reason) return 0; + interval = result->has_seconds ? result->seconds : result->days * 86400UL; + if (interval < 3600) interval = 3600; + SieveVacationHandle(result, handle); + if (!BongoSieveVacationClaim(RulesAgent.SieveStore, client->recipient, + safeSender, handle, interval)) { free(reason); return 1; } + replySize = strlen(reason) + strlen(safeSender) + strlen(safeFrom) + + strlen(subject) + 320; + reply = malloc(replySize); + if (!reply) { + BongoSieveVacationRelease(RulesAgent.SieveStore, client->recipient, + safeSender, handle); + free(reason); + return 0; + } + code = snprintf(reply, replySize, + "From: %s\r\nTo: %s\r\nSubject: %s\r\n" + "Auto-Submitted: auto-replied\r\nPrecedence: bulk\r\n" + "MIME-Version: 1.0\r\n%s%s%s", + safeFrom, safeSender, subject, + result->mime ? "" : "Content-Type: text/plain; charset=UTF-8\r\n" + "Content-Transfer-Encoding: 8bit\r\n\r\n", + reason, result->mime ? "" : "\r\n"); + free(reason); + if (code < 0 || (size_t)code >= replySize) { + BongoSieveVacationRelease(RulesAgent.SieveStore, client->recipient, + safeSender, handle); + free(reply); + return 0; + } +#define VACATION_COMMAND(command) \ + (ConnWriteF(client->conn, command) != -1 && ConnFlush(client->conn) != -1 && \ + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE) == NMAP_OK) + if (!VACATION_COMMAND("QCREA\r\n") || + !VACATION_COMMAND("QSTOR FROM - -\r\n") || + ConnWriteF(client->conn, "QSTOR TO %s - 0\r\n", safeSender) == -1 || + ConnFlush(client->conn) == -1 || + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE) != NMAP_OK || + ConnWriteF(client->conn, "QSTOR MESSAGE %d\r\n", code) == -1 || + ConnWrite(client->conn, reply, code) == -1 || ConnFlush(client->conn) == -1 || + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE) != NMAP_OK || + !VACATION_COMMAND("QRUN\r\n")) { + ConnWriteF(client->conn, "QABRT\r\n"); + ConnFlush(client->conn); + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE); + BongoSieveVacationRelease(RulesAgent.SieveStore, client->recipient, + safeSender, handle); + free(reply); + return 0; + } +#undef VACATION_COMMAND + Log(LOG_INFO, "Sent vacation response for %s to %s", client->recipient, safeSender); + free(reply); + return 1; +} + +static int +SieveSendReject(SieveExecution *execution, const BongoSieveResult *result) +{ + RulesClient *client = execution->client; + const char *sender = client->sender; + char safeSender[1025], safeRecipient[1025], boundary[96]; + char *reason, *notice; + size_t noticeSize; + int code; + + /* RFC 5429 permits suppressing a notification where it would amplify + * forged or automated mail. The rejected message is still discarded. */ + if (!sender || !strcmp(sender, "-") || SieveAutomatedMessage(execution->message) || + !SieveAddress(sender, safeSender)) { + Log(LOG_NOTICE, "Suppressed Sieve reject notification for %s (unsafe sender)", + client->recipient); + execution->written = TRUE; + return 1; + } + if (!SieveAddress(client->recipient, safeRecipient)) return 0; + reason = SieveNormalizeCRLF(result->argument); + if (!reason || strlen(reason) > 64 * 1024 || strstr(reason, "\r\n.\r\n")) { + free(reason); + return 0; + } + snprintf(boundary, sizeof(boundary), "bongo-reject-%lu-%lu", + (unsigned long)getpid(), (unsigned long)time(NULL)); + noticeSize = strlen(reason) + strlen(safeSender) + strlen(safeRecipient) + + strlen(boundary) * 3 + 1024; + notice = malloc(noticeSize); + if (!notice) { free(reason); return 0; } + code = snprintf(notice, noticeSize, + "From: Mail Delivery Subsystem <%s>\r\nTo: %s\r\n" + "Subject: Message rejected by recipient mail filter\r\n" + "Auto-Submitted: auto-replied\r\nMIME-Version: 1.0\r\n" + "Content-Type: multipart/report; report-type=disposition-notification; " + "boundary=\"%s\"\r\n\r\n" + "--%s\r\nContent-Type: text/plain; charset=UTF-8\r\n" + "Content-Transfer-Encoding: 8bit\r\n\r\n" + "The message was refused by the recipient's mail filtering program.\r\n" + "The reason given was:\r\n\r\n%s\r\n" + "--%s\r\nContent-Type: message/disposition-notification\r\n\r\n" + "Reporting-UA: Bongo; %s\r\nFinal-Recipient: rfc822; %s\r\n" + "Disposition: automatic-action/MDN-sent-automatically; deleted\r\n\r\n" + "--%s--\r\n", + safeRecipient, safeSender, boundary, boundary, reason, boundary, + BONGO_VERSION, safeRecipient, boundary); + free(reason); + if (code < 0 || (size_t)code >= noticeSize) { free(notice); return 0; } +#define REJECT_COMMAND(command) \ + (ConnWriteF(client->conn, command) != -1 && ConnFlush(client->conn) != -1 && \ + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE) == NMAP_OK) + if (!REJECT_COMMAND("QCREA\r\n") || + !REJECT_COMMAND("QSTOR FROM - -\r\n") || + ConnWriteF(client->conn, "QSTOR TO %s - 0\r\n", safeSender) == -1 || + ConnFlush(client->conn) == -1 || + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE) != NMAP_OK || + ConnWriteF(client->conn, "QSTOR MESSAGE %d\r\n", code) == -1 || + ConnWrite(client->conn, notice, code) == -1 || ConnFlush(client->conn) == -1 || + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE) != NMAP_OK || + !REJECT_COMMAND("QRUN\r\n")) { + ConnWriteF(client->conn, "QABRT\r\n"); + ConnFlush(client->conn); + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE); + free(notice); + return 0; + } +#undef REJECT_COMMAND + Log(LOG_INFO, "Sent Sieve reject notification for %s to %s", + client->recipient, safeSender); + free(notice); + execution->written = TRUE; + return 1; +} + +static int +SieveMailboxName(const char *input, char output[1025]) +{ + size_t index, length; + if (!input || !(length = strlen(input)) || length > 1024) return 0; + for (index = 0; index < length; index++) { + unsigned char value = (unsigned char)input[index]; + if (value < 0x20 || value == 0x7f) return 0; + output[index] = value == ' ' ? 0x7f : (char)value; + } + output[length] = '\0'; + return 1; +} + +static int +SieveAddress(const char *input, char output[1025]) +{ + char *local = NULL, *domain = NULL; + size_t length; + int valid; + if (!input || !(length = strlen(input)) || length > 1024 || + strchr(input, '\r') || strchr(input, '\n') || strchr(input, ' ')) return 0; + memcpy(output, input, length + 1); + valid = MsgParseAddress(output, length, &local, &domain) && local && *local && + domain && *domain; + MemFree(local); + MemFree(domain); + return valid; +} + +static int +SieveAction(const BongoSieveResult *action_result, void *data) +{ + SieveExecution *execution = data; + RulesClient *client = execution->client; + BongoSieveAction action = action_result->action; + const char *argument = action_result->argument; + char safe[1025]; + if (execution->result == -1) return 0; + switch (action) { + case BONGO_SIEVE_KEEP: + execution->result = ConnWriteF(client->conn, "QMOD RAW %s\r\n", + client->envelopeLine); + execution->written = TRUE; + break; + case BONGO_SIEVE_DISCARD: + execution->written = TRUE; + break; + case BONGO_SIEVE_FILEINTO: + if (!SieveMailboxName(argument, safe)) return 0; + execution->result = NMAPSendCommandF(client->conn, + "QMOD MAILBOX %s %s %lu %s\r\n", client->recipient, + client->recipient, (unsigned long)(client->flags | NO_RULEPROCESSING), + safe); + execution->written = TRUE; + break; + case BONGO_SIEVE_REDIRECT: + if (!SieveAddress(argument, safe)) return 0; + execution->result = NMAPSendCommandF(client->conn, + "QMOD TO %s %s %lu\r\n", safe, client->recipient, + (unsigned long)((client->flags & ~(DSN_SUCCESS | DSN_TIMEOUT | + DSN_FAILURE | DSN_HEADER | DSN_BODY)) | NO_RULEPROCESSING)); + execution->written = TRUE; + break; + case BONGO_SIEVE_REJECT: + return SieveSendReject(execution, action_result); + case BONGO_SIEVE_VACATION: + return SieveSendVacation(execution, action_result); + } + return execution->result != -1; +} + +static int +ProcessSieve(RulesClient *client) +{ + char *script = NULL; + char *message = NULL; + size_t script_length = 0; + int active = 0; + long message_length; + SieveExecution execution = { client, NULL, 0, FALSE }; + if (!RulesAgent.SieveStore || + !BongoSieveStoreGet(RulesAgent.SieveStore, client->recipient, NULL, + &script, &script_length, &active) || !active) return 0; + if (NMAPSendCommandF(client->conn, "QRETR %s MESSAGE\r\n", client->qID) == -1 || + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, TRUE) != 2023) goto failed; + message_length = strtol(client->line, NULL, 10); + if (message_length <= 0 || message_length > 100 * 1024 * 1024) goto failed; + message = malloc((size_t)message_length + 1); + if (!message || ConnReadCount(client->conn, message, (int)message_length) != message_length || + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, TRUE) != NMAP_OK) goto failed; + message[message_length] = '\0'; + execution.message = message; + if (!BongoSieveEvaluate(script, script_length, message, (size_t)message_length, + SieveAction, &execution) || execution.result == -1) goto failed; + if (!execution.written) { + execution.result = ConnWriteF(client->conn, "QMOD RAW %s\r\n", + client->envelopeLine); + } + free(message); + free(script); + return execution.result == -1 ? -1 : 1; +failed: + Log(LOG_ERROR, "Sieve processing failed for user %s queue %s; keeping message", + client->recipient, client->qID); + free(message); + free(script); + return ConnWriteF(client->conn, "QMOD RAW %s\r\n", client->envelopeLine) == -1 ? -1 : 1; +} int GetMimeStructure(RulesClient *c) @@ -56,9 +418,9 @@ GetMimeStructure(RulesClient *c) } void -GetMimeSizes(unsigned char *p, unsigned long *s, unsigned long *l, unsigned long *h) +GetMimeSizes(char *p, unsigned long *s, unsigned long *l, unsigned long *h) { - unsigned char *eof = strrchr(p, '\"'); + char *eof = strrchr(p, '\"'); if (eof) { eof++; sscanf(eof, " %lu %lu %lu", s, l, h); @@ -137,20 +499,20 @@ CompareRuleID(const void *first, const void *second) { unsigned long oneID; unsigned long twoID; - unsigned char temp; - const unsigned char *one = *(unsigned char **)first; - const unsigned char *two = *(unsigned char **)second; + char temp; + char *const one = *(char *const *)first; + char *const two = *(char *const *)second; - if (one && two && (strlen((unsigned char *)one) > 9) && (strlen((unsigned char *)two) > 9)) { - temp = ((unsigned char *)one)[8]; - ((unsigned char *)one)[8] = '\0'; - oneID = strtol((unsigned char *)one, NULL, 16); - ((unsigned char *)one)[8] = temp; + if (one && two && (strlen(one) > 9) && (strlen(two) > 9)) { + temp = one[8]; + one[8] = '\0'; + oneID = strtoul(one, NULL, 16); + one[8] = temp; - temp = ((unsigned char *)two)[8]; - ((unsigned char *)two)[8] = '\0'; - twoID = strtol((unsigned char *)two, NULL, 16); - ((unsigned char *)two)[8] = temp; + temp = two[8]; + two[8] = '\0'; + twoID = strtoul(two, NULL, 16); + two[8] = temp; if (oneID < twoID) { return(-1); @@ -170,8 +532,7 @@ ProcessRules(RulesClient *client) unsigned long start; unsigned long size; unsigned long header; - unsigned char *ptr; - unsigned char *owner; + char *ptr; BOOL result, written=FALSE; BongoRuleAction *action; BongoRuleCondition *condition; @@ -274,6 +635,43 @@ ProcessRules(RulesClient *client) break; } + case RULE_COND_MAILINGLIST: { + static const struct { + const char *header; + const char *value; + } signals[] = { + { "List-Id", "<" }, + { "List-Post", "mailto:" }, + { "Precedence", "list" } + }; + unsigned int signal; + for (signal = 0; signal < sizeof(signals) / sizeof(signals[0]); signal++) { + ccode = NMAPSendCommandF(client->conn, + "QSRCH HEADER %s %s: \"%s\"\r\n", + client->qID, signals[signal].header, + signals[signal].value); + if (ccode != -1 && + (ccode = NMAPReadAnswer(client->conn, client->line, + CONN_BUFSIZE, TRUE)) == NMAP_OK) { + result = TRUE; + break; + } + } + break; + } + + case RULE_COND_LISTID: { + ccode = NMAPSendCommandF(client->conn, + "QSRCH HEADER %s List-Id: \"%s\"\r\n", + client->qID, condition->string[0]); + if (ccode != -1 && + (ccode = NMAPReadAnswer(client->conn, client->line, + CONN_BUFSIZE, TRUE)) == NMAP_OK) { + result = TRUE; + } + break; + } + case RULE_COND_BODY: case RULE_COND_BODY_NOT: { GetMimeStructure(client); @@ -455,7 +853,6 @@ ProcessRules(RulesClient *client) } case RULE_ACT_MOVE: { - owner = NULL; ptr = NULL; used = 0; @@ -471,16 +868,6 @@ ProcessRules(RulesClient *client) ccode = NMAPSendCommandF(client->conn, "QMOD MAILBOX %s %s %lu %s\r\n", client->recipient, client->recipient, (unsigned long)client->flags, action->string[0]); written = TRUE; - if (owner) { - MemFree(owner); - owner = NULL; - } - - if (ptr) { - MemFree(ptr); - ptr = NULL; - } - break; } @@ -608,9 +995,9 @@ ProcessRules(RulesClient *client) static void ParseRules(RulesClient *client) { - unsigned char *cur; - unsigned char *ptr; - unsigned char *limit; + char *cur; + char *ptr; + char *limit; unsigned long used; BOOL parsed = FALSE; BongoRuleAction *action = NULL; @@ -621,7 +1008,7 @@ ParseRules(RulesClient *client) g_array_sort(client->RulesStrings, CompareRuleID); for (used = 0; used < client->RulesStrings->len; used++) { - cur = g_array_index(client->RulesStrings, unsigned char *, used); + cur = g_array_index(client->RulesStrings, char *, used); ptr = cur; limit = cur + strlen(cur); @@ -670,7 +1057,8 @@ ParseRules(RulesClient *client) switch (*ptr) { case RULE_COND_ANY: case RULE_COND_FREE: - case RULE_COND_FREENOT: { + case RULE_COND_FREENOT: + case RULE_COND_MAILINGLIST: { /* no arguments */ if (memcmp(ptr + 1, "000Z000Z", 8) == 0) { condition->type = *ptr; @@ -697,7 +1085,8 @@ ParseRules(RulesClient *client) case RULE_COND_PRIORITY_NOT: case RULE_COND_BODY_NOT: case RULE_COND_HASMIMETYPE: - case RULE_COND_HASMIMETYPENOT: { + case RULE_COND_HASMIMETYPENOT: + case RULE_COND_LISTID: { /* sole argument is a search string */ condition->type = *ptr; condition->length[0] = ((ptr[1] - '0') * 100) + ((ptr[2] - '0') * 10) + (ptr[3] - '0'); @@ -991,7 +1380,8 @@ ParseRules(RulesClient *client) continue; } - Log(LOG_ERROR, "User %s rule %lu cannot be parsed! Offset %lu", client->recipient, used, cur); + Log(LOG_ERROR, "User %s rule %lu cannot be parsed! Offset %lu", + client->recipient, used, (unsigned long)(ptr - cur)); if (condition) { MemFree(condition); @@ -1099,7 +1489,7 @@ ProcessEntry(void *clientp, Connection *conn) envelopeLine = Queue->envelope; while (*envelopeLine) { - unsigned char *eol = envelopeLine + strlen(envelopeLine); + char *eol = envelopeLine + strlen(envelopeLine); switch(*envelopeLine) { case QUEUE_FROM : @@ -1136,6 +1526,10 @@ ProcessEntry(void *clientp, Connection *conn) Queue->recipient = envelopeLine; } + /* An active Sieve script replaces the legacy per-user rule list. */ + ccode = ProcessSieve(Queue); + if (ccode != 0) break; + /* i need to read out this user's rules from the server and parse them */ snprintf(Queue->line, CONN_BUFSIZE, "rules/%s", Queue->recipient); if (!ReadBongoConfiguration(Queue->UserConfig, Queue->line)) { @@ -1174,6 +1568,8 @@ RulesAgentServer(void *ignored) int maxThreads; int minSleep; + UNUSED_PARAMETER(ignored); + XplRenameThread(XplGetThreadID(), AGENT_DN " Server"); BongoQueueAgentGetThreadPoolParameters(&RulesAgent.agent, &minThreads, &maxThreads, &minSleep); @@ -1227,6 +1623,9 @@ XplServiceMain(int argc, char *argv[]) int startupOpts; ConnSSLConfiguration sslconfig; + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); + if (XplSetRealUser(MsgGetUnprivilegedUser()) < 0) { XplConsolePrintf(AGENT_NAME ": Could not drop to unprivileged user '%s'\r\n" AGENT_NAME ": exiting.\n", MsgGetUnprivilegedUser()); return -1; @@ -1250,10 +1649,21 @@ XplServiceMain(int argc, char *argv[]) sslconfig.key.type = GNUTLS_X509_FMT_PEM; RulesAgent.SSL_Context = ConnSSLContextAlloc(&sslconfig); + { + char sieveDatabase[XPL_MAX_PATH + 1]; + snprintf(sieveDatabase, sizeof(sieveDatabase), "%s/sieve.sqlite", + XPL_DEFAULT_DBF_DIR); + if (!BongoSieveStoreOpen(&RulesAgent.SieveStore, sieveDatabase)) { + XplConsolePrintf(AGENT_NAME ": Cannot open Sieve script store\r\n"); + return -1; + } + } + XplSignalHandler(SignalHandler); /* Start the server thread */ XplStartMainThread(AGENT_NAME, &id, RulesAgentServer, 8192, NULL, ccode); + BongoSieveStoreClose(&RulesAgent.SieveStore); XplUnloadApp(XplGetThreadID()); diff --git a/src/agents/rules/rules.h b/src/agents/rules/rules.h index b72b3e0..272b995 100644 --- a/src/agents/rules/rules.h +++ b/src/agents/rules/rules.h @@ -30,6 +30,7 @@ #include #include #include +#include #define AGENT_NAME "bongorules" @@ -39,7 +40,7 @@ typedef struct _BongoRuleCondition { unsigned char type; unsigned char operand; - unsigned char *string[2]; + char *string[2]; unsigned long length[2]; unsigned long count[2]; @@ -50,7 +51,7 @@ typedef struct _BongoRuleAction { struct _BongoRuleAction *next; unsigned char type; - unsigned char *string[2]; + char *string[2]; unsigned long length[2]; } BongoRuleAction; @@ -59,8 +60,8 @@ typedef struct _BongoRule { struct _BongoRule *prev; struct _BongoRule *next; - unsigned char *string; - unsigned char id[9]; + char *string; + char id[9]; BOOL active; @@ -78,9 +79,9 @@ typedef struct _BongoRule { typedef struct { Connection *conn; - unsigned int envelopeLength; - unsigned int messageLength; - unsigned int envelopeLines; + int envelopeLength; + int messageLength; + int envelopeLines; char *envelope; char *sender; /* pointer into the envelope of the from address */ char *authSender; /* pointer into the envelope of the envelope from */ @@ -115,6 +116,7 @@ typedef struct _RulesAgentGlobals { void *OutgoingPool; BongoThreadPool *OutgoingThreadPool; bongo_ssl_context *SSL_Context; + BongoSieveStore *SieveStore; } RulesAgentGlobals; extern RulesAgentGlobals RulesAgent; @@ -128,5 +130,5 @@ static void FreeBongoRule(BongoRule *rule); static int ProcessRules(RulesClient *client); int GetMimeStructure(RulesClient *c); static void RulesCleanupInformation(RulesClient *client); -void GetMimeSizes(unsigned char *p, unsigned long *s, unsigned long *l, unsigned long *h); +void GetMimeSizes(char *p, unsigned long *s, unsigned long *l, unsigned long *h); #endif diff --git a/src/agents/sieve/CMakeLists.txt b/src/agents/sieve/CMakeLists.txt new file mode 100644 index 0000000..8e1d30b --- /dev/null +++ b/src/agents/sieve/CMakeLists.txt @@ -0,0 +1,5 @@ +StrictCompile() +add_executable(bongosieve managesieved.c) +target_link_libraries(bongosieve libbongosieve bongosasl bongoxpl bongoconnio + bongoutil bongomsgapi bongostreamio) +install(TARGETS bongosieve DESTINATION ${SBIN_INSTALL_DIR}) diff --git a/src/agents/sieve/managesieved.c b/src/agents/sieve/managesieved.c new file mode 100644 index 0000000..219e207 --- /dev/null +++ b/src/agents/sieve/managesieved.c @@ -0,0 +1,326 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define AGENT_NAME "bongosieve" +#define LINE_SIZE 8192 + +static BongoAgent agent; +static Connection *listener; +static bongo_ssl_context *tls_context; +static BongoSieveStore *script_store; +static int server_port = 4190; +static int maximum_script_size = 1024 * 1024; +static int maximum_connections = 64; +static BOOL allow_legacy_tls = FALSE; +static char *listen_address; +static XplThreadID server_id; +static XplAtomic active_connections; + +static BongoConfigItem SieveConfig[] = { + { BONGO_JSON_STRING, "o:listen_address/s", &listen_address }, + { BONGO_JSON_INT, "o:port/i", &server_port }, + { BONGO_JSON_BOOL, "o:allow_legacy_tls/b", &allow_legacy_tls }, + { BONGO_JSON_INT, "o:maximum_script_size/i", &maximum_script_size }, + { BONGO_JSON_INT, "o:maximum_connections/i", &maximum_connections }, + { BONGO_JSON_NULL, NULL, NULL } +}; + +static int write_ok(Connection *connection, const char *message) +{ + return message ? ConnWriteF(connection, "OK \"%s\"\r\n", message) + : ConnWriteStr(connection, "OK\r\n"); +} + +static int write_no(Connection *connection, const char *code, const char *message) +{ + return ConnWriteF(connection, "NO (%s) \"%s\"\r\n", code, message); +} + +static int capabilities(Connection *connection, int tls, const char *mechanisms) +{ + if (ConnWriteF(connection, "\"IMPLEMENTATION\" \"Bongo %s\"\r\n", + BONGO_VERSION) == -1 || + ConnWriteStr(connection, + "\"SIEVE\" \"fileinto envelope body variables vacation vacation-seconds\"\r\n" + "\"MAXREDIRECTS\" \"4\"\r\n") == -1) return -1; + if (!tls && ConnWriteStr(connection, "\"STARTTLS\"\r\n") == -1) return -1; + if (tls && mechanisms && ConnWriteF(connection, "\"SASL\" \"%s\"\r\n", + mechanisms) == -1) return -1; + return ConnWriteStr(connection, "OK\r\n"); +} + +static int verify_password(void *context, const char *user, const char *password, + size_t password_length) +{ + char *copy; + int valid; + (void)context; + if (!user || !password || password_length > 4096) return 0; + copy = malloc(password_length + 1); + if (!copy) return 0; + memcpy(copy, password, password_length); + copy[password_length] = '\0'; + valid = MsgAuthFindUser(user) == 0 && MsgAuthVerifyPassword(user, copy) == 0; + memset(copy, 0, password_length); + free(copy); + return valid; +} + +static int read_literal(Connection *connection, size_t length, char **output) +{ + size_t used = 0; + int count; + char *buffer; + if (!length || length > (size_t)maximum_script_size) return 0; + buffer = malloc(length + 1); + if (!buffer) return 0; + while (used < length) { + count = ConnRead(connection, buffer + used, (int)(length - used)); + if (count <= 0) { free(buffer); return 0; } + used += (size_t)count; + } + buffer[length] = '\0'; + *output = buffer; + return 1; +} + +struct list_context { Connection *connection; int failed; }; +static int list_script(const char *name, int active, void *data) +{ + struct list_context *context = data; + if (ConnWriteF(context->connection, "\"%s\"%s\r\n", name, + active ? " ACTIVE" : "") == -1) { + context->failed = 1; + return 0; + } + return 1; +} + +static int authenticate(Connection *connection, BongoSaslSession *sasl, + BongoManageSieveRequest *request, char **username) +{ + unsigned char decoded[8192]; + char encoded[16384]; + size_t decoded_length = 0, encoded_length = 0, output_length = 0; + const void *output = NULL; + int result; + if (request->argument_count == 2 && + !BongoSaslDecode64(request->arguments[1], strlen(request->arguments[1]), + decoded, sizeof(decoded), &decoded_length)) return 0; + result = BongoSaslStart(sasl, request->arguments[0], + request->argument_count == 2 ? decoded : NULL, + decoded_length, &output, &output_length); + while (result == BONGO_SASL_CONTINUE) { + if (!BongoSaslEncode64(output, output_length, encoded, sizeof(encoded), + &encoded_length) || + ConnWriteF(connection, "\"%.*s\"\r\n", (int)encoded_length, encoded) == -1 || + ConnFlush(connection) == -1 || + ConnReadLine(connection, encoded, sizeof(encoded)) <= 0) return 0; + CHOP_NEWLINE(encoded); + if (!strcmp(encoded, "*")) return 0; + /* RFC 5804 SASL responses are strings, so clients commonly quote the + * Base64 value. Base64 itself cannot contain a quote or backslash. */ + if (encoded[0] == '\"') { + size_t length = strlen(encoded); + if (length < 2 || encoded[length - 1] != '\"') return 0; + memmove(encoded, encoded + 1, length - 2); + encoded[length - 2] = '\0'; + } + if (!BongoSaslDecode64(encoded, strlen(encoded), decoded, sizeof(decoded), + &decoded_length)) return 0; + result = BongoSaslStep(sasl, decoded, decoded_length, &output, &output_length); + } + memset(decoded, 0, sizeof(decoded)); + if (result != BONGO_SASL_COMPLETE || !BongoSaslUsername(sasl)) return 0; + *username = strdup(BongoSaslUsername(sasl)); + return *username != NULL; +} + +static void serve(Connection *connection) +{ + char line[LINE_SIZE + 1]; + char *user = NULL; + BongoSaslSession *sasl = NULL; + int tls = 0; + const char *mechanisms = NULL; + BongoManageSieveRequest request; + capabilities(connection, 0, NULL); + ConnFlush(connection); + while (ConnReadLine(connection, line, sizeof(line)) > 0) { + char *literal = NULL; + if (!BongoManageSieveParse(line, strlen(line), &request)) { + write_no(connection, "SYNTAX", "Invalid command"); + ConnFlush(connection); + continue; + } + if (request.literal_length) { + if (!request.literal_nonsynchronizing) { + ConnWriteStr(connection, "OK \"Ready for literal data\"\r\n"); + ConnFlush(connection); + } + if (!read_literal(connection, request.literal_length, &literal)) { + write_no(connection, "QUOTA/MAXSIZE", "Script is too large"); + ConnFlush(connection); + continue; + } + } + if (request.command == BONGO_MANAGESIEVE_CAPABILITY) { + capabilities(connection, tls, mechanisms); + } else if (request.command == BONGO_MANAGESIEVE_STARTTLS && !tls && !user) { + ConnWriteStr(connection, "OK \"Begin TLS negotiation\"\r\n"); + ConnFlush(connection); + if (!ConnNegotiate(connection, tls_context) || + !BongoSaslSessionCreate(&sasl, "sieve", NULL, NULL, NULL, 1, + verify_password, NULL) || + !BongoSaslMechanisms(sasl, &mechanisms)) break; + tls = 1; + } else if (request.command == BONGO_MANAGESIEVE_AUTHENTICATE && tls && !user) { + if (authenticate(connection, sasl, &request, &user)) write_ok(connection, NULL); + else write_no(connection, "AUTH-FAILED", "Authentication failed"); + } else if (request.command == BONGO_MANAGESIEVE_LOGOUT) { + write_ok(connection, "Logout complete"); + ConnFlush(connection); + free(literal); + break; + } else if (!user) { + write_no(connection, "AUTH-REQUIRED", "Authenticate first"); + } else if (request.command == BONGO_MANAGESIEVE_CHECKSCRIPT) { + if (BongoSieveValidate(literal, request.literal_length)) write_ok(connection, NULL); + else write_no(connection, "QUOTA", "Script validation failed"); + } else if (request.command == BONGO_MANAGESIEVE_HAVESPACE) { + write_ok(connection, NULL); + } else if (request.command == BONGO_MANAGESIEVE_PUTSCRIPT) { + if (BongoSieveStorePut(script_store, user, request.arguments[0], literal, + request.literal_length)) write_ok(connection, NULL); + else write_no(connection, "QUOTA", "Script could not be stored"); + } else if (request.command == BONGO_MANAGESIEVE_SETACTIVE) { + if (BongoSieveStoreSetActive(script_store, user, request.arguments[0])) write_ok(connection, NULL); + else write_no(connection, "NONEXISTENT", "Script does not exist"); + } else if (request.command == BONGO_MANAGESIEVE_DELETESCRIPT) { + if (BongoSieveStoreDelete(script_store, user, request.arguments[0])) write_ok(connection, NULL); + else write_no(connection, "ACTIVE", "Active or nonexistent script"); + } else if (request.command == BONGO_MANAGESIEVE_RENAMESCRIPT) { + if (BongoSieveStoreRename(script_store, user, request.arguments[0], + request.arguments[1])) write_ok(connection, NULL); + else write_no(connection, "ALREADYEXISTS", "Source missing or destination exists"); + } else if (request.command == BONGO_MANAGESIEVE_LISTSCRIPTS) { + struct list_context context = { connection, 0 }; + if (BongoSieveStoreList(script_store, user, list_script, &context) && !context.failed) + write_ok(connection, NULL); + } else if (request.command == BONGO_MANAGESIEVE_GETSCRIPT) { + char *script = NULL; size_t length = 0; + if (BongoSieveStoreGet(script_store, user, request.arguments[0], &script, &length, NULL)) { + ConnWriteF(connection, "{%lu}\r\n", (unsigned long)length); + ConnWrite(connection, script, (int)length); + ConnWriteStr(connection, "\r\nOK\r\n"); + free(script); + } else write_no(connection, "NONEXISTENT", "Script does not exist"); + } else if (request.command == BONGO_MANAGESIEVE_NOOP) { + write_ok(connection, NULL); + } else { + write_no(connection, "UNSUPPORTED", "Command not supported"); + } + free(literal); + if (ConnFlush(connection) == -1) break; + } + free(user); + BongoSaslSessionDestroy(&sasl); +} + +static void +client_loop(void *data) +{ + Connection *connection = data; + serve(connection); + ConnClose(connection); + ConnFree(connection); + XplSafeDecrement(active_connections); +} + +static void server_loop(void *unused) +{ + (void)unused; + while (agent.state < BONGO_AGENT_STATE_STOPPING) { + Connection *connection = NULL; + XplThreadID client_id; + int result; + if (ConnAccept(listener, &connection) == -1) continue; + if (XplSafeRead(active_connections) >= maximum_connections) { + ConnWriteStr(connection, "BYE (TRYLATER) \"Too many connections\"\r\n"); + ConnFlush(connection); + ConnClose(connection); + ConnFree(connection); + continue; + } + XplBeginCountedThread(&client_id, client_loop, 32768, connection, + result, active_connections); + if (result != 0) { + ConnClose(connection); + ConnFree(connection); + } + } +} + +static void signal_handler(int signal) { + BongoAgentHandleSignal(&agent, signal); + if (listener) ConnClose(listener); +} +XplServiceCode(signal_handler) + +int XplServiceMain(int argc, char **argv) +{ + ConnSSLConfiguration tls = {0}; + char database[XPL_MAX_PATH + 1]; + int result; + int wait; + (void)argc; (void)argv; (void)GlobalConfig; + if (XplSetRealUser(MsgGetUnprivilegedUser()) < 0) { + XplConsolePrintf(AGENT_NAME ": Could not drop privileges\r\n"); + return 1; + } + XplInit(); + result = BongoAgentInit(&agent, AGENT_NAME, DEFAULT_CONNECTION_TIMEOUT, + BA_STARTUP_CONNIO | BA_STARTUP_NMAP); + if (result == -1 || !ReadBongoConfiguration(SieveConfig, "sieve") || + server_port < 1 || server_port > 65535 || maximum_script_size < 1 || + maximum_script_size > 1024 * 1024 || maximum_connections < 1 || + maximum_connections > 4096 || !BongoSaslInitialize()) return 1; + snprintf(database, sizeof(database), "%s/sieve.sqlite", XPL_DEFAULT_DBF_DIR); + if (!BongoSieveStoreOpen(&script_store, database)) return 1; + tls.certificate.file = XPL_DEFAULT_CERT_PATH; + tls.certificate.type = GNUTLS_X509_FMT_PEM; + tls.key.file = XPL_DEFAULT_KEY_PATH; + tls.key.type = GNUTLS_X509_FMT_PEM; + if (allow_legacy_tls) tls.options |= SSL_ALLOW_LEGACY_PROTOCOLS; + tls_context = ConnSSLContextAlloc(&tls); + listener = ConnAlloc(TRUE); + if (!tls_context || !listener || !listen_address || + inet_aton(listen_address, &listener->socketAddress.sin_addr) == 0) return 1; + listener->socketAddress.sin_family = AF_INET; + listener->socketAddress.sin_port = htons((unsigned short)server_port); + if (ConnServerSocket(listener, 64) == -1) return 1; + XplSignalHandler(signal_handler); + /* Some XPL backends expand this call to a macro which ignores the ID. */ + (void)server_id; + XplStartMainThread(AGENT_NAME, &server_id, server_loop, 32768, NULL, result); + for (wait = 0; wait < 300 && XplSafeRead(active_connections) > 0; wait++) { + XplDelay(100); + } + if (XplSafeRead(active_connections) > 0) { + Log(LOG_WARNING, "%d ManageSieve connections remain during shutdown", + XplSafeRead(active_connections)); + return 1; + } + BongoSieveStoreClose(&script_store); + ConnSSLContextFree(tls_context); + BongoSaslShutdown(); + return 0; +} diff --git a/src/agents/smtp/CMakeLists.txt b/src/agents/smtp/CMakeLists.txt index 54bb18c..305b97e 100644 --- a/src/agents/smtp/CMakeLists.txt +++ b/src/agents/smtp/CMakeLists.txt @@ -10,6 +10,7 @@ target_link_libraries(bongosmtp bongoutil bongojson bongomsgapi + bongomailauth ) add_executable(bongosmtpc @@ -22,6 +23,7 @@ target_link_libraries(bongosmtpc bongoutil bongojson bongomsgapi + bongomailauth ) install(TARGETS bongosmtp DESTINATION ${SBIN_INSTALL_DIR}) diff --git a/src/agents/smtp/smtpc.c b/src/agents/smtp/smtpc.c index 525960d..2ec8dc2 100755 --- a/src/agents/smtp/smtpc.c +++ b/src/agents/smtp/smtpc.c @@ -32,10 +32,174 @@ #include #include #include +#include "mailauth.h" #include "smtpc.h" -SMTPAgentGlobals SMTPAgent = {{0,}, }; +static struct { + BOOL dkim_sign_outgoing; + char *dkim_key_directory; + char *dkim_selector; + char *dkim_signing_domain; + BOOL srs_forward; + char *srs_domain; + char *srs_secret_file; + GArray *lmtp_transports; +} MailAuth; + +static BongoConfigItem LMTPTransportList = { + BONGO_JSON_STRING, NULL, &MailAuth.lmtp_transports +}; + +static BongoConfigItem MailAuthConfig[] = { + { BONGO_JSON_BOOL, "o:dkim_sign_outgoing/b", &MailAuth.dkim_sign_outgoing }, + { BONGO_JSON_STRING, "o:dkim_key_directory/s", &MailAuth.dkim_key_directory }, + { BONGO_JSON_STRING, "o:dkim_selector/s", &MailAuth.dkim_selector }, + { BONGO_JSON_STRING, "o:dkim_signing_domain/s", &MailAuth.dkim_signing_domain }, + { BONGO_JSON_BOOL, "o:srs_forward/b", &MailAuth.srs_forward }, + { BONGO_JSON_STRING, "o:srs_domain/s", &MailAuth.srs_domain }, + { BONGO_JSON_STRING, "o:srs_secret_file/s", &MailAuth.srs_secret_file }, + { BONGO_JSON_ARRAY, "o:lmtp_transports/a", &LMTPTransportList }, + { BONGO_JSON_NULL, NULL, NULL } +}; + +SMTPAgentGlobals SMTPAgent = {0}; + +static BongoMailAuthStatus +GetEnvelopeSender(const SMTPClient *queue, char *rewritten, size_t rewritten_size, + const char **sender) +{ + BOOL forwarded; + + if (queue == NULL || sender == NULL) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + *sender = queue->sender; + forwarded = (queue->flags & MSG_FLAG_SOURCE_EXTERNAL) != 0 && + queue->authSender != NULL && queue->authSender[0] == '-' && + queue->sender != NULL && queue->sender[0] != '\0' && + strcmp(queue->sender, "-") != 0; + if (!MailAuth.srs_forward || !forwarded) { + return BONGO_MAILAUTH_OK; + } + if (MailAuth.srs_domain == NULL || MailAuth.srs_domain[0] == '\0' || + MailAuth.srs_secret_file == NULL || MailAuth.srs_secret_file[0] == '\0') { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + if (BongoMailAuthSRSForward(MailAuth.srs_secret_file, + MailAuth.srs_domain, queue->sender, + rewritten, rewritten_size) != BONGO_MAILAUTH_OK) { + return BONGO_MAILAUTH_PROTOCOL_ERROR; + } + *sender = rewritten; + return BONGO_MAILAUTH_OK; +} + +static BOOL +IsDKIMName(const char *value) +{ + const unsigned char *cursor = (const unsigned char *) value; + + if (cursor == NULL || *cursor == '\0') { + return FALSE; + } + while (*cursor != '\0') { + if (!isalnum(*cursor) && *cursor != '.' && *cursor != '-') { + return FALSE; + } + cursor++; + } + return TRUE; +} + +static BongoMailAuthStatus +GenerateDKIMSignature(SMTPClient *queue) +{ + const char *domain; + const char *at; + char keyPath[XPL_MAX_PATH]; + char *messageLine = NULL; + unsigned long messageLineSize = 0; + long long remaining; + BongoDKIMSigner *signer = NULL; + BongoMailAuthStatus status; + + queue->hasDKIMSignature = FALSE; + if (!MailAuth.dkim_sign_outgoing) { + return BONGO_MAILAUTH_OK; + } + /* Do not add a new author signature to unauthenticated forwarded mail. */ + if ((queue->flags & MSG_FLAG_SOURCE_EXTERNAL) != 0 && + (queue->authSender == NULL || queue->authSender[0] == '-')) { + return BONGO_MAILAUTH_OK; + } + domain = MailAuth.dkim_signing_domain; + if (domain == NULL || domain[0] == '\0') { + at = queue->sender != NULL ? strrchr(queue->sender, '@') : NULL; + domain = at != NULL ? at + 1 : NULL; + } + if (!IsDKIMName(domain) || !IsDKIMName(MailAuth.dkim_selector) || + MailAuth.dkim_key_directory == NULL || + MailAuth.dkim_key_directory[0] == '\0') { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + if (snprintf(keyPath, sizeof(keyPath), "%s/%s/%s.private", + MailAuth.dkim_key_directory, domain, + MailAuth.dkim_selector) >= (int) sizeof(keyPath)) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + status = BongoMailAuthDKIMSignerCreate( + keyPath, MailAuth.dkim_selector, domain, queue->qID, &signer); + if (status != BONGO_MAILAUTH_OK) { + return status; + } + + ConnWriteF(queue->conn, "QRETR %s MESSAGE\r\n", queue->qID); + ConnFlush(queue->conn); + ConnReadAnswer(queue->conn, queue->line, CONN_BUFSIZE); + if (atoi(queue->line) != 2023) { + status = BONGO_MAILAUTH_IO_ERROR; + goto cleanup; + } + remaining = atoll(&queue->line[5]); + while (remaining > 0) { + size_t lineLength; + size_t consumed; + + ConnReadToAllocatedBuffer(queue->conn, &messageLine, + &messageLineSize); + if (messageLine == NULL) { + status = BONGO_MAILAUTH_IO_ERROR; + goto cleanup; + } + lineLength = strlen(messageLine); + status = BongoMailAuthDKIMSignerUpdate(signer, messageLine, lineLength); + if (status == BONGO_MAILAUTH_OK) { + status = BongoMailAuthDKIMSignerUpdate(signer, "\r\n", 2U); + } + if (status != BONGO_MAILAUTH_OK) { + goto cleanup; + } + consumed = lineLength + 2U; + remaining -= remaining > (long long) consumed + ? (long long) consumed : remaining; + } + ConnReadAnswer(queue->conn, queue->line, CONN_BUFSIZE); + if (atoi(queue->line) != 1000) { + status = BONGO_MAILAUTH_IO_ERROR; + goto cleanup; + } + status = BongoMailAuthDKIMSignerFinish( + signer, queue->dkimSignature, sizeof(queue->dkimSignature)); + queue->hasDKIMSignature = status == BONGO_MAILAUTH_OK; + +cleanup: + if (messageLine != NULL) { + MemFree(messageLine); + } + BongoMailAuthDKIMSignerFree(signer); + return status; +} static void SMTPClientFree(void *clientp) @@ -70,7 +234,7 @@ int RecipientCompare(const void *lft, const void *rgt) { } Connection *LookupRemoteMX(RecipStruct *Recipient) { - unsigned char Host[MAXEMAILNAMESIZE+1]; + char Host[MAXEMAILNAMESIZE+1]; XplDns_MxLookup *mx = NULL; XplDns_IpList *list = NULL; Connection *Result=NULL; @@ -79,7 +243,7 @@ Connection *LookupRemoteMX(RecipStruct *Recipient) { *(Recipient->localPart-1) = '\0'; if (Recipient->SortField[0] == '[') { // potential ip address in the host name (e.g. jdoe@[1.1.1.1]) - unsigned char *end; + char *end; end = strchr(Recipient->SortField, ']'); if (end) { *end = '\0'; @@ -181,14 +345,113 @@ finish: return Result; } +static BOOL +FindLMTPTransport(const RecipStruct *recipient, char *host, size_t hostSize, + int *port) +{ + const unsigned char *at; + unsigned int index; + + if (MailAuth.lmtp_transports == NULL || host == NULL || hostSize == 0U || + port == NULL) { + return FALSE; + } + at = (const unsigned char *) strrchr((const char *) recipient->To, '@'); + if (at == NULL) { + return FALSE; + } + for (index = 0U; index < MailAuth.lmtp_transports->len; index++) { + const char *entry = g_array_index(MailAuth.lmtp_transports, char *, index); + const char *equals = entry != NULL ? strchr(entry, '=') : NULL; + const char *colon = equals != NULL ? strrchr(equals + 1, ':') : NULL; + size_t domainLength; + size_t hostLength; + char *end = NULL; + long parsedPort; + + if (equals == NULL || colon == NULL || equals == entry || + colon == equals + 1 || colon[1] == '\0') { + continue; + } + domainLength = (size_t) (equals - entry); + if (strlen((const char *) at + 1) != domainLength || + strncasecmp((const char *) at + 1, entry, domainLength) != 0) { + continue; + } + hostLength = (size_t) (colon - (equals + 1)); + if (hostLength == 0U || hostLength >= hostSize) { + continue; + } + parsedPort = strtol(colon + 1, &end, 10); + if (end == colon + 1 || *end != '\0' || parsedPort <= 0L || + parsedPort > 65535L) { + continue; + } + memcpy(host, equals + 1, hostLength); + host[hostLength] = '\0'; + *port = (int) parsedPort; + return TRUE; + } + return FALSE; +} + +static Connection * +ConnectLMTP(RecipStruct *recipient, const char *host, int targetPort) +{ + struct addrinfo hints; + struct addrinfo *addresses = NULL; + struct addrinfo *address; + char port[16]; + Connection *connection = NULL; + int error; + + if (host == NULL || host[0] == '\0' || targetPort <= 0 || + targetPort > 65535) { + recipient->Result = DELIVER_BOGUS_NAME; + return NULL; + } + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + snprintf(port, sizeof(port), "%d", targetPort); + error = getaddrinfo(host, port, &hints, &addresses); + if (error != 0) { + recipient->Result = error == EAI_AGAIN + ? DELIVER_TIMEOUT : DELIVER_HOST_UNKNOWN; + return NULL; + } + for (address = addresses; address != NULL; address = address->ai_next) { + const struct sockaddr_in *socketAddress = + (const struct sockaddr_in *) address->ai_addr; + + connection = ConnAlloc(TRUE); + if (connection == NULL) { + recipient->Result = DELIVER_TRY_LATER; + break; + } + connection->socketAddress = *socketAddress; + if (ConnConnect(connection, NULL, 0, NULL) > 0) { + break; + } + ConnFree(connection); + connection = NULL; + recipient->Result = errno == ETIMEDOUT + ? DELIVER_TIMEOUT : DELIVER_TRY_LATER; + } + freeaddrinfo(addresses); + return connection; +} + BOOL DeliverMessage(SMTPClient *Queue, SMTPClient *Remote, RecipStruct *Recip) { int Extensions; unsigned long Size=0, MessageLineLen=0; long long MessageLength = 0; int Ret; - unsigned char TimeBuf[80]; + char TimeBuf[80]; char *MessageLine=NULL; + char rewrittenSender[1024]; + const char *envelopeSender = NULL; /* we are connected to the remote smtp server, but we haven't done any conversation yet */ /* read out the banner */ @@ -210,11 +473,12 @@ DeliverMessage(SMTPClient *Queue, SMTPClient *Remote, RecipStruct *Recip) { beginConversation: Extensions = 0; - ConnWriteF(Remote->conn, "EHLO %s\r\n", BongoGlobals.hostname); + ConnWriteF(Remote->conn, "%s %s\r\n", + Remote->isLMTP ? "LHLO" : "EHLO", BongoGlobals.hostname); ConnFlush(Remote->conn); ConnReadAnswer(Remote->conn, Remote->line, CONN_BUFSIZE); - if (Remote->line[0] != '2' ) { + if (Remote->line[0] != '2' && !Remote->isLMTP) { /* the remote server doesn't seem to support ehlo. they should upgrade to bongo! */ ConnWriteF(Remote->conn, "HELO %s\r\n", BongoGlobals.hostname); ConnFlush(Remote->conn); @@ -234,7 +498,7 @@ beginConversation: } else if (strcmp(&Remote->line[4], "STARTTLS") == 0) { Extensions |= EXT_TLS; } else if (strcmp(&Remote->line[4], "SIZE") == 0) { - unsigned char *ptr; + char *ptr; ptr = strchr(&Remote->line[4], ' '); if (ptr) { Extensions |= EXT_SIZE; @@ -263,7 +527,8 @@ beginConversation: } /* begin the delivery process */ - if ((Extensions & EXT_SIZE) && (Queue->messageLength > Size)) { + if ((Extensions & EXT_SIZE) && + ((unsigned long)Queue->messageLength > Size)) { /* the message is too big */ ConnWrite(Remote->conn, "QUIT\r\n", 6); ConnFlush(Remote->conn); @@ -271,8 +536,17 @@ beginConversation: goto finalization; } - /* mail from */ - Ret = ConnWriteF(Remote->conn, "MAIL FROM: <%s>", Queue->sender); + /* Apply SRS only to unauthenticated externally sourced messages which + * Bongo is forwarding. Local submissions and bounces are not rewritten. */ + if (GetEnvelopeSender(Queue, rewrittenSender, sizeof(rewrittenSender), + &envelopeSender) != BONGO_MAILAUTH_OK) { + Log(LOG_ERROR, + "SRS forwarding failed for queue %s; delivery deferred", + Queue->qID); + Recip->Result = DELIVER_TRY_LATER; + goto finalization; + } + Ret = ConnWriteF(Remote->conn, "MAIL FROM: <%s>", envelopeSender); if (Extensions & EXT_SIZE) { Ret += ConnWriteF(Remote->conn, " SIZE=%u", Queue->messageLength); } @@ -313,15 +587,15 @@ beginConversation: ConnWriteF(Remote->conn, "RCPT TO: <%s>", Recip->To); if (Extensions & EXT_DSN) { - if (Recip->ORecip) { + if (Recip->ORecip[0] != '\0') { /* apparently some emails can come through with rfc822; on them?? */ if (strncasecmp(Recip->ORecip, "rfc822;", 7) == 0) { /* we are already set up and can just send the value through. possibly a relay? */ ConnWriteF(Remote->conn, " ORCPT=%s", Recip->ORecip); } else { /* we need to translate the ORecip to xtext as defined in rfc 1891 */ - unsigned char *ptr = Recip->ORecip; - unsigned char Hex[] = "012345678ABCDEF"; + const char *ptr = Recip->ORecip; + static const char Hex[] = "0123456789ABCDEF"; ConnWriteStr(Remote->conn, " ORCPT=rfc822;"); while (*ptr) { @@ -413,6 +687,10 @@ beginConversation: Queue->conn->socketAddress.sin_addr.s_impno, BongoGlobals.hostname, TimeBuf); + if (Queue->hasDKIMSignature) { + ConnWriteF(Remote->conn, "DKIM-Signature: %s\r\n", + Queue->dkimSignature); + } /* stream the message over */ ConnWriteF(Queue->conn, "QRETR %s MESSAGE\r\n", Queue->qID); @@ -517,7 +795,7 @@ ProcessEntry(void *clientp, Connection *conn) * i need to write out all lines as i go */ envelopeLine = Queue->envelope; while (*envelopeLine) { - unsigned char *eol = envelopeLine + strlen(envelopeLine); + char *eol = envelopeLine + strlen(envelopeLine); switch(*envelopeLine) { case QUEUE_FROM : @@ -542,7 +820,7 @@ ProcessEntry(void *clientp, Connection *conn) envelopeLine++; memset(&CurrentRecip, 0, sizeof(RecipStruct)); /* add the recipient into array of recipients */ - unsigned char *orecip, *flags, *ptr; + char *orecip, *flags, *ptr; CurrentRecip.Flags = 0; CurrentRecip.Result = 0; @@ -598,13 +876,23 @@ ProcessEntry(void *clientp, Connection *conn) } /* now that we've got all recipients and locations, let's sort them to use connections more efficiently */ + { + BongoMailAuthStatus dkimStatus = GenerateDKIMSignature(Queue); + if (dkimStatus != BONGO_MAILAUTH_OK) { + Log(LOG_ERROR, + "DKIM signing preparation failed for queue %s (status %d)", + Queue->qID, (int) dkimStatus); + g_array_free(Recipients, TRUE); + return -1; + } + } g_array_sort(Recipients, (ArrayCompareFunc)RecipientCompare); /* now i can skip over any duplicates an do lookups once per remote domain */ for(startLocation=0;startLocationlen;startLocation++) { SMTPClient Remote; RecipStruct NextRecip; - unsigned char *lft; + char *lft; CurrentRecip = g_array_index(Recipients, RecipStruct, startLocation); lft = CurrentRecip.SortField; @@ -622,7 +910,13 @@ ProcessEntry(void *clientp, Connection *conn) /* by the time i get here, CurrentRecip should be the last recip * of any string of duplicates */ - Remote.conn = LookupRemoteMX(&CurrentRecip); + Remote.isLMTP = FindLMTPTransport( + &CurrentRecip, Remote.transportHost, + sizeof(Remote.transportHost), &Remote.transportPort); + Remote.conn = Remote.isLMTP + ? ConnectLMTP(&CurrentRecip, Remote.transportHost, + Remote.transportPort) + : LookupRemoteMX(&CurrentRecip); if (!Remote.conn) { /* there was an error looking up or connecting to the remote server * if i get an error in this stage of the process i need to just skip @@ -682,6 +976,8 @@ SMTPAgentServer(void *ignored) int maxThreads; int minSleep; + UNUSED_PARAMETER(ignored); + XplRenameThread(XplGetThreadID(), AGENT_DN " Server"); BongoQueueAgentGetThreadPoolParameters(&SMTPAgent.agent, &minThreads, &maxThreads, &minSleep); @@ -716,6 +1012,10 @@ ReadConfiguration(void) return FALSE; } + if (! ReadBongoConfiguration(MailAuthConfig, "smtp")) { + return FALSE; + } + return TRUE; } @@ -734,6 +1034,9 @@ XplServiceMain(int argc, char *argv[]) int startupOpts; ConnSSLConfiguration sslconfig; + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); + if (XplSetRealUser(MsgGetUnprivilegedUser()) < 0) { XplConsolePrintf(AGENT_NAME ": Could not drop to unprivileged user '%s'\r\n" AGENT_NAME ": exiting.\n", MsgGetUnprivilegedUser()); return -1; diff --git a/src/agents/smtp/smtpc.h b/src/agents/smtp/smtpc.h index 5093a83..9c047c9 100644 --- a/src/agents/smtp/smtpc.h +++ b/src/agents/smtp/smtpc.h @@ -37,15 +37,20 @@ typedef struct { Connection *conn; - unsigned int envelopeLength; - unsigned int messageLength; - unsigned int envelopeLines; + int envelopeLength; + int messageLength; + int envelopeLines; char *envelope; char *sender; /* pointer into the envelope of the from address */ char *authSender; /* pointer into the envelope of the envelope from */ int flags; char line[CONN_BUFSIZE + 1]; char qID[16]; /* holds the queueid pulled during handshake */ + BOOL hasDKIMSignature; + char dkimSignature[8192]; + BOOL isLMTP; + char transportHost[256]; + int transportPort; } SMTPClient; typedef struct _SMTPAgentGlobals { @@ -62,10 +67,10 @@ typedef struct _SMTPAgentGlobals { } SMTPAgentGlobals; typedef struct { - unsigned char To[MAXEMAILNAMESIZE+1]; - unsigned char ORecip[MAXEMAILNAMESIZE+1]; - unsigned char SortField[MAXEMAILNAMESIZE+2]; /* will be formatted domain.com@emailaddr -- used for comparisons and sorting */ - unsigned char *localPart; /* pointer into the SortField for the beginning of the localPart */ + char To[MAXEMAILNAMESIZE+1]; + char ORecip[MAXEMAILNAMESIZE+1]; + char SortField[MAXEMAILNAMESIZE+2]; /* will be formatted domain.com@emailaddr -- used for comparisons and sorting */ + char *localPart; /* pointer into the SortField for the beginning of the localPart */ size_t ToLen; /* just so we don't have to calculate the length a bunch of times */ unsigned long Flags; int Result; diff --git a/src/agents/smtp/smtpd.c b/src/agents/smtp/smtpd.c index 8f41b57..d2370a8 100644 --- a/src/agents/smtp/smtpd.c +++ b/src/agents/smtp/smtpd.c @@ -43,12 +43,31 @@ #include #include +#include "mailauth.h" #include "smtpd.h" struct { int port; int port_ssl; + BOOL submission_enabled; + int submission_port; + int submission_port_ssl; + BOOL submission_require_auth; + BOOL submission_allow_sender_override; + BOOL internal_relay_enabled; + int internal_relay_port; + char *internal_relay_bind_address; + GArray *internal_relay_networks; + BOOL internal_relay_allow_legacy_tls; + int internal_relay_messages_per_minute; + int internal_relay_recipients_per_minute; + int internal_relay_bytes_per_minute; + int internal_relay_connections_per_ip; + char *internal_relay_domain; + GArray *internal_relay_device_mappings; + GArray *internal_relay_dns_suffixes; BOOL allow_client_ssl; + BOOL allow_legacy_tls; BOOL allow_expn; BOOL allow_auth; BOOL require_auth; @@ -67,12 +86,52 @@ struct { int max_null_sender; int max_mx_servers; int socket_timeout; + BOOL spf_verify; + BOOL dkim_verify; + BOOL dmarc_verify; + BOOL dmarc_enforce; + BOOL srs_reverse; + char *srs_domain; + char *srs_secret_file; + GArray *lmtp_transports; } SMTP; +static BongoConfigItem LMTPTransportList = { + BONGO_JSON_STRING, NULL, &SMTP.lmtp_transports +}; + +static BongoConfigItem InternalRelayNetworkList = { + BONGO_JSON_STRING, NULL, &SMTP.internal_relay_networks +}; +static BongoConfigItem InternalRelayDeviceMappingList = { + BONGO_JSON_STRING, NULL, &SMTP.internal_relay_device_mappings +}; +static BongoConfigItem InternalRelayDNSSuffixList = { + BONGO_JSON_STRING, NULL, &SMTP.internal_relay_dns_suffixes +}; + static BongoConfigItem SMTPConfig[] = { { BONGO_JSON_INT, "o:port/i", &SMTP.port }, { BONGO_JSON_INT, "o:port_ssl/i", &SMTP.port_ssl }, + { BONGO_JSON_BOOL, "o:submission_enabled/b", &SMTP.submission_enabled }, + { BONGO_JSON_INT, "o:submission_port/i", &SMTP.submission_port }, + { BONGO_JSON_INT, "o:submission_port_ssl/i", &SMTP.submission_port_ssl }, + { BONGO_JSON_BOOL, "o:submission_require_auth/b", &SMTP.submission_require_auth }, + { BONGO_JSON_BOOL, "o:submission_allow_sender_override/b", &SMTP.submission_allow_sender_override }, + { BONGO_JSON_BOOL, "o:internal_relay_enabled/b", &SMTP.internal_relay_enabled }, + { BONGO_JSON_INT, "o:internal_relay_port/i", &SMTP.internal_relay_port }, + { BONGO_JSON_STRING, "o:internal_relay_bind_address/s", &SMTP.internal_relay_bind_address }, + { BONGO_JSON_ARRAY, "o:internal_relay_networks/a", &InternalRelayNetworkList }, + { BONGO_JSON_BOOL, "o:internal_relay_allow_legacy_tls/b", &SMTP.internal_relay_allow_legacy_tls }, + { BONGO_JSON_INT, "o:internal_relay_messages_per_minute/i", &SMTP.internal_relay_messages_per_minute }, + { BONGO_JSON_INT, "o:internal_relay_recipients_per_minute/i", &SMTP.internal_relay_recipients_per_minute }, + { BONGO_JSON_INT, "o:internal_relay_bytes_per_minute/i", &SMTP.internal_relay_bytes_per_minute }, + { BONGO_JSON_INT, "o:internal_relay_connections_per_ip/i", &SMTP.internal_relay_connections_per_ip }, + { BONGO_JSON_STRING, "o:internal_relay_domain/s", &SMTP.internal_relay_domain }, + { BONGO_JSON_ARRAY, "o:internal_relay_device_mappings/a", &InternalRelayDeviceMappingList }, + { BONGO_JSON_ARRAY, "o:internal_relay_dns_suffixes/a", &InternalRelayDNSSuffixList }, { BONGO_JSON_BOOL, "o:allow_client_ssl/b", &SMTP.allow_client_ssl }, + { BONGO_JSON_BOOL, "o:allow_legacy_tls/b", &SMTP.allow_legacy_tls }, { BONGO_JSON_BOOL, "o:allow_expn/b", &SMTP.allow_expn }, { BONGO_JSON_BOOL, "o:allow_vrfy/b", &SMTP.allow_vrfy }, { BONGO_JSON_BOOL, "o:allow_auth/b", &SMTP.allow_auth }, @@ -91,6 +150,14 @@ static BongoConfigItem SMTPConfig[] = { { BONGO_JSON_INT, "o:max_mx_servers/i", &SMTP.max_mx_servers }, { BONGO_JSON_BOOL, "o:relay_local_mail/b", &SMTP.relay_local }, { BONGO_JSON_BOOL, "o:require_auth/b", &SMTP.require_auth }, + { BONGO_JSON_BOOL, "o:spf_verify/b", &SMTP.spf_verify }, + { BONGO_JSON_BOOL, "o:dkim_verify/b", &SMTP.dkim_verify }, + { BONGO_JSON_BOOL, "o:dmarc_verify/b", &SMTP.dmarc_verify }, + { BONGO_JSON_BOOL, "o:dmarc_enforce/b", &SMTP.dmarc_enforce }, + { BONGO_JSON_BOOL, "o:srs_reverse/b", &SMTP.srs_reverse }, + { BONGO_JSON_STRING, "o:srs_domain/s", &SMTP.srs_domain }, + { BONGO_JSON_STRING, "o:srs_secret_file/s", &SMTP.srs_secret_file }, + { BONGO_JSON_ARRAY, "o:lmtp_transports/a", &LMTPTransportList }, { BONGO_JSON_NULL, NULL, NULL } }; @@ -104,6 +171,8 @@ XplAtomic SMTPIdleConnThreads; XplAtomic SMTPQueueThreads; Connection *SMTPServerConnection; Connection *SMTPServerConnectionSSL; +Connection *SMTPInternalServerConnection; +Connection *SMTPSubmissionServerConnection; Connection *SMTPQServerConnection; Connection *SMTPQServerConnectionSSL; int TGid; @@ -122,6 +191,17 @@ unsigned long BounceCount; char NMAPServer[20] = "127.0.0.1"; unsigned long LocalAddress = 0; bongo_ssl_context *SSLContext = NULL; +static bongo_ssl_context *InternalSSLContext = NULL; +static GHashTable *InternalRelayRates; +static XplMutex InternalRelayRateLock; + +typedef struct { + time_t window; + unsigned int messages; + unsigned int recipients; + unsigned long bytes; + unsigned int connections; +} InternalRelayRate; static char NMAPHash[NMAP_HASH_SIZE]; #ifdef USE_HOPCOUNT_DETECTION @@ -174,8 +254,311 @@ typedef struct char *AuthFrom; /* Sender Authenticated As */ char User[64]; /* Fixme - Make this bigger */ BOOL IsEHLO; /* used for RFC 3848 */ + BOOL HasSPFResult; + BongoSPFResponse SPFResult; + BOOL InternalRelay; + BOOL Submission; + InternalRelayRate *InternalRate; + char InternalDevice[64]; } ConnectionStruct; +static BOOL +InternalRelayConsume(ConnectionStruct *client, unsigned int messages, + unsigned int recipients, unsigned long bytes) +{ + InternalRelayRate *rate = client->InternalRate; + time_t now = time(NULL); + BOOL allowed = TRUE; + + if (!client->InternalRelay || rate == NULL) return TRUE; + XplMutexLock(InternalRelayRateLock); + if (now < rate->window || now - rate->window >= 60) { + rate->window = now; + rate->messages = rate->recipients = 0; + rate->bytes = 0; + } + if ((messages && rate->messages + messages > (unsigned int) SMTP.internal_relay_messages_per_minute) || + (recipients && rate->recipients + recipients > (unsigned int) SMTP.internal_relay_recipients_per_minute) || + (bytes && rate->bytes + bytes > (unsigned long) SMTP.internal_relay_bytes_per_minute)) { + allowed = FALSE; + } else { + rate->messages += messages; + rate->recipients += recipients; + rate->bytes += bytes; + } + XplMutexUnlock(InternalRelayRateLock); + return allowed; +} + +static BOOL +NormalizeInternalSender(const char *sender, const char *device, char *normalized, + size_t normalizedSize) +{ + const char *at; + const char *domain; + size_t localLength; + size_t domainLength; + + if (sender == NULL || normalized == NULL || normalizedSize == 0 || + SMTP.internal_relay_domain == NULL || *SMTP.internal_relay_domain == '\0') return FALSE; + at = strrchr(sender, '@'); + if (at == NULL || at == sender || at[1] == '\0') return FALSE; + domain = at + 1; + localLength = (size_t) (at - sender); + domainLength = strlen(domain); + if (strcasecmp(domain, "localhost") == 0 || + strcasecmp(domain, "localdomain") == 0 || + strcasecmp(domain, "localhost.localdomain") == 0) { + if (device != NULL && *device != '\0' && + (localLength == 4 && strncasecmp(sender, "root", 4) == 0)) { + return snprintf(normalized, normalizedSize, "%.*s+%s@%s", (int) localLength, + sender, device, SMTP.internal_relay_domain) < (int) normalizedSize; + } + return snprintf(normalized, normalizedSize, "%.*s@%s", (int) localLength, + sender, SMTP.internal_relay_domain) < (int) normalizedSize; + } + if (domainLength > strlen(SMTP.internal_relay_domain) + 1 && + domain[domainLength - strlen(SMTP.internal_relay_domain) - 1] == '.' && + strcasecmp(domain + domainLength - strlen(SMTP.internal_relay_domain), + SMTP.internal_relay_domain) == 0) { + size_t subdomainLength = domainLength - strlen(SMTP.internal_relay_domain) - 1; + return snprintf(normalized, normalizedSize, "%.*s+%.*s@%s", (int) localLength, + sender, (int) subdomainLength, domain, + SMTP.internal_relay_domain) < (int) normalizedSize; + } + return FALSE; +} + +static BOOL +AddressInNetwork(struct in_addr address, const char *network) +{ + char text[INET_ADDRSTRLEN + 4]; + char *slash; + char *end; + struct in_addr base; + unsigned long prefix = 32; + uint32_t mask; + + if (network == NULL || strlen(network) >= sizeof(text)) { + return FALSE; + } + strcpy(text, network); + slash = strchr(text, '/'); + if (slash != NULL) { + *slash++ = '\0'; + errno = 0; + prefix = strtoul(slash, &end, 10); + if (errno != 0 || *slash == '\0' || *end != '\0' || prefix > 32) { + return FALSE; + } + } + if (inet_pton(AF_INET, text, &base) != 1) { + return FALSE; + } + mask = prefix == 0 ? 0 : UINT32_MAX << (32 - prefix); + return (ntohl(address.s_addr) & mask) == (ntohl(base.s_addr) & mask); +} + +static BOOL +InternalRelayAddressAllowed(struct in_addr address) +{ + unsigned int index; + + for (index = 0; SMTP.internal_relay_networks != NULL && + index < SMTP.internal_relay_networks->len; index++) { + const char *network = g_array_index(SMTP.internal_relay_networks, char *, index); + if (AddressInNetwork(address, network)) { + return TRUE; + } + } + return FALSE; +} + +static BOOL +ValidDeviceName(const char *name) +{ + const unsigned char *p = (const unsigned char *) name; + if (p == NULL || *p == '\0') return FALSE; + while (*p != '\0') { + if (!isalnum(*p) && *p != '-') return FALSE; + p++; + } + return TRUE; +} + +static BOOL +HostHasAllowedSuffix(const char *host) +{ + unsigned int index; + size_t hostLength = strlen(host); + for (index = 0; SMTP.internal_relay_dns_suffixes != NULL && + index < SMTP.internal_relay_dns_suffixes->len; index++) { + const char *suffix = g_array_index(SMTP.internal_relay_dns_suffixes, char *, index); + size_t suffixLength = suffix != NULL ? strlen(suffix) : 0; + if (suffixLength && hostLength > suffixLength && + host[hostLength - suffixLength - 1] == '.' && + strcasecmp(host + hostLength - suffixLength, suffix) == 0) return TRUE; + } + return FALSE; +} + +static BOOL +IdentifyInternalDevice(struct in_addr address, char *device, size_t deviceSize) +{ + unsigned int index; + char ip[INET_ADDRSTRLEN]; + char host[NI_MAXHOST]; + struct sockaddr_in peer; + struct addrinfo hints; + struct addrinfo *answers = NULL; + struct addrinfo *answer; + BOOL forwardMatch = FALSE; + + if (inet_ntop(AF_INET, &address, ip, sizeof(ip)) == NULL) return FALSE; + for (index = 0; SMTP.internal_relay_device_mappings != NULL && + index < SMTP.internal_relay_device_mappings->len; index++) { + const char *mapping = g_array_index(SMTP.internal_relay_device_mappings, char *, index); + const char *equals = mapping != NULL ? strchr(mapping, '=') : NULL; + if (equals != NULL && (size_t) (equals - mapping) == strlen(ip) && + strncmp(mapping, ip, strlen(ip)) == 0 && ValidDeviceName(equals + 1)) { + snprintf(device, deviceSize, "%s", equals + 1); + return TRUE; + } + } + memset(&peer, 0, sizeof(peer)); + peer.sin_family = AF_INET; + peer.sin_addr = address; + if (getnameinfo((struct sockaddr *) &peer, sizeof(peer), host, sizeof(host), + NULL, 0, NI_NAMEREQD) != 0 || !HostHasAllowedSuffix(host)) return FALSE; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + if (getaddrinfo(host, NULL, &hints, &answers) != 0) return FALSE; + for (answer = answers; answer != NULL; answer = answer->ai_next) { + struct sockaddr_in *resolved = (struct sockaddr_in *) answer->ai_addr; + if (resolved->sin_addr.s_addr == address.s_addr) { + forwardMatch = TRUE; + break; + } + } + freeaddrinfo(answers); + if (forwardMatch) { + char *dot = strchr(host, '.'); + if (dot != NULL) *dot = '\0'; + if (ValidDeviceName(host)) { + snprintf(device, deviceSize, "%s", host); + return TRUE; + } + } + return FALSE; +} + +static const char * +SPFResultName(BongoSPFResult result) +{ + static const char *const names[] = { + "none", "neutral", "pass", "fail", "softfail", "none", + "temperror", "permerror" + }; + + return result >= BONGO_SPF_INVALID && result <= BONGO_SPF_PERMERROR + ? names[result] : "none"; +} + +static BOOL +IsTransportRecipient(const char *address) +{ + const char *at; + unsigned int index; + + if (address == NULL || SMTP.lmtp_transports == NULL || + (at = strrchr(address, '@')) == NULL || at[1] == '\0') { + return FALSE; + } + for (index = 0U; index < SMTP.lmtp_transports->len; index++) { + const char *entry = g_array_index(SMTP.lmtp_transports, char *, index); + const char *equals = entry != NULL ? strchr(entry, '=') : NULL; + size_t domainLength; + + if (equals == NULL || equals == entry) { + continue; + } + domainLength = (size_t) (equals - entry); + if (strlen(at + 1) == domainLength && + strncasecmp(at + 1, entry, domainLength) == 0) { + return TRUE; + } + } + return FALSE; +} + +static BOOL +AuthenticateUser(ConnectionStruct *client, const char *user, const char *password) +{ + if (user == NULL || password == NULL || *user == '\0' || + MsgAuthFindUser(user) != 0 || MsgAuthVerifyPassword(user, password) != 0) { + Log(LOG_NOTICE, "Authentication failed for user %s at host %s", + user != NULL ? user : "", LOGIP(client->client.conn->socketAddress)); + return FALSE; + } + + Log(LOG_NOTICE, "Successful login for user %s at host %s", user, + LOGIP(client->client.conn->socketAddress)); + MemFree(client->AuthFrom); + client->AuthFrom = MemStrdup(user); + return client->AuthFrom != NULL; +} + +static BOOL +SubmissionSenderAllowed(const char *user, const char *sender) +{ + const char *at; + size_t localLength; + if (SMTP.submission_allow_sender_override || user == NULL || sender == NULL || + *sender == '\0') return TRUE; + if (strcasecmp(user, sender) == 0) return TRUE; + if (strchr(user, '@') != NULL) return FALSE; + at = strrchr(sender, '@'); + localLength = at != NULL ? (size_t) (at - sender) : strlen(sender); + return strlen(user) == localLength && strncasecmp(user, sender, localLength) == 0; +} + +static BOOL +ParsePlainCredentials(char *encoded, const char **user, const char **password) +{ + size_t encodedLength; + size_t decodedLimit; + char *decoded; + char *authcid; + char *separator; + + if (encoded == NULL || user == NULL || password == NULL) { + return FALSE; + } + encodedLength = strlen(encoded); + if (encodedLength == 0 || encodedLength > BUFSIZE) { + return FALSE; + } + decodedLimit = ((encodedLength + 3U) / 4U) * 3U; + decoded = DecodeBase64(encoded); + if (decoded == NULL) { + return FALSE; + } + + separator = memchr(decoded, '\0', decodedLimit); + if (separator == NULL) { + return FALSE; + } + authcid = separator + 1; + separator = memchr(authcid, '\0', decodedLimit - (size_t)(authcid - decoded)); + if (separator == NULL || authcid == separator) { + return FALSE; + } + *user = authcid; + *password = separator + 1; + return (size_t)(*password - decoded) < decodedLimit; +} + typedef struct { char To[MAXEMAILNAMESIZE+1]; @@ -200,7 +583,7 @@ void *SMTPConnectionPool = NULL; /* Prototypes */ long ReadConnection(Connection *conn, char **buffer, unsigned long *buflen); void ProcessRemoteEntry (ConnectionStruct * Client, unsigned long Size, int Lines); -int RewriteAddress (Connection * conn, char *Source, char *Target, unsigned int TargetSize); +int RewriteAddress (Connection * conn, const char *Source, char *Target, unsigned int TargetSize); BOOL FlushClient (ConnectionStruct * Client); BOOL EndClientConnection (ConnectionStruct * Client); static int PullLine (char *Line, unsigned long LineSize, char **NextLine); @@ -333,6 +716,12 @@ EndClientConnection (ConnectionStruct * Client) MemFree (Client->AuthFrom); Client->AuthFrom = NULL; } + if (Client->InternalRate != NULL) { + XplMutexLock(InternalRelayRateLock); + if (Client->InternalRate->connections > 0) Client->InternalRate->connections--; + XplMutexUnlock(InternalRelayRateLock); + Client->InternalRate = NULL; + } /* Bump our thread count */ if (Client->Flags < STATE_WAITING) { @@ -352,7 +741,7 @@ HandleConnection (void *param) { ConnectionStruct *Client = (ConnectionStruct *) param; int count; - int ReplyInt; + int ReplyInt = 0; BOOL Ready; BOOL Working = TRUE; BOOL IsTrusted = TRUE; @@ -369,9 +758,18 @@ HandleConnection (void *param) time (&connectionTime); + if (Client->InternalRelay && + IdentifyInternalDevice(Client->client.conn->socketAddress.sin_addr, + Client->InternalDevice, + sizeof(Client->InternalDevice))) { + Log(LOG_INFO, "Identified internal SMTP device %s at %s", + Client->InternalDevice, LOGIP(Client->client.conn->socketAddress)); + } + if (Client->client.conn->ssl.enable) { Log(LOG_INFO, "SSL connection from %s", LOGIP(Client->client.conn->socketAddress)); - if (!ConnNegotiate(Client->client.conn, SSLContext)) { + if (!ConnNegotiate(Client->client.conn, + Client->InternalRelay ? InternalSSLContext : SSLContext)) { return (EndClientConnection (Client)); } } else { @@ -379,7 +777,7 @@ HandleConnection (void *param) } XplRWReadLockAcquire (&ConfigLock); - if (! SMTP.allow_auth) { + if (!SMTP.allow_auth || Client->InternalRelay) { AllowAuth = FALSE; } @@ -397,8 +795,8 @@ HandleConnection (void *param) Log(LOG_ERROR, "Unable to authenticate to Queue agent at %s", LOGIP(Client->client.conn->socketAddress)); } else { - Log(LOG_ERROR, "Unable to connect to Queue agent at %s (reply was %d)", - LOGIP(Client->client.conn->socketAddress), ReplyInt); + Log(LOG_ERROR, "Unable to connect to Queue agent at %s", + LOGIP(Client->client.conn->socketAddress)); } count = snprintf(Reply, sizeof(Reply), "421 %s %s\r\n", BongoGlobals.hostname, MSG421SHUTDOWN); @@ -447,8 +845,8 @@ HandleConnection (void *param) if (Client->State == STATE_FRESH) { snprintf (Answer, sizeof (Answer), "250 %s %s\r\n", BongoGlobals.hostname, MSG250HELLO); ConnWrite (Client->client.conn, Answer, strlen (Answer)); - strncpy (Client->RemoteHost, Client->Command + 5, - sizeof (Client->RemoteHost)); + snprintf(Client->RemoteHost, sizeof(Client->RemoteHost), + "%s", Client->Command + 5); Client->State = STATE_HELO; NullSender = FALSE; @@ -472,7 +870,8 @@ HandleConnection (void *param) case 'S':{ /* SAML, SOML, SEND */ switch (toupper (Client->Command[1])) { case 'T':{ /* STARTTLS */ - if (!SMTP.allow_client_ssl) { + if ((!Client->InternalRelay && !Client->Submission && !SMTP.allow_client_ssl) || + (Client->InternalRelay && InternalSSLContext == NULL)) { ConnWrite (Client->client.conn, MSG500UNKNOWN, MSG500UNKNOWN_LEN); break; } @@ -483,7 +882,8 @@ HandleConnection (void *param) /* try to negotiate the connection */ Client->client.conn->ssl.enable = TRUE; - if (ConnNegotiate(Client->client.conn, SSLContext)) { + if (ConnNegotiate(Client->client.conn, + Client->InternalRelay ? InternalSSLContext : SSLContext)) { /* negotiation worked */ Client->State = STATE_FRESH; } @@ -507,74 +907,83 @@ HandleConnection (void *param) } case 'A':{ /* AUTH */ - char *PW; + const char *user = NULL; + const char *password = NULL; + BOOL authenticated = FALSE; - if (AllowAuth == FALSE) { + if (AllowAuth == FALSE || !Client->client.conn->ssl.enable) { ConnWrite (Client->client.conn, MSG500UNKNOWN, MSG500UNKNOWN_LEN); break; } - /* rfc 2554 */ + /* RFC 4954 */ if (IsAuthed) { ConnWrite (Client->client.conn, MSG503BADORDER, MSG503BADORDER_LEN); break; - } - - if (XplStrNCaseCmp (Client->Command + 5, "LOGIN", 5) != 0) { - ConnWrite (Client->client.conn, MSG504BADAUTH, MSG504BADAUTH_LEN); - break; } - if (!isspace (Client->Command[10])) { + + if (XplStrNCaseCmp(Client->Command + 5, "PLAIN", 5) == 0) { + char *encoded = Client->Command + 10; + + while (isspace((unsigned char)*encoded)) { + encoded++; + } + if (*encoded == '\0') { + ConnWrite(Client->client.conn, "334 \r\n", 6); + ConnFlush(Client->client.conn); + if (ConnReadToAllocatedBuffer(Client->client.conn, + &Client->client.buffer, &Client->client.buflen) <= 0) { + return EndClientConnection(Client); + } + encoded = Client->client.buffer; + } + if (strcmp(encoded, "*") == 0) { + ConnWrite(Client->client.conn, + "501 5.7.0 Authentication canceled\r\n", 35); + break; + } + if (ParsePlainCredentials(encoded, &user, &password)) { + authenticated = AuthenticateUser(Client, user, password); + } + } else if (XplStrNCaseCmp(Client->Command + 5, "LOGIN", 5) == 0) { + if (!isspace((unsigned char)Client->Command[10])) { ConnWrite (Client->client.conn, "334 VXNlcm5hbWU6\r\n", 18); ConnFlush (Client->client.conn); - ConnReadToAllocatedBuffer(Client->client.conn, &(Client->client.buffer), &(Client->client.buflen)); - strcpy (Reply, Client->client.buffer); - } - else { - strcpy (Reply, Client->client.buffer + 11); - } - - /* it might be possible to abort the auth here (rfc 2554). - * this is a code duplication from below */ - if (Client->client.buffer[0] == '*' && Client->client.buffer[1] == '\0') { - ConnWrite (Client->client.conn, "501 Authentication aborted by client\r\n", 38); - break; - } - - ConnWrite (Client->client.conn, "334 UGFzc3dvcmQ6\r\n", 18); - ConnFlush (Client->client.conn); - - ConnReadToAllocatedBuffer(Client->client.conn, &(Client->client.buffer), &(Client->client.buflen)); - - if (Client->client.buffer[0] == '*' && Client->client.buffer[1] == '\0') { - ConnWrite (Client->client.conn, "501 Authentication aborted by client\r\n", 38); - break; - } - - PW = DecodeBase64 (Client->client.buffer); - DecodeBase64 (Reply); - - if (MsgAuthFindUser(Reply) == 0) { - if (MsgAuthVerifyPassword(Reply, PW) != 0) { - ConnWrite (Client->client.conn, "501 Authentication failed!\r\n", 28); - Log(LOG_NOTICE, "Wrong password from user %s at host %s", - Reply, - LOGIP(Client->client.conn->socketAddress)); + if (ConnReadToAllocatedBuffer(Client->client.conn, + &Client->client.buffer, &Client->client.buflen) <= 0) { + return EndClientConnection(Client); + } + snprintf(Reply, sizeof(Reply), "%s", Client->client.buffer); } else { - Log(LOG_NOTICE, "Successful login for user %s at host %s", - Reply, - LOGIP(Client->client.conn->socketAddress)); - if (Client->AuthFrom != NULL) MemFree (Client->AuthFrom); - Client->AuthFrom = MemStrdup (Reply); - Client->State = STATE_AUTH; - ConnWrite (Client->client.conn, "235 Authentication successful!\r\n", 32); - IsAuthed = TRUE; - IsTrusted = TRUE; + snprintf(Reply, sizeof(Reply), "%s", Client->Command + 11); + } + if (strcmp(Reply, "*") != 0) { + user = DecodeBase64(Reply); + ConnWrite(Client->client.conn, "334 UGFzc3dvcmQ6\r\n", 18); + ConnFlush(Client->client.conn); + if (ConnReadToAllocatedBuffer(Client->client.conn, + &Client->client.buffer, &Client->client.buflen) <= 0) { + return EndClientConnection(Client); + } + if (strcmp(Client->client.buffer, "*") != 0) { + password = DecodeBase64(Client->client.buffer); + authenticated = AuthenticateUser(Client, user, password); + } } } else { - Log(LOG_NOTICE, "Unknown user %s at host %s", Reply, - LOGIP(Client->client.conn->socketAddress)); - ConnWrite (Client->client.conn, "501 Authentication failed!\r\n", 28); + ConnWrite(Client->client.conn, MSG504BADAUTH, MSG504BADAUTH_LEN); + break; + } + + if (authenticated) { + Client->State = STATE_AUTH; + IsAuthed = TRUE; + IsTrusted = TRUE; + ConnWrite(Client->client.conn, + "235 2.7.0 Authentication successful\r\n", 37); + } else { + ConnWrite(Client->client.conn, + "535 5.7.8 Authentication credentials invalid\r\n", 46); } break; } @@ -603,7 +1012,9 @@ HandleConnection (void *param) char temp, *name; BOOL GotFlags = FALSE; char To[MAXEMAILNAMESIZE + 1] = ""; + char SRSAddress[MAXEMAILNAMESIZE + 1] = ""; char *Orcpt = NULL; + const char *RoutingName; if (Client->State < STATE_FROM) { ConnWrite (Client->client.conn, MSG501NOSENDER, @@ -754,10 +1165,60 @@ HandleConnection (void *param) break; } - if ((ReplyInt = RewriteAddress (Client->nmap.conn, name, To, sizeof (To))) == MAIL_BOGUS) { + RoutingName = name; + if (ptr != NULL && strncasecmp(name, "SRS", 3) == 0) { + BOOL reverseSRS; + char srsDomain[MAXEMAILNAMESIZE + 1]; + char secretFile[XPL_MAX_PATH]; + + XplRWReadLockAcquire(&ConfigLock); + reverseSRS = SMTP.srs_reverse; + snprintf(srsDomain, sizeof(srsDomain), "%s", + SMTP.srs_domain != NULL ? SMTP.srs_domain : ""); + snprintf(secretFile, sizeof(secretFile), "%s", + SMTP.srs_secret_file != NULL + ? SMTP.srs_secret_file : ""); + XplRWReadLockRelease(&ConfigLock); + + if (reverseSRS && srsDomain[0] != '\0' && + strcasecmp(ptr + 1, srsDomain) == 0) { + BongoMailAuthStatus srsStatus = + BongoMailAuthSRSReverse(secretFile, name, + SRSAddress, + sizeof(SRSAddress)); + if (srsStatus == BONGO_MAILAUTH_PROTOCOL_ERROR) { + Log(LOG_NOTICE, + "Rejected invalid SRS recipient %s from host %s", + name, + LOGIP(Client->client.conn->socketAddress)); + ConnWrite(Client->client.conn, MSG550SRSINVALID, + MSG550SRSINVALID_LEN); + goto QuitRcpt; + } + if (srsStatus != BONGO_MAILAUTH_OK) { + Log(LOG_ERROR, + "Unable to validate SRS recipient %s (status %d)", + name, (int) srsStatus); + ConnWrite(Client->client.conn, + MSG451INTERNALERR, + MSG451INTERNALERR_LEN); + goto QuitRcpt; + } + RoutingName = SRSAddress; + } + } + + if ((ReplyInt = RewriteAddress (Client->nmap.conn, + RoutingName, To, + sizeof (To))) == MAIL_BOGUS) { ConnWrite (Client->client.conn, MSG501RECIPNO, MSG501RECIPNO_LEN); } else { + if (ReplyInt == MAIL_REMOTE && + IsTransportRecipient(RoutingName)) { + ReplyInt = MAIL_RELAY; + snprintf(To, sizeof(To), "%s", RoutingName); + } switch (ReplyInt) { case MAIL_REMOTE:{ if (!IsAuthed) { @@ -769,7 +1230,7 @@ HandleConnection (void *param) goto QuitRcpt; } XplRWReadLockAcquire (&ConfigLock); - if (Client->RecipCount >= SMTP.max_recips) { + if (Client->RecipCount >= (unsigned int)SMTP.max_recips) { XplRWReadLockRelease (&ConfigLock); Log(LOG_INFO, "Recipient limit from %s at host %s to user %s", Client->AuthFrom? @@ -812,7 +1273,8 @@ HandleConnection (void *param) case MAIL_LOCAL:{ XplRWReadLockAcquire (&ConfigLock); - if ((NullSender == FALSE) || (Client->RecipCount < SMTP.max_null_sender)) { + if ((NullSender == FALSE) || + (Client->RecipCount < (unsigned int)SMTP.max_null_sender)) { if (!SMTP.verify_address) { XplRWReadLockRelease (&ConfigLock); snprintf (Answer, sizeof (Answer), @@ -851,6 +1313,12 @@ HandleConnection (void *param) break; } } + if (!InternalRelayConsume(Client, 0, 1, 0)) { + ConnWrite(Client->client.conn, + "451 4.7.1 Internal relay recipient rate exceeded\r\n", + strlen("451 4.7.1 Internal relay recipient rate exceeded\r\n")); + goto QuitRcpt; + } NMAPSendCommand (Client->nmap.conn, Answer, strlen (Answer)); if (NMAPReadResponse (Client->nmap.conn, Reply, sizeof (Reply), TRUE) != 1000) { ConnWrite (Client->client.conn, MSG451INTERNALERR, @@ -880,11 +1348,18 @@ HandleConnection (void *param) char temp, *name; char *Envid = NULL; char *more; + char normalizedSender[MAXEMAILNAMESIZE + 1]; if (SMTP.require_auth && !IsTrusted) { ConnWrite (Client->client.conn, MSG553SPAMBLOCK, MSG553SPAMBLOCK_LEN); break; } + if (Client->Submission && SMTP.submission_require_auth && !IsAuthed) { + ConnWrite(Client->client.conn, + "530 5.7.0 Authentication required\r\n", + strlen("530 5.7.0 Authentication required\r\n")); + break; + } /* rfc 2821 */ if (!Client->State || Client->State >= STATE_FROM) { @@ -1088,7 +1563,8 @@ HandleConnection (void *param) ptr++; size = atol (ptr); XplRWReadLockAcquire (&ConfigLock); - if (SMTP.message_limit > 0 && size > SMTP.message_limit) { + if (SMTP.message_limit > 0 && + size > (unsigned long)SMTP.message_limit) { XplRWReadLockRelease (&ConfigLock); ConnWrite (Client->client.conn, MSG552MSGTOOBIG, MSG552MSGTOOBIG_LEN); @@ -1109,6 +1585,23 @@ HandleConnection (void *param) more++; } + if (Client->InternalRelay && + NormalizeInternalSender(name, Client->InternalDevice, normalizedSender, + sizeof(normalizedSender))) { + Log(LOG_INFO, "Normalized internal sender %s to %s for %s", + name, normalizedSender, LOGIP(Client->client.conn->socketAddress)); + name = normalizedSender; + } + if (Client->Submission && + !SubmissionSenderAllowed(Client->AuthFrom, name)) { + Log(LOG_WARN, "Submission user %s attempted sender %s from %s", + Client->AuthFrom != NULL ? Client->AuthFrom : "", name, + LOGIP(Client->client.conn->socketAddress)); + ConnWrite(Client->client.conn, + "553 5.7.1 Sender address not permitted\r\n", + strlen("553 5.7.1 Sender address not permitted\r\n")); + goto QuitMail; + } ReplyInt = strlen (name); if (ReplyInt > MAXEMAILNAMESIZE) { ConnWrite (Client->client.conn, MSG501GOAWAY, MSG501GOAWAY_LEN); @@ -1131,6 +1624,30 @@ HandleConnection (void *param) break; } + Client->HasSPFResult = FALSE; + XplRWReadLockAcquire(&ConfigLock); + { + BOOL verifySPF = SMTP.spf_verify; + XplRWReadLockRelease(&ConfigLock); + if (verifySPF) { + char clientAddress[INET_ADDRSTRLEN]; + BongoMailAuthStatus spfStatus; + + if (inet_ntop(AF_INET, + &Client->client.conn->socketAddress.sin_addr, + clientAddress, sizeof(clientAddress)) != NULL) { + spfStatus = BongoMailAuthCheckSPF( + BongoGlobals.hostname, clientAddress, + Client->RemoteHost, + name[0] != '\0' ? name : "", + &Client->SPFResult); + Client->HasSPFResult = + spfStatus == BONGO_MAILAUTH_OK || + spfStatus == BONGO_MAILAUTH_PROTOCOL_ERROR; + } + } + } + if (size > 0) { NMAPSendCommand (Client->nmap.conn, "QDSPC\r\n", 7); @@ -1260,15 +1777,38 @@ HandleConnection (void *param) TimeBuf); NMAPSendCommand(Client->nmap.conn, Answer, strlen(Answer)); + if (Client->HasSPFResult) { + snprintf(Answer, sizeof(Answer), + "Authentication-Results: %s; spf=%s smtp.mailfrom=%s\r\n", + BongoGlobals.hostname, + SPFResultName(Client->SPFResult.result), + (Client->From != NULL && Client->From[0] != '\0') + ? Client->From : "<>"); + NMAPSendCommand(Client->nmap.conn, Answer, strlen(Answer)); + } + /* read all the data to the nmap connection. this function * takes into account the single . terminating the connection */ - if (ConnReadToConnUntilEOS(Client->client.conn, Client->nmap.conn) < 0) { + count = ConnReadToConnUntilEOS(Client->client.conn, Client->nmap.conn); + if (count < 0) { ConnWrite(Client->client.conn, MSG451INTERNALERR, MSG451INTERNALERR_LEN); return(EndClientConnection(Client)); } + BReceived = (unsigned long) count; + if (!InternalRelayConsume(Client, 1, 0, BReceived)) { + NMAPSendCommand(Client->nmap.conn, "QABRT\r\n", 7); + NMAPReadResponse(Client->nmap.conn, Reply, sizeof(Reply), TRUE); + ConnWrite(Client->client.conn, + "451 4.7.1 Internal relay message rate exceeded\r\n", + strlen("451 4.7.1 Internal relay message rate exceeded\r\n")); + Client->State = STATE_HELO; + Client->RecipCount = 0; + break; + } XplRWReadLockAcquire (&ConfigLock); - if (SMTP.message_limit > 0 && BReceived > SMTP.message_limit) { /* Message over limit */ + if (SMTP.message_limit > 0 && + BReceived > (unsigned long)SMTP.message_limit) { /* Message over limit */ XplRWReadLockRelease (&ConfigLock); if ((NMAPSendCommand (Client->nmap.conn, "QABRT\r\n", 7) < 0) || NMAPReadResponse (Client->nmap.conn, Reply, sizeof (Reply), TRUE) != 1000) { @@ -1282,7 +1822,7 @@ HandleConnection (void *param) XplRWReadLockRelease (&ConfigLock); if ((NullSender == FALSE) - || (Client->RecipCount < SMTP.max_null_sender)) { + || (Client->RecipCount < (unsigned int)SMTP.max_null_sender)) { if (NMAPSendCommand (Client->nmap.conn, "QRUN\r\n", 6)<0) { ConnWrite (Client->client.conn, MSG451INTERNALERR, MSG451INTERNALERR_LEN); @@ -1351,13 +1891,14 @@ HandleConnection (void *param) XplRWReadLockAcquire (&ConfigLock); if (SMTP.message_limit > 0) { snprintf (Answer, sizeof (Answer), - "250-%s %s\r\n%s%s%s%s %lu\r\n", + "250-%s %s\r\n%s%s%s%s %d\r\n", BongoGlobals.hostname, MSG250HELLO, SMTP.accept_etrn ? MSG250ETRN : "", - (SMTP.allow_client_ssl + ((SMTP.allow_client_ssl || Client->InternalRelay || Client->Submission) + && (Client->InternalRelay ? InternalSSLContext != NULL : TRUE) && !Client->client.conn->ssl.enable) ? MSG250TLS : "", - (AllowAuth == - TRUE) ? MSG250AUTH : "", + (AllowAuth == TRUE && + Client->client.conn->ssl.enable) ? MSG250AUTH : "", MSG250EHLO, SMTP.message_limit); } else { @@ -1365,16 +1906,17 @@ HandleConnection (void *param) "250-%s %s\r\n%s%s%s%s\r\n", BongoGlobals.hostname, MSG250HELLO, SMTP.accept_etrn ? MSG250ETRN : "", - (SMTP.allow_client_ssl + ((SMTP.allow_client_ssl || Client->InternalRelay || Client->Submission) + && (Client->InternalRelay ? InternalSSLContext != NULL : TRUE) && !Client->client.conn->ssl.enable) ? MSG250TLS : "", - (AllowAuth == - TRUE) ? MSG250AUTH : "", + (AllowAuth == TRUE && + Client->client.conn->ssl.enable) ? MSG250AUTH : "", MSG250EHLO); } XplRWReadLockRelease (&ConfigLock); ConnWrite (Client->client.conn, Answer, strlen (Answer)); - strncpy (Client->RemoteHost, Client->Command + 5, - sizeof (Client->RemoteHost)); + snprintf(Client->RemoteHost, sizeof(Client->RemoteHost), + "%s", Client->Command + 5); Client->State = STATE_HELO; NullSender = FALSE; @@ -1662,13 +2204,11 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, char Reply[1024]; char *ptr, *EnvID = NULL; int status; - long Size, len, MessageSize; + long Size, len; unsigned int i; BOOL EscapedState = FALSE; unsigned long LastNMAPContact; char TimeBuf[80]; - BOOL ehlo=TRUE; - BOOL tls=FALSE; status = GetAnswer (Client, Reply, sizeof (Reply)); HandleFailure (220); @@ -1688,7 +2228,6 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, ConnFlush(Client->remotesmtp.conn); status = GetAnswer (Client, Reply, sizeof (Reply)); HandleFailure (250); - ehlo=FALSE; } else { /* The other server supports ESMTP */ @@ -1711,7 +2250,6 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, } ConnFlush(Client->remotesmtp.conn); status = GetAnswer (Client, Reply, sizeof (Reply)); - tls=TRUE; } } @@ -1755,8 +2293,7 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, } /* Keep this close to the above code; we rely on Reply not being overwritten */ - snprintf (Answer, sizeof (Answer), "MAIL FROM:<%s>", Reply); - if (ConnWrite(Client->remotesmtp.conn, Answer, strlen(Answer)) < 1) { + if (ConnWriteF(Client->remotesmtp.conn, "MAIL FROM:<%s>", Reply) < 1) { DELIVER_ERROR (DELIVER_TRY_LATER); } @@ -1834,7 +2371,7 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, } if (Extensions & EXT_DSN) { - if (Recips[i].ORecip) { + if (Recips[i].ORecip[0] != '\0') { if (XplStrNCaseCmp (Recips[i].ORecip, "rfc822;", 7) == 0) { /* Add "rfc822;" if not already present */ snprintf (Answer, sizeof (Answer), " ORCPT=%s", Recips[i].ORecip); @@ -1951,7 +2488,7 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, if (status == 550) { Recips[i].Result = DELIVER_USER_UNKNOWN; if ((unsigned long) ResultLen > strlen (Reply)) { - snprintf (Result, sizeof (Result), + snprintf (Result, ResultLen, ">>> RCPT TO: <%s>\\r\\n<<< %d %s", Recips[i].To, status, Reply); } @@ -1960,7 +2497,7 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, } continue; } - else if ((status == 501) && Recips[i].ORecip + else if ((status == 501) && Recips[i].ORecip[0] != '\0' && (Extensions & EXT_DSN)) { /* workaround for smtp implimentations that do not do ORCPT: properly */ Extensions ^= EXT_DSN; @@ -2005,7 +2542,7 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, if (status == 550) { Recips[i].Result = DELIVER_USER_UNKNOWN; if ((unsigned long) ResultLen > strlen (Reply)) { /* FIXME? This is not enough to prevent overwrites, but the buffer is really big... */ - snprintf (Result, sizeof (Result), + snprintf (Result, ResultLen, ">>> RCPT TO: <%s>\\r\\n<<< %d %s", Recips[i].To, status, Reply); } @@ -2057,8 +2594,6 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, DELIVER_ERROR (DELIVER_FAILURE); } - MessageSize = Size; - /* Sending message to remote system; must escape any dots on a line */ while (Size > 0) { if ((unsigned int)Size < sizeof (Answer)) { @@ -2135,7 +2670,7 @@ DeliverRemoteMessage (ConnectionStruct * Client, char *Sender, conn->socketAddress.sin_addr.s_addr = inet_addr(SMTP.relay_host); conn->socketAddress.sin_family = AF_INET; conn->socketAddress.sin_port = htons (25); // TODO: configurable? - if (conn->socketAddress.sin_addr.s_addr == -1) { + if (conn->socketAddress.sin_addr.s_addr == INADDR_NONE) { // TODO: Resolve host? Implies a resolve loop for A recs. tho. DELIVER_ERROR_NO_RETURN(DELIVER_TRY_LATER); } else { @@ -2254,10 +2789,11 @@ finish: } int -RewriteAddress(Connection * conn, char *Source, char *Target, unsigned int TargetSize) +RewriteAddress(Connection * conn, const char *Source, char *Target, unsigned int TargetSize) { char WorkSpace[1024]; char *Src, *Dst; + const char *Input; unsigned int i; int RetVal = MAIL_BOGUS; @@ -2280,15 +2816,15 @@ RewriteAddress(Connection * conn, char *Source, char *Target, unsigned int Targe } /** remove illegal chars **/ - Src = Source; + Input = Source; Dst = WorkSpace; - while (*Src) { - if (!isspace (*Src) && *Src != '"') { - *(Dst++) = *(Src++); + while (*Input) { + if (!isspace ((unsigned char) *Input) && *Input != '"') { + *(Dst++) = *(Input++); } else { - Src++; + Input++; } } *Dst = '\0'; @@ -2453,15 +2989,15 @@ RewriteAddress(Connection * conn, char *Source, char *Target, unsigned int Targe static int RecipCompare (const RecipStruct * Recip1, const RecipStruct * Recip2) { - char *ptr1, *ptr2; + const char *ptr1, *ptr2; char char1, char2; if ((!(Recip1->To[0])) || (!(Recip2->To[0]))) { return (-1); } - ptr1 = strchr (Recip1->To, '@') + 1; /* We already made sure the @s exist */ - ptr2 = strchr (Recip2->To, '@') + 1; /* when we were building the list */ + ptr1 = strchr ((const char *) Recip1->To, '@') + 1; /* The @s were validated earlier. */ + ptr2 = strchr ((const char *) Recip2->To, '@') + 1; while (*ptr1 && *ptr2 && *ptr1 != ' ' && *ptr2 != ' ') { char1 = *(ptr1++) & 0xdf; char2 = *(ptr2++) & 0xdf; @@ -2586,7 +3122,7 @@ ProcessRemoteEntry (ConnectionStruct * Client, unsigned long Size, int Lines) Local = TRUE; j = 0; } - if (Recips[NumRecips].ORecip) { + if (Recips[NumRecips].ORecip[0] != '\0') { rc = snprintf (Reply, sizeof (Reply), "QSTOR LOCAL %s %s %lu\r\n", Recips[NumRecips].To, @@ -2661,7 +3197,7 @@ ProcessRemoteEntry (ConnectionStruct * Client, unsigned long Size, int Lines) case DELIVER_TRY_LATER:{ len = snprintf (Reply, sizeof (Reply), "QMOD RAW R%s %s %lu\r\n", Recips[j].To, - Recips[j].ORecip ? Recips[j]. + Recips[j].ORecip[0] != '\0' ? Recips[j]. ORecip : Recips[j].To, Recips[j].Flags); NMAPSendCommand (Client->client.conn, Reply, len); } @@ -2677,7 +3213,7 @@ ProcessRemoteEntry (ConnectionStruct * Client, unsigned long Size, int Lines) len = snprintf (Reply, sizeof (Reply), "QRTS %s %s %lu %d %s\r\n", Recips[j].To, - Recips[j].ORecip ? Recips[j]. + Recips[j].ORecip[0] != '\0' ? Recips[j]. ORecip : Recips[j].To, Recips[j].Flags, Recips[j].Result, Result); @@ -2685,7 +3221,7 @@ ProcessRemoteEntry (ConnectionStruct * Client, unsigned long Size, int Lines) len = snprintf (Reply, sizeof (Reply), "QRTS %s %s %lu %d\r\n", Recips[j].To, - Recips[j].ORecip ? Recips[j]. + Recips[j].ORecip[0] != '\0' ? Recips[j]. ORecip : Recips[j].To, Recips[j].Flags, Recips[j].Result); } @@ -2760,7 +3296,9 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) case QUEUE_CALENDAR_LOCAL:{ /* All calendar messages should be delivered locally. */ - NMAPSendCommand (client->client.conn, reply, snprintf (reply, BUFSIZE, "QMOD RAW %s\r\n", reply)); + NMAPSendCommand (client->client.conn, result, + snprintf (result, sizeof(result), + "QMOD RAW %s\r\n", reply)); continue; } @@ -2769,7 +3307,9 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) if (msgFlags & (MSG_FLAG_SOURCE_EXTERNAL | MSG_FLAG_CALENDAR_OBJECT)) { /* Calendar messages should be delivered locally. Externally received messages (relayed here) should be delivered locally. */ - NMAPSendCommand (client->client.conn, reply, snprintf (reply, BUFSIZE, "QMOD RAW %s\r\n", reply)); + NMAPSendCommand (client->client.conn, result, + snprintf (result, sizeof(result), + "QMOD RAW %s\r\n", reply)); continue; } @@ -2779,7 +3319,7 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) continue; } - /* Fall through */ + __attribute__((fallthrough)); /* All internally generated messages should be relayed. */ } @@ -2860,7 +3400,7 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) j = 0; } - if (recips[recipCount].ORecip) { + if (recips[recipCount].ORecip[0] != '\0') { NMAPSendCommand (client->client.conn, reply, snprintf (reply, sizeof (reply), @@ -2976,7 +3516,7 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) snprintf (reply, sizeof (reply), "QMOD RAW R%s %s %lu\r\n", recips[j].To, - recips[j].ORecip ? recips[j]. + recips[j].ORecip[0] != '\0' ? recips[j]. ORecip : recips[j].To, recips[j].Flags)); break; @@ -2994,7 +3534,7 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) "QRTS %s %s %lu %d %s\r\n", recips[j].To, recips[j]. - ORecip ? recips[j]. + ORecip[0] != '\0' ? recips[j]. ORecip : recips[j].To, recips[j].Flags, recips[j].Result, @@ -3006,7 +3546,7 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) "QRTS %s %s %lu %d\r\n", recips[j].To, recips[j]. - ORecip ? recips[j]. + ORecip[0] != '\0' ? recips[j]. ORecip : recips[j].To, recips[j].Flags, recips[j].Result)); @@ -3092,6 +3632,8 @@ RelayLocalEntry (ConnectionStruct * client, unsigned long size, char reply[BUFSIZE + 1]; BOOL Local = FALSE; + UNUSED_PARAMETER(lines); + envelope = MemMalloc (size + 1); next = envelope; while ((rc = NMAPReadResponse (client->client.conn, buffer, sizeof (buffer) - 1, FALSE)) != 6021) { @@ -3234,7 +3776,7 @@ RelayLocalEntry (ConnectionStruct * client, unsigned long size, case QUEUE_CALENDAR_LOCAL:{ msgFlags |= MSG_FLAG_CALENDAR_OBJECT; - /* Fall through */ + __attribute__((fallthrough)); } case QUEUE_RECIP_LOCAL: @@ -3292,6 +3834,8 @@ ProcessLocalEntry (ConnectionStruct * Client, unsigned long Size, char *Envelope = MemMalloc (Size + 1); char *NextLine = Envelope; + UNUSED_PARAMETER(Lines); + while ((rc = NMAPReadResponse (Client->client.conn, Line, sizeof (Line), FALSE)) != 6021) { rc = strlen (Line); memcpy (NextLine, Line, rc); @@ -3501,7 +4045,7 @@ HandleQueueConnection (void *ClientIn) return; } -static void +static void __attribute__((unused)) QueueServerStartup (void *ignored) { Connection *conn; @@ -3510,6 +4054,8 @@ QueueServerStartup (void *ignored) XplThreadID id = 0; BOOL qd = FALSE, qo = FALSE; + UNUSED_PARAMETER(ignored); + XplRenameThread (XplGetThreadID (), "SMTP NMAP Q Monitor"); SMTPQServerConnection = ConnAlloc(FALSE); @@ -3592,6 +4138,16 @@ SMTPShutdownSigHandler (int sigtype) ConnFree(SMTPServerConnection); SMTPServerConnection = NULL; } + if (SMTPInternalServerConnection) { + ConnClose(SMTPInternalServerConnection); + ConnFree(SMTPInternalServerConnection); + SMTPInternalServerConnection = NULL; + } + if (SMTPSubmissionServerConnection) { + ConnClose(SMTPSubmissionServerConnection); + ConnFree(SMTPSubmissionServerConnection); + SMTPSubmissionServerConnection = NULL; + } XplSignalLocalSemaphore (SMTPShutdownSemaphore); } else if (signaled && (sigtype == SIGTERM || sigtype == SIGINT)) { @@ -3650,7 +4206,8 @@ ServerSocketSSLInit (void) } SMTPServerConnectionSSL->socketAddress.sin_family = AF_INET; - SMTPServerConnectionSSL->socketAddress.sin_port = htons (SMTP.port_ssl); + SMTPServerConnectionSSL->socketAddress.sin_port = htons( + SMTP.submission_enabled ? SMTP.submission_port_ssl : SMTP.port_ssl); SMTPServerConnectionSSL->socketAddress.sin_addr.s_addr = MsgGetAgentBindIPAddress (); /* Get root privs back for the bind. It's ok if this fails - @@ -3673,6 +4230,182 @@ ServerSocketSSLInit (void) return 0; } +static int +SubmissionServerSocketInit(void) +{ + int ccode; + SMTPSubmissionServerConnection = ConnAlloc(FALSE); + if (SMTPSubmissionServerConnection == NULL) return -1; + SMTPSubmissionServerConnection->socketAddress.sin_family = AF_INET; + SMTPSubmissionServerConnection->socketAddress.sin_port = htons(SMTP.submission_port); + SMTPSubmissionServerConnection->socketAddress.sin_addr.s_addr = MsgGetAgentBindIPAddress(); + XplSetEffectiveUserId(0); + SMTPSubmissionServerConnection->socket = ConnServerSocket(SMTPSubmissionServerConnection, 256); + if (XplSetEffectiveUser(MsgGetUnprivilegedUser()) < 0) return -1; + if (SMTPSubmissionServerConnection->socket < 0) { + ccode = SMTPSubmissionServerConnection->socket; + ConnFree(SMTPSubmissionServerConnection); + SMTPSubmissionServerConnection = NULL; + return ccode; + } + return 0; +} + +static void +SMTPSubmissionServer(void *ignored) +{ + Connection *conn; + ConnectionStruct *client; + XplThreadID id; + int ccode; + (void) ignored; + XplRenameThread(XplGetThreadID(), "SMTP Submission Server"); + while (!Exiting) { + if (ConnAccept(SMTPSubmissionServerConnection, &conn) == -1) { + if (!Exiting && errno == EINTR) continue; + break; + } + if (SMTPReceiverStopped || XplSafeRead(SMTPConnThreads) >= SMTP.max_thread_load) { + ConnSend(conn, MSG451NOCONNECTIONS, MSG451NOCONNECTIONS_LEN); + ConnClose(conn); + ConnFree(conn); + continue; + } + client = GetSMTPConnection(); + if (client == NULL) { + ConnSend(conn, MSG500NOMEMORY, MSG500NOMEMORY_LEN); + ConnClose(conn); + ConnFree(conn); + continue; + } + client->client.conn = conn; + client->Submission = TRUE; + XplBeginCountedThread(&id, HandleConnection, STACKSIZE_S, client, ccode, SMTPConnThreads); + if (ccode == 0) continue; + ReturnSMTPConnection(client); + ConnClose(conn); + ConnFree(conn); + } + XplSafeDecrement(SMTPServerThreads); + Log(LOG_DEBUG, "SMTP submission listening thread terminated"); +} + +static int +InternalServerSocketInit(void) +{ + int ccode; + + SMTPInternalServerConnection = ConnAlloc(FALSE); + if (SMTPInternalServerConnection == NULL) { + return -1; + } + SMTPInternalServerConnection->socketAddress.sin_family = AF_INET; + SMTPInternalServerConnection->socketAddress.sin_port = htons(SMTP.internal_relay_port); + if (inet_pton(AF_INET, SMTP.internal_relay_bind_address, + &SMTPInternalServerConnection->socketAddress.sin_addr) != 1) { + Log(LOG_ERROR, "Invalid internal SMTP bind address: %s", + SMTP.internal_relay_bind_address); + ConnFree(SMTPInternalServerConnection); + SMTPInternalServerConnection = NULL; + return -1; + } + XplSetEffectiveUserId(0); + SMTPInternalServerConnection->socket = ConnServerSocket(SMTPInternalServerConnection, 128); + if (XplSetEffectiveUser(MsgGetUnprivilegedUser()) < 0) { + return -1; + } + if (SMTPInternalServerConnection->socket < 0) { + ccode = SMTPInternalServerConnection->socket; + Log(LOG_ERROR, "Could not bind internal SMTP to %s:%d", + SMTP.internal_relay_bind_address, SMTP.internal_relay_port); + ConnFree(SMTPInternalServerConnection); + SMTPInternalServerConnection = NULL; + return ccode; + } + return 0; +} + +static void +SMTPInternalServer(void *ignored) +{ + Connection *conn; + ConnectionStruct *client; + XplThreadID id; + int ccode; + + (void) ignored; + XplRenameThread(XplGetThreadID(), "SMTP Internal Server"); + while (!Exiting) { + if (ConnAccept(SMTPInternalServerConnection, &conn) == -1) { + if (!Exiting && errno == EINTR) continue; + break; + } + if (!InternalRelayAddressAllowed(conn->socketAddress.sin_addr)) { + Log(LOG_WARN, "Rejected internal SMTP connection from %s", LOGIP(conn->socketAddress)); + ConnSend(conn, "554 5.7.1 Source address not permitted\r\n", 41); + ConnClose(conn); + ConnFree(conn); + continue; + } + if (SMTPReceiverStopped || XplSafeRead(SMTPConnThreads) >= SMTP.max_thread_load) { + ConnSend(conn, MSG451NOCONNECTIONS, MSG451NOCONNECTIONS_LEN); + ConnClose(conn); + ConnFree(conn); + continue; + } + client = GetSMTPConnection(); + if (client == NULL) { + ConnSend(conn, MSG500NOMEMORY, MSG500NOMEMORY_LEN); + ConnClose(conn); + ConnFree(conn); + continue; + } + client->client.conn = conn; + client->InternalRelay = TRUE; + XplMutexLock(InternalRelayRateLock); + client->InternalRate = g_hash_table_lookup(InternalRelayRates, + &conn->socketAddress.sin_addr.s_addr); + if (client->InternalRate == NULL) { + uint32_t *key = g_new(uint32_t, 1); + client->InternalRate = g_new0(InternalRelayRate, 1); + if (key != NULL && client->InternalRate != NULL) { + *key = conn->socketAddress.sin_addr.s_addr; + client->InternalRate->window = time(NULL); + g_hash_table_insert(InternalRelayRates, key, client->InternalRate); + } else { + g_free(key); + g_free(client->InternalRate); + client->InternalRate = NULL; + } + } + if (client->InternalRate == NULL || + client->InternalRate->connections >= + (unsigned int) SMTP.internal_relay_connections_per_ip) { + XplMutexUnlock(InternalRelayRateLock); + ConnSend(conn, "451 4.7.1 Too many internal relay connections\r\n", + strlen("451 4.7.1 Too many internal relay connections\r\n")); + ReturnSMTPConnection(client); + ConnClose(conn); + ConnFree(conn); + continue; + } + client->InternalRate->connections++; + XplMutexUnlock(InternalRelayRateLock); + XplBeginCountedThread(&id, HandleConnection, STACKSIZE_S, client, ccode, SMTPConnThreads); + if (ccode == 0) continue; + XplMutexLock(InternalRelayRateLock); + if (client->InternalRate != NULL && client->InternalRate->connections > 0) { + client->InternalRate->connections--; + } + XplMutexUnlock(InternalRelayRateLock); + ReturnSMTPConnection(client); + ConnClose(conn); + ConnFree(conn); + } + XplSafeDecrement(SMTPServerThreads); + Log(LOG_DEBUG, "Internal SMTP listening thread terminated"); +} + static void SMTPServer (void *ignored) { @@ -3683,6 +4416,8 @@ SMTPServer (void *ignored) ConnectionStruct *client; XplThreadID id; + UNUSED_PARAMETER(ignored); + XplSafeIncrement (SMTPServerThreads); XplRenameThread (XplGetThreadID(), "SMTP Server"); @@ -3760,6 +4495,7 @@ SMTPServer (void *ignored) Exiting = TRUE; oldTGID = XplSetThreadGroupID (TGid); + UNUSED_PARAMETER(oldTGID); Log(LOG_DEBUG, "Closing server socket"); @@ -3773,6 +4509,16 @@ SMTPServer (void *ignored) ConnFree(SMTPServerConnectionSSL); SMTPServerConnectionSSL = NULL; } + if (SMTPInternalServerConnection) { + ConnClose(SMTPInternalServerConnection); + ConnFree(SMTPInternalServerConnection); + SMTPInternalServerConnection = NULL; + } + if (SMTPSubmissionServerConnection) { + ConnClose(SMTPSubmissionServerConnection); + ConnFree(SMTPSubmissionServerConnection); + SMTPSubmissionServerConnection = NULL; + } if (SMTPQServerConnection) { ConnClose(SMTPQServerConnection); @@ -3816,6 +4562,14 @@ SMTPServer (void *ignored) ConnSSLContextFree(SSLContext); SSLContext = NULL; } + if (InternalSSLContext && InternalSSLContext != SSLContext) { + ConnSSLContextFree(InternalSSLContext); + InternalSSLContext = NULL; + } + + g_hash_table_destroy(InternalRelayRates); + InternalRelayRates = NULL; + XplMutexDestroy(InternalRelayRateLock); LogShutdown (); @@ -3849,6 +4603,8 @@ SMTPSSLServer (void *ignored) ConnectionStruct *client; XplThreadID id = 0; + UNUSED_PARAMETER(ignored); + XplRenameThread (XplGetThreadID (), "SMTP SSL Server"); while(!Exiting) { @@ -3860,6 +4616,7 @@ SMTPSSLServer (void *ignored) client = GetSMTPConnection(); if (client) { client->client.conn = conn; + client->Submission = SMTP.submission_enabled; XplBeginCountedThread (&id, HandleConnection, STACKSIZE_S, client, ccode, SMTPConnThreads); if (ccode == 0) { continue; @@ -3974,6 +4731,82 @@ ReadConfiguration (void) return (TRUE); } +static BOOL +ValidateListenerConfiguration(void) +{ + unsigned int index; + if (SMTP.submission_enabled && + (SMTP.submission_port < 1 || SMTP.submission_port > 65535 || + SMTP.submission_port_ssl < 1 || SMTP.submission_port_ssl > 65535)) { + Log(LOG_ERROR, "SMTP submission ports must be between 1 and 65535"); + return FALSE; + } + if (SMTP.submission_enabled && (!SMTP.allow_auth || !SMTP.submission_require_auth)) { + Log(LOG_ERROR, "SMTP submission must have authentication enabled and required"); + return FALSE; + } + if (SMTP.submission_enabled && + (SMTP.submission_port == SMTP.port || + SMTP.submission_port_ssl == SMTP.port || + SMTP.submission_port == SMTP.submission_port_ssl)) { + Log(LOG_ERROR, "SMTP relay and submission ports must be distinct"); + return FALSE; + } + if (SMTP.internal_relay_enabled) { + if (SMTP.internal_relay_port < 1 || SMTP.internal_relay_port > 65535) { + Log(LOG_ERROR, "Internal SMTP relay port must be between 1 and 65535"); + return FALSE; + } + if (SMTP.internal_relay_port == SMTP.port || + (SMTP.submission_enabled && + (SMTP.internal_relay_port == SMTP.submission_port || + SMTP.internal_relay_port == SMTP.submission_port_ssl))) { + Log(LOG_ERROR, "Internal SMTP relay port conflicts with another SMTP listener"); + return FALSE; + } + if (SMTP.internal_relay_bind_address == NULL || + *SMTP.internal_relay_bind_address == '\0' || + SMTP.internal_relay_networks == NULL || + SMTP.internal_relay_networks->len == 0) { + Log(LOG_ERROR, "Internal SMTP relay requires a bind address and at least one allowed source network"); + return FALSE; + } + if (SMTP.internal_relay_messages_per_minute < 1 || + SMTP.internal_relay_recipients_per_minute < 1 || + SMTP.internal_relay_bytes_per_minute < 1 || + SMTP.internal_relay_connections_per_ip < 1) { + Log(LOG_ERROR, "Internal SMTP relay limits must be greater than zero"); + return FALSE; + } + if (SMTP.internal_relay_domain == NULL || + *SMTP.internal_relay_domain == '\0') { + Log(LOG_ERROR, "Internal SMTP relay requires a normalization domain"); + return FALSE; + } + for (index = 0; SMTP.internal_relay_device_mappings != NULL && + index < SMTP.internal_relay_device_mappings->len; index++) { + const char *mapping = g_array_index(SMTP.internal_relay_device_mappings, + char *, index); + const char *equals = mapping != NULL ? strchr(mapping, '=') : NULL; + char ip[INET_ADDRSTRLEN]; + struct in_addr parsed; + size_t length = equals != NULL ? (size_t) (equals - mapping) : 0; + if (length == 0 || length >= sizeof(ip) || !ValidDeviceName(equals + 1)) { + Log(LOG_ERROR, "Invalid internal SMTP device mapping: %s", + mapping != NULL ? mapping : "(null)"); + return FALSE; + } + memcpy(ip, mapping, length); + ip[length] = '\0'; + if (inet_pton(AF_INET, ip, &parsed) != 1) { + Log(LOG_ERROR, "Invalid IP address in internal SMTP device mapping: %s", mapping); + return FALSE; + } + } + } + return TRUE; +} + XplServiceCode (SMTPShutdownSigHandler) int XplServiceMain (int argc, char *argv[]) @@ -3989,6 +4822,13 @@ int XplServiceMain (int argc, char *argv[]) } XplInit(); + XplMutexInit(InternalRelayRateLock); + InternalRelayRates = g_hash_table_new_full(g_int_hash, g_int_equal, g_free, g_free); + if (InternalRelayRates == NULL) { + Log(LOG_ERROR, "Could not allocate internal SMTP rate-limit table"); + return 1; + } + XplSignalHandler (SMTPShutdownSigHandler); XplSafeWrite (SMTPServerThreads, 0); @@ -4037,6 +4877,9 @@ int XplServiceMain (int argc, char *argv[]) Log(LOG_ERROR, "Unable to read Global configration from the store."); exit(-1); } + if (!ValidateListenerConfiguration()) { + return 1; + } ReadConfiguration (); if (ServerSocketInit () < 0) { @@ -4046,9 +4889,13 @@ int XplServiceMain (int argc, char *argv[]) // XplBeginCountedThread (&ID, QueueServerStartup, STACKSIZE_Q, NULL, ccode, SMTPServerThreads); - if (SMTP.allow_client_ssl) { + if (SMTP.allow_client_ssl || SMTP.submission_enabled) { if (!ServerSocketSSLInit()) { ConnSSLConfiguration sslconfig; + memset(&sslconfig, 0, sizeof(sslconfig)); + if (SMTP.allow_legacy_tls) { + sslconfig.options |= SSL_ALLOW_LEGACY_PROTOCOLS; + } sslconfig.key.file = MsgGetFile(MSGAPI_FILE_PRIVKEY, NULL, 0); sslconfig.certificate.file = MsgGetFile(MSGAPI_FILE_PUBKEY, NULL, 0); sslconfig.key.type = GNUTLS_X509_FMT_PEM; @@ -4057,10 +4904,50 @@ int XplServiceMain (int argc, char *argv[]) if (SSLContext) { XplBeginCountedThread (&ID, SMTPSSLServer, STACKSIZE_S, NULL, ccode, SMTPServerThreads); } else { - SMTP.allow_client_ssl = FALSE; + if (!SMTP.submission_enabled) SMTP.allow_client_ssl = FALSE; } } else { - SMTP.allow_client_ssl = FALSE; + if (!SMTP.submission_enabled) SMTP.allow_client_ssl = FALSE; + } + } + + if (SMTP.submission_enabled) { + if (SSLContext == NULL) { + Log(LOG_ERROR, "SMTP submission requires a usable TLS certificate and key"); + return 1; + } + if (SubmissionServerSocketInit() < 0) { + Log(LOG_ERROR, "Can't open SMTP submission port %d", SMTP.submission_port); + return 1; + } + XplBeginCountedThread(&ID, SMTPSubmissionServer, STACKSIZE_S, NULL, ccode, + SMTPServerThreads); + if (ccode != 0) return 1; + } + + if (SMTP.internal_relay_enabled) { + ConnSSLConfiguration sslconfig; + + if (InternalServerSocketInit() < 0) { + Log(LOG_ERROR, "Can't open internal SMTP relay port"); + return 1; + } + memset(&sslconfig, 0, sizeof(sslconfig)); + if (SMTP.internal_relay_allow_legacy_tls) { + sslconfig.options |= SSL_ALLOW_LEGACY_PROTOCOLS; + } + sslconfig.key.file = MsgGetFile(MSGAPI_FILE_PRIVKEY, NULL, 0); + sslconfig.certificate.file = MsgGetFile(MSGAPI_FILE_PUBKEY, NULL, 0); + sslconfig.key.type = GNUTLS_X509_FMT_PEM; + InternalSSLContext = ConnSSLContextAlloc(&sslconfig); + if (InternalSSLContext == NULL) { + Log(LOG_WARN, "Internal SMTP STARTTLS disabled: TLS context could not be loaded"); + } + XplBeginCountedThread(&ID, SMTPInternalServer, STACKSIZE_S, NULL, ccode, + SMTPServerThreads); + if (ccode != 0) { + Log(LOG_ERROR, "Could not start internal SMTP listener thread"); + return 1; } } diff --git a/src/agents/smtp/smtpd.h b/src/agents/smtp/smtpd.h index 9bbbcb5..77d870e 100755 --- a/src/agents/smtp/smtpd.h +++ b/src/agents/smtp/smtpd.h @@ -75,7 +75,7 @@ #define MSG250ETRN "250-ETRN\r\n" #define MSG250TLS "250-STARTTLS\r\n" -#define MSG250AUTH "250-AUTH LOGIN\r\n250-AUTH=LOGIN\r\n" +#define MSG250AUTH "250-AUTH PLAIN LOGIN\r\n" #define MSG250EHLO "250-HELP\r\n250-EXPN\r\n250-PIPELINING\r\n250-8BITMIME\r\n250-DSN\r\n250 SIZE" #define MSG251NOMESSAGES "251 OK No messages waiting for you\r\n" @@ -176,6 +176,9 @@ #define MSG550TOOMANY "550 Too many recipients\r\n" #define MSG550TOOMANY_LEN 25 +#define MSG550SRSINVALID "550 Invalid or expired SRS recipient\r\n" +#define MSG550SRSINVALID_LEN 38 + #define MSG552MSGTOOBIG "552 Message size exceeds maximum allowed by this server\r\n" #define MSG552MSGTOOBIG_LEN 57 @@ -192,4 +195,3 @@ /* DO NOT ADD A CR/LF AT THE END OF THE FOLLOWING STRING - this is not a response code, but a fake error code for sending */ #define MSG422CONNERROR "422 Remote system unexpectedly closed the connection." #define MSG422CONNERROR_LEN 53 - diff --git a/src/agents/store/command-parsing.c b/src/agents/store/command-parsing.c index a38a47e..f950520 100644 --- a/src/agents/store/command-parsing.c +++ b/src/agents/store/command-parsing.c @@ -13,7 +13,7 @@ typedef struct { const StorePrivilege priv; - const char const *name; + const char *name; } StorePrivName; static const StorePrivName StorePrivilegeTable[] = { @@ -35,7 +35,7 @@ static const StorePrivName StorePrivilegeTable[] = { }; CCode -ParsePrivilege(StoreClient *client, const char const *token, StorePrivilege *priv) +ParsePrivilege(StoreClient *client, const char *token, StorePrivilege *priv) { StorePrivName const *priventry = StorePrivilegeTable; @@ -75,7 +75,7 @@ PrivilegeToString(const StorePrivilege priv, char const **out) } CCode -ParsePrincipal(StoreClient *client, const char const *token, StorePrincipalType *type) +ParsePrincipal(StoreClient *client, const char *token, StorePrincipalType *type) { if (client == NULL) return -1; @@ -186,7 +186,7 @@ ParseUnsignedLong(StoreClient *client, char *token, unsigned long *out) CCode -ParseInt(StoreClient *client, const char const *token, int *out) +ParseInt(StoreClient *client, const char *token, int *out) { long t; char *endp; @@ -293,6 +293,7 @@ ParseIcalDateTime(StoreClient *client, CCode ParseDateTime(StoreClient *client, char *token, BongoCalTime *timeOut) { + CCode ccode; int len; len = strlen(token); @@ -303,7 +304,6 @@ ParseDateTime(StoreClient *client, char *token, BongoCalTime *timeOut) return ParseIcalDateTime(client, token, timeOut); } else { unsigned long ulong=0; - CCode ccode; ccode = ParseUnsignedLong(client, token, &ulong); @@ -335,7 +335,6 @@ ParseDateTimeToUint64(StoreClient *client, char *token, uint64_t *timeOut) } else { /* If it's a long arg, just pass it through */ unsigned long ulong=0; - CCode ccode; ccode = ParseUnsignedLong(client, token, &ulong); diff --git a/src/agents/store/command-parsing.h b/src/agents/store/command-parsing.h index 0a440bd..3938220 100644 --- a/src/agents/store/command-parsing.h +++ b/src/agents/store/command-parsing.h @@ -10,7 +10,7 @@ CheckTokC(StoreClient *client, int n, int min, int max); CCode ParseGUID(StoreClient *client, char *token, uint64_t *out); CCode ParseUnsignedLong(StoreClient *client, char *token, unsigned long *out); -CCode ParseInt(StoreClient *client, const char const *token, int *out); +CCode ParseInt(StoreClient *client, const char *token, int *out); CCode ParseHexU32(StoreClient *client, char *token, uint32_t *out); CCode ParseStreamLength(StoreClient *client, char *token, int *out); int ParseRange(StoreClient *client, char *token, int *startOut, int *endOut); @@ -31,8 +31,8 @@ CCode ParseCreateDocument(StoreClient *client, char *token, uint64_t *outGuid, S CCode ParseDocType(StoreClient *client, char *token, StoreDocumentType *out); CCode ParsePropName(StoreClient *client, char *token, StorePropInfo *prop); CCode ParsePropList(StoreClient *client, char *token, StorePropInfo * props, int propcount, int *count, int requireLengths); -CCode ParsePrivilege(StoreClient *client, const char const *token, StorePrivilege *priv); -CCode ParsePrincipal(StoreClient *client, const char const *token, StorePrincipalType *type); +CCode ParsePrivilege(StoreClient *client, const char *token, StorePrivilege *priv); +CCode ParsePrincipal(StoreClient *client, const char *token, StorePrincipalType *type); CCode ParseHeaderList(StoreClient *client, char *token, StoreHeaderInfo *headers, int size, int *count); CCode ParseStoreName(StoreClient *client, char *token); CCode ParseUserName(StoreClient *client, char *token); diff --git a/src/agents/store/command.c b/src/agents/store/command.c index 984508a..6903871 100644 --- a/src/agents/store/command.c +++ b/src/agents/store/command.c @@ -75,7 +75,10 @@ CommandCmp(const void *cmda, const void *cmdb) return 0; } -#define CHECK_NOT_READONLY(x) { CCode ret = IsStoreReadonly((x)); if (ret != 0) return ret; } +#define CHECK_NOT_READONLY(x) do { \ + CCode readonly_code = IsStoreReadonly((x)); \ + if (readonly_code != 0) return readonly_code; \ +} while (0); static CCode IsStoreReadonly(StoreClient *client) @@ -260,7 +263,7 @@ StoreCommandLoop(StoreClient *client) if (XplStrNCaseCmp("AUTH", client->buffer, 4) == 0) { strcpy(command_message, "-> AUTH ....."); } else { - snprintf(command_message, 255, "-> %s", client->buffer); + snprintf(command_message, sizeof(command_message), "-> %.251s", client->buffer); } Ringlog(command_message); @@ -269,7 +272,8 @@ StoreCommandLoop(StoreClient *client) if (0 == n) { command = STORE_COMMAND_NULL; } else { - command = (StoreCommand) BongoHashtableGet(CommandTable, tokens[0]); + command = (StoreCommand) (intptr_t) + BongoHashtableGet(CommandTable, tokens[0]); } if (client->watch.collection.guid > 0) { @@ -1637,7 +1641,11 @@ StoreCommandCOPY(StoreClient *client, StoreObject *object, StoreObject *collecti newobject.collection_guid = collection->guid; newobject.time_created = newobject.time_modified = time(NULL); - StoreObjectFixUpFilename(collection, &newobject); + if (StoreObjectFixUpFilename(collection, &newobject) != 0) { + StoreObjectRemove(client, &newobject); + ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX); + goto finish; + } StoreObjectSave(client, &newobject); @@ -1782,7 +1790,11 @@ StoreCommandCREATE(StoreClient *client, char *name, uint64_t guid) // check we can save this new object and if not, error out object.collection_guid = container.guid; - StoreObjectFixUpFilename(&container, &object); + if (StoreObjectFixUpFilename(&container, &object) != 0) { + StoreObjectRemove(client, &object); + LogicalLockRelease(client, &container, LLOCK_READWRITE, "StoreCommandCREATE"); + return ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX); + } if (StoreObjectSave(client, &object)) { StoreObjectRemove(client, &object); @@ -2394,7 +2406,10 @@ StoreCommandMOVE(StoreClient *client, StoreObject *object, object->collection_guid = destination_collection->guid; if (newfilename != NULL) strncpy(object->filename, newfilename, MAX_FILE_NAME); - StoreObjectFixUpFilename(destination_collection, object); + if (StoreObjectFixUpFilename(destination_collection, object) != 0) { + ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX); + goto finish; + } StoreObjectUpdateImapUID(client, object); if (STORE_DOCTYPE_MAIL == object->type) { @@ -2794,15 +2809,15 @@ StoreCommandREPAIR(StoreClient *client) switch (test) { case 1: // repaired - sprintf(reason, "2001 %s\r\n", object.filename); + snprintf(reason, sizeof(reason), "2001 %.192s\r\n", object.filename); break; case -1: // broken - sprintf(reason, "2011 %s\r\n", object.filename); + snprintf(reason, sizeof(reason), "2011 %.192s\r\n", object.filename); break; case -2: // removed - sprintf(reason, "2012 %s\r\n", object.filename); + snprintf(reason, sizeof(reason), "2012 %.192s\r\n", object.filename); break; default: break; @@ -3093,7 +3108,11 @@ StoreCommandWRITE(StoreClient *client, // place the new document in the right collection // we take the lock on the collection this late, newdocument.collection_guid = collection->guid; - StoreObjectFixUpFilename(collection, &newdocument); + if (StoreObjectFixUpFilename(collection, &newdocument) != 0) { + unlink(path); + LogicalLockRelease(client, collection, LLOCK_READWRITE, "StoreCommandWRITE"); + return ConnWriteStr(client->conn, MSG4224CANTWRITE); + } StoreObjectUpdateImapUID(client, &newdocument); if (StoreObjectSave(client, &newdocument)) { diff --git a/src/agents/store/contacts.c b/src/agents/store/contacts.c index d110dca..96e1a44 100644 --- a/src/agents/store/contacts.c +++ b/src/agents/store/contacts.c @@ -39,11 +39,17 @@ StoreProcessIncomingContact(StoreClient *client, StoreObject *contact, char *pat BongoJsonResult result; result = BongoJsonJPathGetString(node, "o:fullName/s", &name); - if (result != BONGO_JSON_OK) name = NULL; + if (result != BONGO_JSON_OK) { + result = BongoJsonJPathGetString(node, "o:fn/s", &name); + if (result != BONGO_JSON_OK) name = NULL; + } // FIXME: want to find the preferred address really. result = BongoJsonJPathGetString(node, "o:email/a:0/o:address/s", &email); - if (result != BONGO_JSON_OK) email = NULL; + if (result != BONGO_JSON_OK) { + result = BongoJsonJPathGetString(node, "o:email/a:0/o:value/s", &email); + if (result != BONGO_JSON_OK) email = NULL; + } if (name) SetDocProp(client, contact, "bongo.contact.name", name); if (email) SetDocProp(client, contact, "bongo.contact.email", email); diff --git a/src/agents/store/cookie.c b/src/agents/store/cookie.c index d6661c4..9568df0 100644 --- a/src/agents/store/cookie.c +++ b/src/agents/store/cookie.c @@ -21,6 +21,7 @@ OpenCookieDB(StoreClient *client) MsgSQLStatement *schema = NULL; MsgSQLHandle *cdb = MsgSQLOpen(path, &client->memstack, 3000); + if (cdb == NULL) return NULL; if (MsgSQLBeginTransaction(cdb)) return NULL; @@ -64,17 +65,14 @@ StoreCommandCOOKIENEW(StoreClient *client, uint64_t timeout) //MsgSQLStatement *ret; memset(&stmt, 0, sizeof(MsgSQLStatement)); - char token[50]; - xpl_hash_context context; + unsigned char random[32]; + char token[(sizeof(random) * 2) + 1]; MsgSQLHandle *db = NULL; - XplHashNew(&context, XPLHASH_MD5); - snprintf(token, sizeof(token) - 1, "%x%x", - (unsigned int) XplGetThreadID(), (unsigned int) time(NULL)); - XplHashWrite(&context, token, strlen(token)); - XplRandomData(token, 8); - XplHashWrite(&context, token, strlen(token)); - XplHashFinal(&context, XPLHASH_LOWERCASE, token, XPLHASH_MD5_LENGTH); + XplRandomData(random, sizeof(random)); + for (size_t i = 0; i < sizeof(random); i++) { + snprintf(token + (i * 2), 3, "%02x", random[i]); + } uint64_t expiration = time(NULL) + timeout; @@ -96,11 +94,11 @@ StoreCommandCOOKIENEW(StoreClient *client, uint64_t timeout) MsgSQLClose(db); - return ConnWriteF(client->conn, "1000 %.32s\r\n", token); + return ConnWriteF(client->conn, "1000 %s\r\n", token); abort: MsgSQLFinalize(&stmt); - MsgSQLAbortTransaction(db); + if (db) MsgSQLAbortTransaction(db); if (db) MsgSQLClose(db); return ConnWriteStr(client->conn, MSG5004INTERNALERR); } @@ -141,7 +139,7 @@ StoreCommandCOOKIEDELETE(StoreClient *client, const char *token) abort: MsgSQLFinalize(&stmt); - MsgSQLAbortTransaction(db); + if (db) MsgSQLAbortTransaction(db); if (db) MsgSQLClose(db); return ConnWriteStr(client->conn, MSG5004INTERNALERR); } @@ -167,7 +165,8 @@ StoreCommandAUTHCOOKIE(StoreClient *client, char *user, char *token, int nouser) int result; MsgSQLHandle *db = OpenCookieDB(client); - + if (db == NULL) return ConnWriteStr(client->conn, MSG5004INTERNALERR); + if (MsgSQLBeginTransaction(db)) return -2; memset(&stmt, 0, sizeof(MsgSQLStatement)); @@ -198,7 +197,7 @@ StoreCommandAUTHCOOKIE(StoreClient *client, char *user, char *token, int nouser) abort: MsgSQLFinalize(&stmt); - MsgSQLAbortTransaction(db); + if (db) MsgSQLAbortTransaction(db); if (db) MsgSQLClose(db); return ConnWriteStr(client->conn, MSG5004INTERNALERR); } @@ -210,7 +209,8 @@ StoreCommandCOOKIELIST(StoreClient *client) MsgSQLStatement *find = NULL; MsgSQLHandle *db = OpenCookieDB(client); - + if (db == NULL) return ConnWriteStr(client->conn, MSG5004INTERNALERR); + if (MsgSQLBeginTransaction(db)) return -2; memset(&stmt, 0, sizeof(MsgSQLStatement)); @@ -239,8 +239,7 @@ StoreCommandCOOKIELIST(StoreClient *client) abort: MsgSQLFinalize(&stmt); - MsgSQLAbortTransaction(db); + if (db) MsgSQLAbortTransaction(db); if (db) MsgSQLClose(db); return ConnWriteStr(client->conn, MSG5004INTERNALERR); } - diff --git a/src/agents/store/db.c b/src/agents/store/db.c index 5dbd851..349672f 100644 --- a/src/agents/store/db.c +++ b/src/agents/store/db.c @@ -91,7 +91,12 @@ StoreDBOpen(StoreClient *client, const char *user) // this bit is slow, but I can't think how we could // do this outside the dbpool.lock without being racy :( - snprintf(path, XPL_MAX_PATH, "%s/%s/store.db", StoreAgent.store.rootDir, user); + if (snprintf(path, sizeof(path), "%s/%s/store.db", + StoreAgent.store.rootDir, user) >= (int)sizeof(path)) { + BongoHashtableRemove(StoreAgent.dbpool.entries, (void *)user); + DBPoolEntryDelete(entry); + goto db_fail; + } entry->handle = MsgSQLOpen(path, &client->memstack, 3000); if (entry->handle == NULL) { BongoHashtableRemove(StoreAgent.dbpool.entries, (void *)user); diff --git a/src/agents/store/guid.c b/src/agents/store/guid.c index 2fc1125..e2e5ad4 100644 --- a/src/agents/store/guid.c +++ b/src/agents/store/guid.c @@ -36,7 +36,7 @@ void GuidReset(void) { #ifdef HAVE_KSTAT_H kstat_t *ksp; kstat_ctl_t *kc; -#elifdef HAVE_SYS_SYSINFO_H +#elif defined(HAVE_SYS_SYSINFO_H) struct sysinfo si; #endif struct timeval tv; diff --git a/src/agents/store/mail.c b/src/agents/store/mail.c index d691199..15b6f50 100644 --- a/src/agents/store/mail.c +++ b/src/agents/store/mail.c @@ -31,8 +31,8 @@ SetDocProp(StoreClient *client, StoreObject *doc, char *pname, char *value) } struct wanted_header { - const char const *header; - const char const *propname; + const char *header; + const char *propname; }; // properties we want to look for and set automatically @@ -45,6 +45,11 @@ static struct wanted_header header_list[] = { { "References", "bongo.references" }, { "X-Auth-OK", "bongo.xauthok" }, { "List-Id", "bongo.listid" }, + { "List-Post", "bongo.listpost" }, + { "List-Unsubscribe", "bongo.listunsubscribe" }, + { "List-Unsubscribe-Post", "bongo.listunsubscribepost" }, + { "List-Archive", "bongo.listarchive" }, + { "Auto-Submitted", "bongo.autosubmitted" }, { "Subject", "nmap.mail.subject" }, { NULL, NULL } }; @@ -238,7 +243,10 @@ StoreProcessIncomingMail(StoreClient *client, // save setting collection_guid til last, so that the new convo // 'appears' atomically conversation.collection_guid = STORE_CONVERSATIONS_GUID; - StoreObjectFixUpFilename(&convo_collection, &conversation); + if (StoreObjectFixUpFilename(&convo_collection, &conversation) != 0) { + result = MSG5005DBLIBERR; + goto finish; + } if (StoreObjectSave(client, &conversation) != 0) { result = MSG5005DBLIBERR; goto finish; @@ -261,4 +269,3 @@ finish: return result; } - diff --git a/src/agents/store/maildir.c b/src/agents/store/maildir.c index 68ac20e..5ba7021 100644 --- a/src/agents/store/maildir.c +++ b/src/agents/store/maildir.c @@ -112,7 +112,10 @@ MaildirRemove(const char *store_path, uint64_t collection) DIR *dir; struct dirent *content; - snprintf(rootpath, XPL_MAX_PATH, "%s" GUID_FMT "%s", store_path, collection, dirs[i]); + if (snprintf(rootpath, sizeof(rootpath), "%s" GUID_FMT "%s", + store_path, collection, dirs[i]) >= (int)sizeof(rootpath)) { + return ENAMETOOLONG; + } dir = opendir(rootpath); if (dir) { char filename[XPL_MAX_PATH+1]; @@ -122,7 +125,11 @@ MaildirRemove(const char *store_path, uint64_t collection) if ((c[0] == '.') && ((c[1] == '.') || (c[1] == '\0'))){ continue; } - snprintf(filename, XPL_MAX_PATH, "%s/%s", rootpath, content->d_name); + if (snprintf(filename, sizeof(filename), "%s/%s", + rootpath, content->d_name) >= (int)sizeof(filename)) { + closedir(dir); + return ENAMETOOLONG; + } if (unlink(filename) == -1) { // can't remove one of the contents closedir(dir); @@ -134,7 +141,10 @@ MaildirRemove(const char *store_path, uint64_t collection) if (rmdir(rootpath) != 0) return errno; } - snprintf(rootpath, XPL_MAX_PATH, "%s" GUID_FMT, store_path, collection); + if (snprintf(rootpath, sizeof(rootpath), "%s" GUID_FMT, + store_path, collection) >= (int)sizeof(rootpath)) { + return ENAMETOOLONG; + } if (rmdir(rootpath) != 0) return errno; return 0; diff --git a/src/agents/store/mime.c b/src/agents/store/mime.c index 08e4166..a2476f7 100644 --- a/src/agents/store/mime.c +++ b/src/agents/store/mime.c @@ -388,12 +388,12 @@ MimeParse(FILE *fh, size_t messageSize, char *line, unsigned long lineSize) do { /* Check to see if we've hit the end of the header. 'CR LF' is the only valid indicator */ /* but this will handle common broken alternatives such as 'CR CR LF' and 'LF' */ - unsigned long i = 0; + unsigned long line_index = 0; - while (line[i] == '\r') { - i++; + while (line[line_index] == '\r') { + line_index++; } - if (line[i] == '\n') { + if (line[line_index] == '\n') { lastHeaderLen = sPos - lastHeaderStart; if (!skip) { inHeader=FALSE; @@ -536,7 +536,7 @@ MimeParse(FILE *fh, size_t messageSize, char *line, unsigned long lineSize) doingDigest = TRUE; } ParseTypeParameters(end + 1, &part[workPartNo]); - if (part[workPartNo].separator) { + if (part[workPartNo].separator[0] != '\0') { currentSeparator = part[workPartNo].separator; } } diff --git a/src/agents/store/object-model.c b/src/agents/store/object-model.c index e9953f0..426b2a0 100644 --- a/src/agents/store/object-model.c +++ b/src/agents/store/object-model.c @@ -188,18 +188,24 @@ StorePropFixupNames(StorePropInfo *props, int propcount) } // fix up the filename in object based on its container -void +int StoreObjectFixUpFilename(StoreObject *container, StoreObject *object) { + int length; + // filename must be unique in the store! if (object->filename[0] == '\0') { // create a temp name if we weren't given one - snprintf(object->filename, MAX_FILE_NAME, "%s/bongo-" GUID_FMT, container->filename, object->guid); + length = snprintf(object->filename, sizeof(object->filename), + "%s/bongo-" GUID_FMT, container->filename, object->guid); + if (length < 0 || length >= (int)sizeof(object->filename)) { + object->filename[0] = '\0'; + return -1; + } } else { // need to prepand the filename of the parent collection // FIXME - should collections end with '/' ? char new_name[MAX_FILE_NAME]; - char *format = "%s/%s"; char *filepart; filepart = strrchr(object->filename, '/'); @@ -209,12 +215,20 @@ StoreObjectFixUpFilename(StoreObject *container, StoreObject *object) filepart++; // don't include separating / when prepending '/' - if (container->filename[1] == '\0') - format = "%s%s"; - - snprintf(new_name, MAX_FILE_NAME, format, container->filename, filepart); - strncpy(object->filename, new_name, MAX_FILE_NAME); + if (container->filename[1] == '\0') { + length = snprintf(new_name, sizeof(new_name), "%s%s", + container->filename, filepart); + } else { + length = snprintf(new_name, sizeof(new_name), "%s/%s", + container->filename, filepart); + } + if (length < 0 || length >= (int)sizeof(new_name)) { + return -1; + } + memcpy(object->filename, new_name, (size_t)length + 1); } + + return 0; } /** @@ -275,9 +289,11 @@ StoreObjectCreate(StoreClient *client, StoreObject *object) if (MsgSQLCommitTransaction(client->storedb)) goto abort; - if (object->filename[0] != '/') + if (object->filename[0] != '/') { // assume we need to fix the filename if it's not rooted. - StoreObjectFixUpFilename(&root_collection, object); + if (StoreObjectFixUpFilename(&root_collection, object) != 0) + goto abort; + } // create any files etc. needed if (STORE_IS_FOLDER(object->type)) { @@ -673,13 +689,14 @@ StoreOutputProperty(StoreClient *client, StorePropertyType type, const char *nam { if (value != NULL) { const char *out = value; + char outval[65]; int outlen; if ((type == STORE_PROP_CREATED) || (type == STORE_PROP_LASTMODIFIED)) { // need to turn an int into a date. struct tm tm; time_t tt; - char *endp, outval[65]; + char *endp; uint64_t intval; intval = strtol(value, &endp, 10); @@ -710,7 +727,7 @@ StoreObjectIterQueryBuilderPropResult(StoreClient *client, // start value below "col_id + 9" depends on how many columns // we pull back in our initial query; careful :) for (col_id = 0, result_id = 0; col_id < properties; col_id++) { - const StorePropInfo const *prop = g_ptr_array_index(builder->properties, col_id); + const StorePropInfo *prop = g_ptr_array_index(builder->properties, col_id); // ignore properties we don't want output; if (prop->output == FALSE) continue; @@ -1217,7 +1234,8 @@ StoreObjectRemoveCollection(StoreClient *client, StoreObject *collection) ret = MsgSQLPrepare(client->storedb, query, &stmt); if (ret == NULL) goto abort; - snprintf(path, MAX_FILE_NAME, "%s/", collection->filename); + if (snprintf(path, sizeof(path), "%s/", collection->filename) >= (int)sizeof(path)) + goto abort; MsgSQLBindString(&stmt, 1, path, FALSE); MsgSQLBindInt(&stmt, 2, strlen(path)); MsgSQLExecute(client->storedb, &stmt); @@ -1299,8 +1317,10 @@ StoreObjectRenameSubobjects(StoreClient *client, StoreObject *container, const c return 0; // nothing to do memset(&stmt, 0, sizeof(MsgSQLStatement)); - snprintf(path, MAX_FILE_NAME, "%s/", container->filename); - snprintf(newpath, MAX_FILE_NAME, "%s/", new_root); + if (snprintf(path, sizeof(path), "%s/", container->filename) >= (int)sizeof(path)) + return -1; + if (snprintf(newpath, sizeof(newpath), "%s/", new_root) >= (int)sizeof(newpath)) + return -1; MsgSQLBeginTransaction(client->storedb); @@ -1336,11 +1356,13 @@ int StoreObjectSetFilename(StoreObject *object, const StoreObject *collection, const char *filename) { if (filename == NULL) { - snprintf(object->filename, sizeof(object->filename), "%s/bongo-" GUID_FMT, - collection->filename, object->guid); + if (snprintf(object->filename, sizeof(object->filename), "%s/bongo-" GUID_FMT, + collection->filename, object->guid) >= (int)sizeof(object->filename)) + return -1; } else { - snprintf(object->filename, sizeof(object->filename), "%s/%s", - collection->filename, filename); + if (snprintf(object->filename, sizeof(object->filename), "%s/%s", + collection->filename, filename) >= (int)sizeof(object->filename)) + return -1; } return 0; } @@ -1784,16 +1806,16 @@ ACLCheckOnGUID(StoreClient *client, uint64_t guid, int prop) while ((status = MsgSQLResults(client->storedb, &stmt)) > 0) { int priv; int type; - int deny; + int row_denies; char *who; BOOL *grant; priv = MsgSQLResultInt(&stmt, 0); type = MsgSQLResultInt(&stmt, 1); - deny = MsgSQLResultInt(&stmt, 2); + row_denies = MsgSQLResultInt(&stmt, 2); MsgSQLResultTextPtr(&stmt, 3, &who); - if (deny == 0) { + if (row_denies == 0) { grant = &allow; } else { grant = &deny; diff --git a/src/agents/store/object-model.h b/src/agents/store/object-model.h index 51a01f4..2919575 100644 --- a/src/agents/store/object-model.h +++ b/src/agents/store/object-model.h @@ -80,7 +80,7 @@ int StoreObjectListACL(StoreClient *client, StoreObject *object); int StoreObjectSetProperty(StoreClient *client, StoreObject *object, StorePropInfo *prop); int StoreObjectSetExternalProperty(StoreClient *client, StoreObject *object, StorePropInfo *prop); int StoreObjectSetFilename(StoreObject *object, const StoreObject *collection, const char *filename); -void StoreObjectFixUpFilename(StoreObject *container, StoreObject *object); +int StoreObjectFixUpFilename(StoreObject *container, StoreObject *object); void StoreObjectUpdateModifiedTime(StoreObject *object); // iterators on store contents. diff --git a/src/agents/store/query-builder.c b/src/agents/store/query-builder.c index de8925c..3b677e5 100644 --- a/src/agents/store/query-builder.c +++ b/src/agents/store/query-builder.c @@ -30,9 +30,9 @@ QueryBuilderStart(QueryBuilder *builder) builder->order_direction = ORDER_NONE; builder->output_mode = MODE_COLLECTIONS; - builder->properties = g_ptr_array_new(); - builder->links = g_ptr_array_new(); - builder->parameters = g_ptr_array_new(); + builder->properties = g_ptr_array_new_with_free_func(g_free); + builder->links = g_ptr_array_new_with_free_func(g_free); + builder->parameters = g_ptr_array_new_with_free_func(g_free); return 0; } @@ -48,13 +48,10 @@ QueryBuilderFinish(QueryBuilder *builder) QueryParserFinish(&builder->external_parser); /* free all the properties, links and parameters */ - g_ptr_array_foreach (builder->properties, (GFunc)g_free, NULL); g_ptr_array_free(builder->properties, TRUE); - g_ptr_array_foreach (builder->links, (GFunc)g_free, NULL); g_ptr_array_free(builder->links, TRUE); - g_ptr_array_foreach (builder->parameters, (GFunc)g_free, NULL); g_ptr_array_free(builder->parameters, TRUE); } @@ -503,7 +500,8 @@ QueryExpressionToSQL(QueryBuilder *builder, struct expression *exp, BongoStringB val = malloc(len + 4); val[0] = '\''; val[1] = '%'; char *expression = (char*)exp->exp2; - for(int i=1; i < len; i++) val[i+1] = expression[i]; + for (int value_index = 1; value_index < len; value_index++) + val[value_index + 1] = expression[value_index]; val[len] = '%'; val[len+1] = '\''; val[len+2] = '\0'; diff --git a/src/agents/store/search.c b/src/agents/store/search.c index ff4107f..a307b08 100644 --- a/src/agents/store/search.c +++ b/src/agents/store/search.c @@ -73,10 +73,12 @@ SearchDocument(StoreClient *client, StoreObject *document, if (query->type == STORE_SEARCH_HEADERS) { break; } - /* nothing found in headers; fall through to STORE_SEARCH_BODY */ + goto search_body; } case STORE_SEARCH_BODY: { +search_body: + ; MimeReport *report = NULL; unsigned i; diff --git a/src/agents/store/store.c b/src/agents/store/store.c index ce919d9..f872577 100644 --- a/src/agents/store/store.c +++ b/src/agents/store/store.c @@ -183,8 +183,16 @@ SelectStore(StoreClient *client, char *user) // close current selected store if necessary UnselectStore(client); - snprintf(path, XPL_MAX_PATH, "%s/%s/", StoreAgent.store.rootDir, user); - strcpy(client->store, path); + if (snprintf(path, sizeof(path), "%s/%s/", + StoreAgent.store.rootDir, user) >= (int)sizeof(path)) { + Log(LOG_ERROR, "Store path is too long for user %s", user); + return -1; + } + if (strlen(path) >= sizeof(client->store)) { + Log(LOG_ERROR, "Store path does not fit the client buffer for user %s", user); + return -1; + } + memcpy(client->store, path, strlen(path) + 1); client->storeRoot = storeRoot; client->storeHash = BongoStringHash(user); diff --git a/src/agents/store/stored.c b/src/agents/store/stored.c index d288dda..5feab02 100644 --- a/src/agents/store/stored.c +++ b/src/agents/store/stored.c @@ -84,10 +84,15 @@ ProcessEntry(void *clientp, ccode = -1; } else if (!IS_MANAGER(client)) { /* issue nmap challenge */ - sprintf(client->authToken, "%x%s%x", - (unsigned int) XplGetThreadID(), - StoreAgent.server.host, - (unsigned int) time(NULL)); + unsigned char challenge[(sizeof(client->authToken) - 1) / 2]; + size_t challenge_index; + + XplRandomData(challenge, sizeof(challenge)); + for (challenge_index = 0; challenge_index < sizeof(challenge); challenge_index++) { + snprintf(client->authToken + challenge_index * 2, + sizeof(client->authToken) - challenge_index * 2, + "%02x", challenge[challenge_index]); + } ccode = ConnWriteF(client->conn, MSG4242AUTHREQUIRED, client->authToken); } else { ccode = ConnWriteF(client->conn, "1000 %s %s\r\n", diff --git a/src/agents/store/stored.h b/src/agents/store/stored.h index 8b478b0..01fe14f 100644 --- a/src/agents/store/stored.h +++ b/src/agents/store/stored.h @@ -185,7 +185,8 @@ struct _StoreClient { StoreToken *tokens; } principal; - char authToken[64]; + /* 256-bit challenge encoded as 64 hex digits plus the NUL terminator. */ + char authToken[65]; struct { StoreObject collection; diff --git a/src/apps/admin/bongo/admintool/DemoCommands.py b/src/apps/admin/bongo/admintool/DemoCommands.py index e80bac2..a9eac0c 100644 --- a/src/apps/admin/bongo/admintool/DemoCommands.py +++ b/src/apps/admin/bongo/admintool/DemoCommands.py @@ -1,5 +1,5 @@ -import bongo.external.simplejson as simplejson -import bongo.external.vobject as vobject +import json as simplejson +import vobject import logging import os import re @@ -9,10 +9,10 @@ import hashlib import urllib.request, urllib.parse, urllib.error import email -from email.MIMEText import MIMEText -from email.MIMEMessage import MIMEMessage -from email.MIMEMultipart import MIMEMultipart -from email.Message import Message +from email.mime.text import MIMEText +from email.mime.message import MIMEMessage +from email.mime.multipart import MIMEMultipart +from email.message import Message import bongo.table as table diff --git a/src/apps/config/config.c b/src/apps/config/config.c index ef3da4a..50e1343 100644 --- a/src/apps/config/config.c +++ b/src/apps/config/config.c @@ -73,8 +73,6 @@ GetInstallParameters(void){ config.domains = BongoJsonNodeNewArray(g_array_new(FALSE, FALSE, sizeof(char *))); while (1) { char *tmp = NULL; - BongoJsonNode *domain; - GetInteractiveData(_("Mail Domains (one per line, blank line to finish):"), &tmp, ""); if (tmp == NULL) { return; @@ -133,8 +131,8 @@ SetStoreConfigurationModifications(StoreClient *client) { /* add the default configuration document for each domain */ for(x=0;xlen;x++) { - BongoJsonNode *current = BongoJsonArrayGet(domains, x); - PutOrReplaceConfig(client, "/config/aliases", BongoJsonNodeAsString(current), default_config, default_config_len); + BongoJsonNode *domain_node = BongoJsonArrayGet(domains, x); + PutOrReplaceConfig(client, "/config/aliases", BongoJsonNodeAsString(domain_node), default_config, default_config_len); } /* put back the old domains array so that the free can sanely free it */ @@ -181,7 +179,7 @@ void InitializeDataArea(void) { MsgApiDirectory dir; - const unsigned char *unpriv_user; + const char *unpriv_user; uid_t uid = 0; gid_t gid = 0; @@ -315,25 +313,45 @@ GetInteractiveData(char *description, char **data, char *def) { #define CERTSIZE 10240 +static BOOL +WriteCryptoFile(const char *path, const unsigned char *data, size_t length, mode_t mode) +{ + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, mode); + FILE *stream; + BOOL success; + + if (fd < 0) + return FALSE; + if (fchmod(fd, mode) != 0) { + close(fd); + return FALSE; + } + stream = fdopen(fd, "wb"); + if (stream == NULL) { + close(fd); + return FALSE; + } + success = fwrite(data, 1, length, stream) == length && fclose(stream) == 0; + return success; +} + BOOL GenerateCryptoData() { - static gnutls_dh_params_t dh_params; - static gnutls_rsa_params_t rsa_params; - gnutls_x509_privkey key; - gnutls_x509_crt certificate; - unsigned char dhdata[CERTSIZE], rsadata[CERTSIZE], private_key[CERTSIZE], public_cert[CERTSIZE]; + gnutls_x509_privkey_t key = NULL; + gnutls_x509_crt_t certificate = NULL; + unsigned char private_key[CERTSIZE], public_cert[CERTSIZE]; + unsigned char serial[16]; int ret; - size_t dsize = sizeof(unsigned char) * (CERTSIZE); - int cert_version = 1; - mode_t private = S_IRUSR | S_IWUSR; - FILE *params; + size_t dsize; + BOOL success = FALSE; static const char *org = "Bongo Project"; - static const char *unit = "Fake Certs Dept."; - static const char *state = "None"; - static const char *country = "US"; - static const char *blank = ""; + const char *hostname = config.dns ? config.dns : "localhost"; - gnutls_global_init(); + ret = gnutls_global_init(); + if (ret < 0) { + XplConsolePrintf(_("ERROR: Couldn't initialize TLS. %s\n"), gnutls_strerror(ret)); + return FALSE; + } // hack: we need to ensure the files below can be saved... if (0 != mkdir(XPL_DEFAULT_DBF_DIR, 0755)) { @@ -345,155 +363,81 @@ GenerateCryptoData() { // save a random seed for faster Xpl startup in future. XplSaveRandomSeed(); - // various magic params - gnutls_dh_params_init(&dh_params); - gnutls_dh_params_generate2(dh_params, 1024); - dsize = CERTSIZE; - gnutls_dh_params_export_pkcs3 (dh_params, GNUTLS_X509_FMT_PEM, dhdata, &dsize); - params = fopen(XPL_DEFAULT_DHPARAMS_PATH, "w"); - chmod(XPL_DEFAULT_DHPARAMS_PATH, private); - if (params) { - fprintf(params, "%s\n", dhdata); - fclose(params); - } else { - gnutls_dh_params_deinit(dh_params); - gnutls_global_deinit(); - XplConsolePrintf(_("ERROR: Couldn't write DH params to %s\n"), XPL_DEFAULT_DHPARAMS_PATH); - return FALSE; - } - - gnutls_rsa_params_init(&rsa_params); - gnutls_rsa_params_generate2(rsa_params, 512); - dsize = CERTSIZE; - gnutls_rsa_params_export_pkcs1 (rsa_params, GNUTLS_X509_FMT_PEM, rsadata, &dsize); - params = fopen(XPL_DEFAULT_RSAPARAMS_PATH, "w"); - if (params) { - chmod(XPL_DEFAULT_RSAPARAMS_PATH, private); - fprintf(params, "%s\n", dhdata); - fclose(params); - } else { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); - XplConsolePrintf(_("ERROR: Couldn't write RSA params to %s\n"), XPL_DEFAULT_DHPARAMS_PATH); - return FALSE; - } - // create private key ret = gnutls_x509_privkey_init(&key); + if (ret < 0) + goto tls_error; + ret = gnutls_x509_privkey_generate(key, GNUTLS_PK_RSA, 3072, 0); if (ret < 0) { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); - XplConsolePrintf(_("ERROR: %s\n"), gnutls_strerror (ret)); - return FALSE; - } - ret = gnutls_x509_privkey_generate(key, GNUTLS_PK_RSA, 1024, 0); - if (ret < 0) { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); XplConsolePrintf(_("ERROR: Couldn't create private key. %s\n"), gnutls_strerror(ret)); - return FALSE; + goto cleanup; } dsize = CERTSIZE; - ret = gnutls_x509_privkey_export(key, GNUTLS_X509_FMT_PEM, &private_key, &dsize); + ret = gnutls_x509_privkey_export(key, GNUTLS_X509_FMT_PEM, private_key, &dsize); if (ret < 0) { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); XplConsolePrintf(_("ERROR: Couldn't export private key. %s\n"), gnutls_strerror(ret)); - XplConsolePrintf(_(" Size: %zd, Ret: %d\n"), dsize, ret); - return FALSE; - } else { - params = fopen(XPL_DEFAULT_KEY_PATH, "w"); - if (params) { - chmod(XPL_DEFAULT_KEY_PATH, private); - fprintf(params, "%s\n", private_key); - fclose(params); - } else { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); - XplConsolePrintf(_("ERROR: Couldn't write private key to %s\n"), XPL_DEFAULT_KEY_PATH); - return FALSE; - } + goto cleanup; + } + if (!WriteCryptoFile(XPL_DEFAULT_KEY_PATH, private_key, dsize, S_IRUSR | S_IWUSR)) { + XplConsolePrintf(_("ERROR: Couldn't write private key to %s\n"), XPL_DEFAULT_KEY_PATH); + goto cleanup; } // create certificate ret = gnutls_x509_crt_init(&certificate); - if (ret < 0) { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); - XplConsolePrintf(_("ERROR: %s\n"), gnutls_strerror(ret)); - return FALSE; - } + if (ret < 0) + goto tls_error; gnutls_x509_crt_set_activation_time(certificate, time(NULL)); - gnutls_x509_crt_set_expiration_time(certificate, time(NULL) + (700 * 24 * 60 * 60)); + gnutls_x509_crt_set_expiration_time(certificate, time(NULL) + (10 * 365 * 24 * 60 * 60)); gnutls_x509_crt_set_key(certificate, key); - gnutls_x509_crt_set_version(certificate, 1); - gnutls_x509_crt_set_serial(certificate, &cert_version, sizeof(int)); + gnutls_x509_crt_set_version(certificate, 3); + XplRandomData(serial, sizeof(serial)); + serial[0] &= 0x7f; + gnutls_x509_crt_set_serial(certificate, serial, sizeof(serial)); gnutls_x509_crt_set_dn_by_oid(certificate, GNUTLS_OID_X520_ORGANIZATION_NAME, 0, org, strlen(org)); gnutls_x509_crt_set_dn_by_oid(certificate, - GNUTLS_OID_X520_ORGANIZATIONAL_UNIT_NAME, 0, unit, strlen(unit)); - gnutls_x509_crt_set_dn_by_oid(certificate, - GNUTLS_OID_X520_STATE_OR_PROVINCE_NAME, 0, state, strlen(state)); - gnutls_x509_crt_set_dn_by_oid(certificate, - GNUTLS_OID_X520_COUNTRY_NAME, 0, country, strlen(country)); - gnutls_x509_crt_set_dn_by_oid(certificate, - GNUTLS_OID_X520_LOCALITY_NAME, 0, blank, strlen(blank)); - gnutls_x509_crt_set_dn_by_oid(certificate, - GNUTLS_OID_X520_COMMON_NAME, 0, config.dns, strlen(config.dns)); - gnutls_x509_crt_set_dn_by_oid(certificate, - GNUTLS_OID_LDAP_UID, 0, blank, strlen(blank)); + GNUTLS_OID_X520_COMMON_NAME, 0, hostname, strlen(hostname)); gnutls_x509_crt_set_subject_alternative_name (certificate, - GNUTLS_SAN_DNSNAME, config.dns); + GNUTLS_SAN_DNSNAME, hostname); gnutls_x509_crt_set_key_usage(certificate, GNUTLS_KEY_DIGITAL_SIGNATURE | GNUTLS_KEY_DATA_ENCIPHERMENT); ret = GNUTLS_SELF_SIGN(certificate, key); if (ret < 0) { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); XplConsolePrintf(_("ERROR: Couldn't self-sign certificate. %s\n"), gnutls_strerror(ret)); - return FALSE; + goto cleanup; } dsize = CERTSIZE; - ret = gnutls_x509_crt_export(certificate, GNUTLS_X509_FMT_PEM, &public_cert, &dsize); + ret = gnutls_x509_crt_export(certificate, GNUTLS_X509_FMT_PEM, public_cert, &dsize); if (ret < 0) { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); XplConsolePrintf(_("ERROR: Couldn't create certificate. %s\n"), gnutls_strerror(ret)); - return FALSE; + goto cleanup; } - params = fopen(XPL_DEFAULT_CERT_PATH, "w"); - if (params) { - fprintf(params, "%s\n", public_cert); - fclose(params); - } else { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); + if (!WriteCryptoFile(XPL_DEFAULT_CERT_PATH, public_cert, dsize, + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) { XplConsolePrintf(_("ERROR: Couldn't write certificate to %s\n"), XPL_DEFAULT_CERT_PATH); - return FALSE; + goto cleanup; } - - gnutls_x509_crt_deinit(certificate); - gnutls_x509_privkey_deinit(key); - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); + + success = TRUE; + goto cleanup; + +tls_error: + XplConsolePrintf(_("ERROR: %s\n"), gnutls_strerror(ret)); +cleanup: + if (certificate) + gnutls_x509_crt_deinit(certificate); + if (key) + gnutls_x509_privkey_deinit(key); gnutls_global_deinit(); - - XplConsolePrintf(_("Crypto data generated.\n")); - - return TRUE; + + if (success) + XplConsolePrintf(_("Crypto data generated.\n")); + return success; } void @@ -701,24 +645,24 @@ main(int argc, char *argv[]) { if (next_arg >= argc) { XplConsolePrintf(_("USAGE : user [add|password|list] []\n")); } else { - char *command = argv[next_arg++]; + char *user_command = argv[next_arg++]; - if (!strncmp(command, "list", 4)) { + if (!strncmp(user_command, "list", 4)) { UserList(); } else { char *username; if (next_arg >= argc) { - XplConsolePrintf(_("USAGE : user %s = + (int)sizeof(currentpath) || + snprintf(fullpath, sizeof(fullpath), "CREATE %s\r\n", currentpath) >= + (int)sizeof(fullpath)) { + XplConsolePrintf(_("ERROR: Configuration path is too long\n")); + fclose(config); + return FALSE; + } result = NMAPSimpleCommand(client, fullpath); if ((result==1000) || (result==4226)) { @@ -80,6 +86,12 @@ ImportSystemBackupFile(StoreClient *client, const char *path) } delim = strrchr(filename, '/'); + if (delim == NULL) { + MemFree(file); + XplConsolePrintf(_("ERROR: Invalid configuration archive path: %s\n"), filename); + fclose(config); + return FALSE; + } *delim = 0; snprintf(fullpath, 108, "/%s", filename); if (!PutOrReplaceConfig(client, fullpath, diff --git a/src/apps/manager/manager.c b/src/apps/manager/manager.c index 2a6b14b..7f4fb67 100644 --- a/src/apps/manager/manager.c +++ b/src/apps/manager/manager.c @@ -561,8 +561,11 @@ ParentSignalHandler(int signo) switch (signo) { case SIGCHLD: ChildError = TRUE; + Exiting = TRUE; + break; case SIGUSR1: Exiting = TRUE; + break; } } diff --git a/src/apps/storetool/CMakeLists.txt b/src/apps/storetool/CMakeLists.txt index 65ec562..08e20c5 100644 --- a/src/apps/storetool/CMakeLists.txt +++ b/src/apps/storetool/CMakeLists.txt @@ -1,6 +1,8 @@ install(DIRECTORY ./bongo DESTINATION "${PYTHON_SITEPACKAGES_PATH}/" - FILES_MATCHING PATTERN "*.py" + FILES_MATCHING + PATTERN "__pycache__" EXCLUDE + PATTERN "*.py" PATTERN ".svn" EXCLUDE ) diff --git a/src/apps/storetool/bongo/storetool/CalendarCommands.py b/src/apps/storetool/bongo/storetool/CalendarCommands.py index 871c240..7c8bee5 100644 --- a/src/apps/storetool/bongo/storetool/CalendarCommands.py +++ b/src/apps/storetool/bongo/storetool/CalendarCommands.py @@ -1,5 +1,5 @@ -import bongo.external.simplejson as simplejson -import bongo.external.vobject as vobject +import json as simplejson +import vobject import logging import os import re @@ -8,10 +8,10 @@ import random import hashlib import email -from email.MIMEText import MIMEText -from email.MIMEMessage import MIMEMessage -from email.MIMEMultipart import MIMEMultipart -from email.Message import Message +from email.mime.text import MIMEText +from email.mime.message import MIMEMessage +from email.mime.multipart import MIMEMultipart +from email.message import Message import bongo.table as table diff --git a/src/apps/storetool/bongo/storetool/ContactCommands.py b/src/apps/storetool/bongo/storetool/ContactCommands.py index ef838d3..7584d82 100644 --- a/src/apps/storetool/bongo/storetool/ContactCommands.py +++ b/src/apps/storetool/bongo/storetool/ContactCommands.py @@ -1,5 +1,5 @@ -import bongo.external.simplejson as simplejson -import bongo.external.vobject as vobject +import json as simplejson +import vobject import bongo.table as table import logging diff --git a/src/apps/storetool/bongo/storetool/ExportMailbox.py b/src/apps/storetool/bongo/storetool/ExportMailbox.py index e5ece62..54c8409 100644 --- a/src/apps/storetool/bongo/storetool/ExportMailbox.py +++ b/src/apps/storetool/bongo/storetool/ExportMailbox.py @@ -4,29 +4,27 @@ import socket import errno import io import mailbox -import email, email.Generator +import email +from email.generator import Generator +from email.utils import parsedate_tz, mktime_tz import sys from time import time, mktime, strptime, ctime, sleep -from rfc822 import Message, parsedate_tz, mktime_tz - -class MboxMailbox(mailbox.PortableUnixMailbox): +class MboxMailbox: def __init__(self,file): - mailbox.PortableUnixMailbox.__init__(self, file, email.message_from_file) self.boxname = file self.content = "" def add(self, msg): fp = io.StringIO() - g = email.Generator.Generator(fp, mangle_from_=False) + g = Generator(fp, mangle_from_=False) envelope = email.message_from_string(msg) g.flatten(envelope, unixfrom=True) self.content += "%s" % fp.getvalue() def write(self): - outfile=open(self.boxname, "wb") - outfile.write("%s\n" % self.content) - outfile.close() + with open(self.boxname, "w", encoding="utf-8", newline="") as outfile: + outfile.write("%s\n" % self.content) class MaildirMailbox(mailbox.Maildir): def __init__(self,file): @@ -35,14 +33,8 @@ class MaildirMailbox(mailbox.Maildir): else: self.boxname = file + "/" - while 1: - try: - os.makedirs(self.boxname + "cur") - os.makedirs(self.boxname + "new") - os.makedirs(self.boxname + "tmp") - except OSError as err: - if err.errno == errno.EEXIST: - break + for directory in ("cur", "new", "tmp"): + os.makedirs(os.path.join(self.boxname, directory), exist_ok=True) mailbox.Maildir.__init__(self, file, email.message_from_file) def tmp_open(self): @@ -83,12 +75,14 @@ class MaildirMailbox(mailbox.Maildir): def write_message(self, raw_msg, fd, tmp): try: + if isinstance(raw_msg, str): + raw_msg = raw_msg.encode("utf-8") os.write(fd, raw_msg) os.fsync(fd) os.close(fd) except OSError as err: os.unlink(tmp) - raise Error("unable to fsync() or close() temp file: %s" % err) + raise OSError("unable to fsync() or close() temp file: %s" % err) def finish_message(self, tmp, dir, info, dtime): @@ -109,7 +103,7 @@ class MaildirMailbox(mailbox.Maildir): try: fp = io.StringIO(raw_msg) - message = Message(fp) + message = email.message_from_file(fp) (dir, info) = self.status(message) dtime = self.get_delivery_time(message) self.write_message(raw_msg, fd, tmp) diff --git a/src/apps/storetool/bongo/storetool/MailCommands.py b/src/apps/storetool/bongo/storetool/MailCommands.py index c759815..d2074f0 100644 --- a/src/apps/storetool/bongo/storetool/MailCommands.py +++ b/src/apps/storetool/bongo/storetool/MailCommands.py @@ -145,19 +145,15 @@ class MailImapCommand(Command): collection = "/mail/" + folder store.Create(collection, existingOk=True) - try: - M=imaplib.IMAP4(imap_server, imap_port) - M.login(imap_username, imap_password) - M.select(imap_folder) - - try: - typ, data = M.search(None, 'ALL') - for num in data[0].split(): - store.WriteImap(collection, DocTypes.Mail, M, num) + M=imaplib.IMAP4(imap_server, imap_port) + M.login(imap_username, imap_password) + M.select(imap_folder) - finally: - M.close() - M.logout() + try: + typ, data = M.search(None, 'ALL') + for num in data[0].split(): + store.WriteImap(collection, DocTypes.Mail, M, num) finally: - return + M.close() + M.logout() diff --git a/src/libs/cal/bongo-cal-instance.c b/src/libs/cal/bongo-cal-instance.c index dc14ed3..565df88 100644 --- a/src/libs/cal/bongo-cal-instance.c +++ b/src/libs/cal/bongo-cal-instance.c @@ -394,7 +394,7 @@ ReadRecur(BongoJsonObject *obj, BongoCalRule *recur) recur->bymonth = IntArrayToList(arrayVal, 1, 12); for (l = recur->bymonth; l != NULL; l = l->next) { - l->data--; + l->data = BongoListIntToVoid(BongoListVoidToInt(l->data) - 1); } } diff --git a/src/libs/cal/bongo-cal-time.c b/src/libs/cal/bongo-cal-time.c index e6f0b03..aa37ab8 100644 --- a/src/libs/cal/bongo-cal-time.c +++ b/src/libs/cal/bongo-cal-time.c @@ -463,7 +463,7 @@ BongoCalTimeToStringConcise(BongoCalTime t, char *buffer, int buflen) { uint64_t tnow, twhen; BongoCalTime now; - char tmbuf[10]; + char tmbuf[32]; now = BongoCalTimeNow(NULL); tnow = BongoCalTimeAsUint64(now); @@ -510,7 +510,7 @@ BongoCalTimeAdjust(BongoCalTime t, int days, int hours, int minutes, int seconds { int second, minute, hour, day; int minutesOverflow = 0, hoursOverflow = 0, daysOverflow = 0, yearsOverflow = 0; - int daysInMonth; + int monthDays; ASSERTTZ(t); @@ -559,8 +559,8 @@ BongoCalTimeAdjust(BongoCalTime t, int days, int hours, int minutes, int seconds day = t.day + days + daysOverflow; if (day > 0) { while (1) { - daysInMonth = BongoCalDaysInMonth(t.month, t.year); - if (day <= daysInMonth) { + monthDays = BongoCalDaysInMonth(t.month, t.year); + if (day <= monthDays) { break; } t.month++; @@ -569,7 +569,7 @@ BongoCalTimeAdjust(BongoCalTime t, int days, int hours, int minutes, int seconds t.month = 0; } - day -= daysInMonth; + day -= monthDays; } } else { while (day <= 0) { @@ -769,4 +769,3 @@ BongoCalDurationParseIcal(const char *str) error: return emptyDuration; } - diff --git a/src/libs/connio/connio.c b/src/libs/connio/connio.c index f89d534..c672cfa 100644 --- a/src/libs/connio/connio.c +++ b/src/libs/connio/connio.c @@ -50,46 +50,60 @@ #ifdef HAVE_NETINET_IN_H #include #endif +#include #include #include "conniop.h" ConnIOGlobals ConnIO; -gnutls_dh_params_t dh_params; -gnutls_rsa_params_t rsa_params; - BOOL -__gnutls_new(Connection *conn, bongo_ssl_context *context, gnutls_connection_end_t con_end) { +__gnutls_new(Connection *conn, bongo_ssl_context *context, unsigned int flags) { int ccode; conn->ssl.context = NULL; conn->ssl.credentials = NULL; - ccode = gnutls_init(&conn->ssl.context, con_end); - if (ccode) { + ccode = gnutls_init(&conn->ssl.context, flags); + if (ccode < 0) { return FALSE; } - if (con_end == GNUTLS_SERVER) { - ccode = gnutls_set_default_export_priority(conn->ssl.context); + /* Secure by default. The compatibility profile is deliberately opt-in + * for legacy clients and still offers modern protocol versions first. */ + ccode = gnutls_priority_set_direct( + conn->ssl.context, + (context->options & SSL_ALLOW_LEGACY_PROTOCOLS) + ? "LEGACY:-VERS-ALL:+VERS-TLS1.3:+VERS-TLS1.2:+VERS-TLS1.1:+VERS-TLS1.0:+VERS-SSL3.0" + : "NORMAL:-VERS-ALL:+VERS-TLS1.3:+VERS-TLS1.2", + NULL); + if (ccode < 0) { + gnutls_deinit(conn->ssl.context); + conn->ssl.context = NULL; + return FALSE; + } - /* store in the credetials loaded earler */ + if (flags & GNUTLS_SERVER) { ccode = gnutls_credentials_set(conn->ssl.context, GNUTLS_CRD_CERTIFICATE, context->cert_cred); } else { - /* defaults are ok here */ - gnutls_set_default_priority (conn->ssl.context); + ccode = gnutls_certificate_allocate_credentials(&conn->ssl.credentials); + if (ccode >= 0) { + ccode = gnutls_certificate_set_x509_system_trust(conn->ssl.credentials); + } + if (ccode >= 0) { + ccode = gnutls_credentials_set(conn->ssl.context, GNUTLS_CRD_CERTIFICATE, + conn->ssl.credentials); + } + } - /* store the priority for x509 or openpgp out there - * i doubt that openpgp will be used but perhaps there is a server that supports it */ - gnutls_certificate_allocate_credentials (&conn->ssl.credentials); - gnutls_certificate_set_x509_trust_file (conn->ssl.credentials, XPL_DEFAULT_CERT_PATH, GNUTLS_X509_FMT_PEM); - - /* set the empty credentials in the session */ - gnutls_credentials_set (conn->ssl.context, GNUTLS_CRD_CERTIFICATE, conn->ssl.credentials); - - /* require at least 512 dh bits */ - gnutls_dh_set_prime_bits(conn->ssl.context, 512); + if (ccode < 0) { + if (conn->ssl.credentials) { + gnutls_certificate_free_credentials(conn->ssl.credentials); + conn->ssl.credentials = NULL; + } + gnutls_deinit(conn->ssl.context); + conn->ssl.context = NULL; + return FALSE; } return TRUE; @@ -101,55 +115,9 @@ ConnStartup(unsigned long TimeOut) /** * Start up the connio library - must be initialised before use */ - FILE *genparams; - /* initialize the gnutls library */ - gnutls_global_init(); - - /* try to load dh data out of a file */ - gnutls_dh_params_init(&dh_params); - genparams = fopen(XPL_DEFAULT_DHPARAMS_PATH, "r"); - if (genparams) { - unsigned char tmpdata[2048]; - gnutls_datum dh_parameters; - - dh_parameters.size = fread(tmpdata, 1, sizeof(tmpdata)-1, genparams); - /* null the last byte */ - tmpdata[dh_parameters.size] = 0x00; - - fclose(genparams); - - /* store the data in the datum so that we can initialize it */ - dh_parameters.data = tmpdata; - - /* import the key */ - gnutls_dh_params_import_pkcs3(dh_params, &dh_parameters, GNUTLS_X509_FMT_PEM); - } else { - /* generate dh stuff */ - gnutls_dh_params_generate2(dh_params, 1024); - } - - /* try to load the rsa data out of a file */ - gnutls_rsa_params_init(&rsa_params); - genparams = fopen(XPL_DEFAULT_RSAPARAMS_PATH, "r"); - if (genparams) { - unsigned char tmpdata[2048]; - gnutls_datum rsa_parameters; - - rsa_parameters.size = fread(tmpdata, 1, sizeof(tmpdata)-1, genparams); - /* null the last byte */ - tmpdata[rsa_parameters.size] = 0x00; - - fclose(genparams); - - /* store the data in the datum so that we can initialize it */ - rsa_parameters.data = tmpdata; - - /* import the key */ - gnutls_rsa_params_import_pkcs1(rsa_params, &rsa_parameters, GNUTLS_X509_FMT_PEM); - } else { - /* generate rsa stuff */ - gnutls_rsa_params_generate2(rsa_params, 512); + if (gnutls_global_init() < 0) { + return FALSE; } XplOpenLocalSemaphore(ConnIO.allocated.sem, 0); @@ -185,7 +153,16 @@ ConnSSLContextAlloc(ConnSSLConfiguration *ConfigSSL) int ccode; /* get me the context */ + if (ConfigSSL == NULL || ConfigSSL->certificate.file == NULL || + ConfigSSL->key.file == NULL) { + return NULL; + } result = MemMalloc(sizeof(bongo_ssl_context)); + if (result == NULL) { + return NULL; + } + memset(result, 0, sizeof(*result)); + result->options = ConfigSSL->options; /* allocate the credential holder */ ccode = gnutls_certificate_allocate_credentials(&(result->cert_cred)); @@ -194,11 +171,22 @@ ConnSSLContextAlloc(ConnSSLConfiguration *ConfigSSL) return(NULL); } - ccode = gnutls_certificate_set_x509_key_file(result->cert_cred, ConfigSSL->certificate.file, ConfigSSL->key.file, ConfigSSL->key.type); + ccode = gnutls_certificate_set_x509_key_file( + result->cert_cred, ConfigSSL->certificate.file, + ConfigSSL->key.file, ConfigSSL->key.type); + if (ccode < 0) { + gnutls_certificate_free_credentials(result->cert_cred); + MemFree(result); + return NULL; + } - /* store both the rsa and dh data in the creds */ - gnutls_certificate_set_dh_params(result->cert_cred, dh_params); - gnutls_certificate_set_rsa_export_params(result->cert_cred, rsa_params); + ccode = gnutls_certificate_set_known_dh_params( + result->cert_cred, GNUTLS_SEC_PARAM_HIGH); + if (ccode < 0) { + gnutls_certificate_free_credentials(result->cert_cred); + MemFree(result); + return NULL; + } return result; } @@ -397,11 +385,11 @@ ConnConnectEx(Connection *conn, struct sockaddr *saddr, socklen_t slen, bongo_ss int ConnEncrypt(Connection *conn, bongo_ssl_context *context) { - int ccode; + int ccode = 1; register Connection *c = conn; if (__gnutls_new(c, context, GNUTLS_CLIENT) != FALSE) { - setsockopt(c->socket, IPPROTO_TCP, 1, (unsigned char *)&ccode, sizeof(ccode)); + setsockopt(c->socket, IPPROTO_TCP, TCP_NODELAY, &ccode, sizeof(ccode)); gnutls_transport_set_ptr(c->ssl.context, (gnutls_transport_ptr_t) (long) c->socket); ccode = gnutls_handshake (c->ssl.context); @@ -424,18 +412,17 @@ ConnEncrypt(Connection *conn, bongo_ssl_context *context) BOOL ConnNegotiate(Connection *conn, bongo_ssl_context *context) { - int ccode; + int ccode = 1; register Connection *c = conn; if (c->ssl.enable == FALSE) { - ccode = 1; - setsockopt(c->socket, IPPROTO_TCP, 1, (unsigned char *)&ccode, sizeof(ccode)); + setsockopt(c->socket, IPPROTO_TCP, TCP_NODELAY, &ccode, sizeof(ccode)); return(TRUE); } if (__gnutls_new(conn, context, GNUTLS_SERVER) != FALSE) { - setsockopt(c->socket, IPPROTO_TCP, 1, (unsigned char *)&ccode, sizeof(ccode)); + setsockopt(c->socket, IPPROTO_TCP, TCP_NODELAY, &ccode, sizeof(ccode)); gnutls_transport_set_ptr(c->ssl.context, (gnutls_transport_ptr_t) (long) c->socket); ccode = gnutls_handshake(c->ssl.context); @@ -571,8 +558,6 @@ ConnShutdown(void) XplCloseLocalSemaphore(ConnIO.allocated.sem); - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); gnutls_global_deinit(); return; @@ -691,7 +676,7 @@ ConnRead(Connection *Conn, char *Dest, int Length) /* bytes read which can only be Count on success. */ int -ConnReadCount(Connection *Conn, char *Dest, int Count) +ConnReadCount(Connection *Conn, void *Dest, int Count) { size_t buffered; size_t remaining = Count; @@ -1473,7 +1458,7 @@ ConnReadToConnUntilEOS(Connection *Src, Connection *Dest) } int -ConnWrite(Connection *Conn, const char *Buffer, int Length) +ConnWrite(Connection *Conn, const void *Buffer, int Length) { const char *b; size_t i; @@ -1719,4 +1704,3 @@ XplPrintIPAddress(char *buffer, int bufLen, unsigned long address) ((address>>24) & 0xff)); #endif } - diff --git a/src/libs/connio/conniop.h b/src/libs/connio/conniop.h index 9a9b03d..b7c4b53 100644 --- a/src/libs/connio/conniop.h +++ b/src/libs/connio/conniop.h @@ -48,7 +48,7 @@ typedef struct _ConnIO { extern ConnIOGlobals ConnIO; -BOOL __gnutls_new(Connection *conn, bongo_ssl_context *context, gnutls_connection_end_t con_end); +BOOL __gnutls_new(Connection *conn, bongo_ssl_context *context, unsigned int flags); long ConnAppendToAllocatedBuffer(const char *source, const long size, char **buffer, unsigned long start_of_buffer, unsigned long *buffersize); diff --git a/src/libs/connio/nmap.c b/src/libs/connio/nmap.c index 2199181..ab71059 100644 --- a/src/libs/connio/nmap.c +++ b/src/libs/connio/nmap.c @@ -34,6 +34,7 @@ #include #include #include +#include /* prototypes */ BOOL NMAPAuthenticate(Connection *conn, char *response, int length); @@ -1300,7 +1301,7 @@ NMAPReadConfigFile(const char *file, char **output) } if (count == 0) { // take into account "empty" files - *output = '\0'; + (*output)[0] = '\0'; } } retcode = TRUE; @@ -1420,7 +1421,7 @@ NMAPConnectQueueEx(char *address, struct sockaddr_in *addr, TraceDestination *de BOOL NMAPEncrypt(Connection *conn, char *response, int length, BOOL force) { - int ccode; + int ccode = 1; BOOL result; Connection *c = conn; @@ -1430,7 +1431,7 @@ NMAPEncrypt(Connection *conn, char *response, int length, BOOL force) } if (NMAPLibrary.context) { - setsockopt(c->socket, IPPROTO_TCP, 1, (unsigned char *)&ccode, sizeof(ccode)); + setsockopt(c->socket, IPPROTO_TCP, TCP_NODELAY, &ccode, sizeof(ccode)); if (((ccode = ConnWrite(c, "CAPA\r\n", 6)) != -1) && ((ccode = ConnFlush(c)) != -1)) { diff --git a/src/libs/json/json.c b/src/libs/json/json.c index 1a62f9c..f050ba7 100644 --- a/src/libs/json/json.c +++ b/src/libs/json/json.c @@ -33,18 +33,19 @@ BongoJsonNode * BongoJsonJPath(BongoJsonNode *root, const char *path) { char rule[50]; - char *select, *nextrule; + char *select; + const char *nextrule; size_t rulelen; BongoJsonResult result; nextrule = strchr(path, '/'); if (! nextrule) { - strncpy(rule, path, 49); - rulelen = 49; + rulelen = min(strlen(path), sizeof(rule) - 1); + memcpy(rule, path, rulelen); } else { - strncpy(rule, path, min(nextrule - path, 49)); - rulelen = nextrule - path; + rulelen = min((size_t)(nextrule - path), sizeof(rule) - 1); + memcpy(rule, path, rulelen); if (rulelen < strlen(path)) { // there's another rule after this nextrule++; @@ -952,4 +953,3 @@ BongoJsonObjectResolve(BongoJsonObject *obj, const char *path, BongoJsonNode **o return res; } - diff --git a/src/libs/json/parse.c b/src/libs/json/parse.c index 7e705d2..8ac2294 100644 --- a/src/libs/json/parse.c +++ b/src/libs/json/parse.c @@ -654,18 +654,18 @@ BongoJsonQuoteStringToStringBuilder(const char *str, BongoStringBuilder *sb) BongoStringBuilderAppendChar(sb, '"'); for (i = 0; i < len; i++) { - int len; + int char_len; c = str[i]; - UTF8_LENGTH(c, len); + UTF8_LENGTH(c, char_len); - if (len < 2) { + if (char_len < 2) { if ((c == '\\') || (c == '"')) { - char str[2]; - str[0] = '\\'; - str[1] = c; - BongoStringBuilderAppendN(sb, str, 2); + char escaped[2]; + escaped[0] = '\\'; + escaped[1] = c; + BongoStringBuilderAppendN(sb, escaped, 2); } else if (c == '\b') { BongoStringBuilderAppend(sb, "\\b"); } else if (c == '\t') { @@ -682,8 +682,8 @@ BongoJsonQuoteStringToStringBuilder(const char *str, BongoStringBuilder *sb) } } } else { - BongoStringBuilderAppendN(sb, str + i, len); - i += len - 1; + BongoStringBuilderAppendN(sb, str + i, char_len); + i += char_len - 1; } } diff --git a/src/libs/mailauth/CMakeLists.txt b/src/libs/mailauth/CMakeLists.txt new file mode 100644 index 0000000..32f815e --- /dev/null +++ b/src/libs/mailauth/CMakeLists.txt @@ -0,0 +1,48 @@ +add_library(bongomailauth + mailauth.c + spf.c + dkim.c + dmarc.c + srs.c +) + +target_include_directories(bongomailauth + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${PSL_INCLUDE_DIRS} +) + +target_link_libraries(bongomailauth + PUBLIC + SPF2::SPF2 + OpenDKIM::OpenDKIM + OpenDMARC::OpenDMARC + SRS2::SRS2 + ${PSL_LIBRARIES} +) + +install(TARGETS bongomailauth DESTINATION ${LIB_INSTALL_DIR}) + +if(BUILD_TESTING) + add_executable(mailauth-srs-test tests/srs-test.c) + target_link_libraries(mailauth-srs-test PRIVATE bongomailauth) + add_test(NAME mailauth-srs COMMAND mailauth-srs-test) + + add_executable(mailauth-spf-test tests/spf-test.c) + target_link_libraries(mailauth-spf-test PRIVATE bongomailauth) + add_test(NAME mailauth-spf COMMAND mailauth-spf-test) + + find_program(OPENSSL_EXECUTABLE openssl) + if(OPENSSL_EXECUTABLE) + set(DKIM_TEST_KEY "${CMAKE_CURRENT_BINARY_DIR}/dkim-test.private") + add_test(NAME mailauth-dkim-key + COMMAND ${OPENSSL_EXECUTABLE} genpkey -quiet -algorithm RSA + -pkeyopt rsa_keygen_bits:2048 -out ${DKIM_TEST_KEY}) + set_tests_properties(mailauth-dkim-key PROPERTIES FIXTURES_SETUP dkim_key) + + add_executable(mailauth-dkim-test tests/dkim-test.c) + target_link_libraries(mailauth-dkim-test PRIVATE bongomailauth) + add_test(NAME mailauth-dkim COMMAND mailauth-dkim-test ${DKIM_TEST_KEY}) + set_tests_properties(mailauth-dkim PROPERTIES FIXTURES_REQUIRED dkim_key) + endif() +endif() diff --git a/src/libs/mailauth/dkim.c b/src/libs/mailauth/dkim.c new file mode 100644 index 0000000..14cf485 --- /dev/null +++ b/src/libs/mailauth/dkim.c @@ -0,0 +1,155 @@ +#include + +#include +#include +#include +#include + +#include "mailauth.h" + +#define BONGO_DKIM_KEY_MAX (1024U * 1024U) + +struct BongoDKIMSigner { + DKIM_LIB *library; + DKIM *context; + unsigned char *key; +}; + +static BongoMailAuthStatus +ReadPrivateKey(const char *path, unsigned char **result) +{ + struct stat metadata; + FILE *file; + unsigned char *key; + size_t length; + + if (path == NULL || result == NULL || stat(path, &metadata) != 0 || + !S_ISREG(metadata.st_mode) || metadata.st_size <= 0 || + (uintmax_t) metadata.st_size > BONGO_DKIM_KEY_MAX) { + return BONGO_MAILAUTH_IO_ERROR; + } + if ((metadata.st_mode & S_IRWXO) != 0) { + return BONGO_MAILAUTH_INSECURE_SECRET; + } + file = fopen(path, "rb"); + if (file == NULL) { + return BONGO_MAILAUTH_IO_ERROR; + } + length = (size_t) metadata.st_size; + key = malloc(length + 1U); + if (key == NULL) { + fclose(file); + return BONGO_MAILAUTH_NO_MEMORY; + } + if (fread(key, 1U, length, file) != length || fclose(file) != 0) { + memset(key, 0, length); + free(key); + return BONGO_MAILAUTH_IO_ERROR; + } + key[length] = '\0'; + *result = key; + return BONGO_MAILAUTH_OK; +} + +int +BongoMailAuthDKIMAvailable(void) +{ + return OPENDKIM_LIB_VERSION >= 0; +} + +BongoMailAuthStatus +BongoMailAuthDKIMSignerCreate(const char *key_file, const char *selector, + const char *domain, const char *message_id, + BongoDKIMSigner **result) +{ + BongoDKIMSigner *signer; + BongoMailAuthStatus status; + DKIM_STAT dkimStatus; + + if (selector == NULL || selector[0] == '\0' || domain == NULL || + domain[0] == '\0' || message_id == NULL || message_id[0] == '\0' || + result == NULL) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + *result = NULL; + signer = calloc(1U, sizeof(*signer)); + if (signer == NULL) { + return BONGO_MAILAUTH_NO_MEMORY; + } + status = ReadPrivateKey(key_file, &signer->key); + if (status != BONGO_MAILAUTH_OK) { + free(signer); + return status; + } + signer->library = dkim_init(NULL, NULL); + if (signer->library == NULL) { + BongoMailAuthDKIMSignerFree(signer); + return BONGO_MAILAUTH_NO_MEMORY; + } + signer->context = dkim_sign( + signer->library, (const unsigned char *) message_id, NULL, + signer->key, (const unsigned char *) selector, + (const unsigned char *) domain, DKIM_CANON_RELAXED, + DKIM_CANON_RELAXED, DKIM_SIGN_RSASHA256, -1, &dkimStatus); + if (signer->context == NULL || dkimStatus != DKIM_STAT_OK) { + BongoMailAuthDKIMSignerFree(signer); + return BONGO_MAILAUTH_PROTOCOL_ERROR; + } + *result = signer; + return BONGO_MAILAUTH_OK; +} + +BongoMailAuthStatus +BongoMailAuthDKIMSignerUpdate(BongoDKIMSigner *signer, const void *data, + size_t length) +{ + if (signer == NULL || signer->context == NULL || + (data == NULL && length != 0U)) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + return dkim_chunk(signer->context, (unsigned char *) data, length) == + DKIM_STAT_OK + ? BONGO_MAILAUTH_OK : BONGO_MAILAUTH_PROTOCOL_ERROR; +} + +BongoMailAuthStatus +BongoMailAuthDKIMSignerFinish(BongoDKIMSigner *signer, char *signature, + size_t signature_size) +{ + _Bool testKey = 0; + + if (signer == NULL || signer->context == NULL || signature == NULL || + signature_size < 2U) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + if (dkim_chunk(signer->context, NULL, 0U) != DKIM_STAT_OK || + dkim_eom(signer->context, &testKey) != DKIM_STAT_OK || + dkim_getsighdr(signer->context, (unsigned char *) signature, + signature_size, strlen("DKIM-Signature: ")) != + DKIM_STAT_OK) { + return BONGO_MAILAUTH_PROTOCOL_ERROR; + } + signature[signature_size - 1U] = '\0'; + return BONGO_MAILAUTH_OK; +} + +void +BongoMailAuthDKIMSignerFree(BongoDKIMSigner *signer) +{ + if (signer == NULL) { + return; + } + if (signer->context != NULL) { + dkim_free(signer->context); + } + if (signer->library != NULL) { + dkim_close(signer->library); + } + if (signer->key != NULL) { + size_t length = strlen((const char *) signer->key); + memset(signer->key, 0, length); + free(signer->key); + } + memset(signer, 0, sizeof(*signer)); + free(signer); +} diff --git a/src/libs/mailauth/dmarc.c b/src/libs/mailauth/dmarc.c new file mode 100644 index 0000000..012f6ad --- /dev/null +++ b/src/libs/mailauth/dmarc.c @@ -0,0 +1,7 @@ +#include + +int +BongoMailAuthDMARCAvailable(void) +{ + return DMARC_POLICY_SPF_ORIGIN_MAILFROM >= 0; +} diff --git a/src/libs/mailauth/mailauth.c b/src/libs/mailauth/mailauth.c new file mode 100644 index 0000000..31d24b2 --- /dev/null +++ b/src/libs/mailauth/mailauth.c @@ -0,0 +1,19 @@ +#include "mailauth.h" + +int BongoMailAuthSPFAvailable(void); +int BongoMailAuthDKIMAvailable(void); +int BongoMailAuthDMARCAvailable(void); +int BongoMailAuthSRSAvailable(void); + +BongoMailAuthCapabilities +BongoMailAuthGetCapabilities(void) +{ + BongoMailAuthCapabilities capabilities = { + (unsigned int) BongoMailAuthSPFAvailable(), + (unsigned int) BongoMailAuthDKIMAvailable(), + (unsigned int) BongoMailAuthDMARCAvailable(), + (unsigned int) BongoMailAuthSRSAvailable() + }; + + return capabilities; +} diff --git a/src/libs/mailauth/mailauth.h b/src/libs/mailauth/mailauth.h new file mode 100644 index 0000000..d79e2d3 --- /dev/null +++ b/src/libs/mailauth/mailauth.h @@ -0,0 +1,77 @@ +#ifndef BONGO_MAILAUTH_H +#define BONGO_MAILAUTH_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct BongoMailAuthCapabilities { + unsigned int spf; + unsigned int dkim; + unsigned int dmarc; + unsigned int srs; +} BongoMailAuthCapabilities; + +/* Reports the protocol engines compiled into this Bongo build. */ +BongoMailAuthCapabilities BongoMailAuthGetCapabilities(void); + +typedef enum BongoMailAuthStatus { + BONGO_MAILAUTH_OK = 0, + BONGO_MAILAUTH_INVALID_ARGUMENT, + BONGO_MAILAUTH_NO_MEMORY, + BONGO_MAILAUTH_IO_ERROR, + BONGO_MAILAUTH_INSECURE_SECRET, + BONGO_MAILAUTH_PROTOCOL_ERROR +} BongoMailAuthStatus; + +typedef enum BongoSPFResult { + BONGO_SPF_INVALID = 0, + BONGO_SPF_NEUTRAL, + BONGO_SPF_PASS, + BONGO_SPF_FAIL, + BONGO_SPF_SOFTFAIL, + BONGO_SPF_NONE, + BONGO_SPF_TEMPERROR, + BONGO_SPF_PERMERROR +} BongoSPFResult; + +typedef struct BongoSPFResponse { + BongoSPFResult result; + char smtp_comment[512]; + char received_spf[1024]; +} BongoSPFResponse; + +BongoMailAuthStatus BongoMailAuthCheckSPF(const char *receiver_domain, + const char *client_ip, + const char *helo_domain, + const char *mail_from, + BongoSPFResponse *response); + +BongoMailAuthStatus BongoMailAuthSRSForward(const char *secret_file, + const char *srs_domain, + const char *sender, + char *output, + size_t output_size); +BongoMailAuthStatus BongoMailAuthSRSReverse(const char *secret_file, + const char *sender, + char *output, + size_t output_size); + +typedef struct BongoDKIMSigner BongoDKIMSigner; + +BongoMailAuthStatus BongoMailAuthDKIMSignerCreate( + const char *key_file, const char *selector, const char *domain, + const char *message_id, BongoDKIMSigner **signer); +BongoMailAuthStatus BongoMailAuthDKIMSignerUpdate( + BongoDKIMSigner *signer, const void *data, size_t length); +BongoMailAuthStatus BongoMailAuthDKIMSignerFinish( + BongoDKIMSigner *signer, char *signature, size_t signature_size); +void BongoMailAuthDKIMSignerFree(BongoDKIMSigner *signer); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/libs/mailauth/spf.c b/src/libs/mailauth/spf.c new file mode 100644 index 0000000..a983728 --- /dev/null +++ b/src/libs/mailauth/spf.c @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "mailauth.h" + +static void +CopyString(char *destination, size_t destination_size, const char *source) +{ + if (destination_size == 0U) { + return; + } + snprintf(destination, destination_size, "%s", source != NULL ? source : ""); +} + +int +BongoMailAuthSPFAvailable(void) +{ + return SPF_LIB_VERSION_MAJOR >= 0; +} + +BongoMailAuthStatus +BongoMailAuthCheckSPF(const char *receiver_domain, const char *client_ip, + const char *helo_domain, const char *mail_from, + BongoSPFResponse *response) +{ + SPF_server_t *server = NULL; + SPF_request_t *request = NULL; + SPF_response_t *spf_response = NULL; + SPF_errcode_t error; + struct in_addr ipv4; + struct in6_addr ipv6; + BongoMailAuthStatus status = BONGO_MAILAUTH_PROTOCOL_ERROR; + + if (receiver_domain == NULL || receiver_domain[0] == '\0' || + client_ip == NULL || client_ip[0] == '\0' || + helo_domain == NULL || helo_domain[0] == '\0' || + mail_from == NULL || response == NULL) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + memset(response, 0, sizeof(*response)); + + if (inet_pton(AF_INET, client_ip, &ipv4) != 1 && + inet_pton(AF_INET6, client_ip, &ipv6) != 1) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + + server = SPF_server_new(SPF_DNS_CACHE, 0); + if (server == NULL) { + return BONGO_MAILAUTH_NO_MEMORY; + } + if (SPF_server_set_rec_dom(server, receiver_domain) != SPF_E_SUCCESS) { + goto cleanup; + } + + request = SPF_request_new(server); + if (request == NULL) { + status = BONGO_MAILAUTH_NO_MEMORY; + goto cleanup; + } + error = strchr(client_ip, ':') != NULL + ? SPF_request_set_ipv6_str(request, client_ip) + : SPF_request_set_ipv4_str(request, client_ip); + if (error != SPF_E_SUCCESS || + SPF_request_set_helo_dom(request, helo_domain) != SPF_E_SUCCESS || + SPF_request_set_env_from(request, mail_from) != SPF_E_SUCCESS) { + status = BONGO_MAILAUTH_INVALID_ARGUMENT; + goto cleanup; + } + + error = SPF_request_query_mailfrom(request, &spf_response); + if (spf_response == NULL) { + goto cleanup; + } + response->result = (BongoSPFResult) SPF_response_result(spf_response); + CopyString(response->smtp_comment, sizeof(response->smtp_comment), + SPF_response_get_smtp_comment(spf_response)); + CopyString(response->received_spf, sizeof(response->received_spf), + SPF_response_get_received_spf(spf_response)); + status = error == SPF_E_SUCCESS ? BONGO_MAILAUTH_OK : BONGO_MAILAUTH_PROTOCOL_ERROR; + +cleanup: + SPF_response_free(spf_response); + SPF_request_free(request); + SPF_server_free(server); + return status; +} diff --git a/src/libs/mailauth/srs.c b/src/libs/mailauth/srs.c new file mode 100644 index 0000000..bf86403 --- /dev/null +++ b/src/libs/mailauth/srs.c @@ -0,0 +1,123 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mailauth.h" + +#define BONGO_SRS_SECRET_MAX 1024U +#define BONGO_SRS_SECRET_MIN 16U + +static BongoMailAuthStatus +LoadSecrets(srs_t *srs, const char *path) +{ + struct stat metadata; + FILE *file; + char line[BONGO_SRS_SECRET_MAX + 2U]; + unsigned int count = 0U; + + if (stat(path, &metadata) != 0 || !S_ISREG(metadata.st_mode)) { + return BONGO_MAILAUTH_IO_ERROR; + } + if ((metadata.st_mode & S_IRWXO) != 0) { + return BONGO_MAILAUTH_INSECURE_SECRET; + } + file = fopen(path, "r"); + if (file == NULL) { + return BONGO_MAILAUTH_IO_ERROR; + } + while (fgets(line, sizeof(line), file) != NULL) { + size_t length = strcspn(line, "\r\n"); + line[length] = '\0'; + if (length == 0U || line[0] == '#') { + continue; + } + if (length < BONGO_SRS_SECRET_MIN || length > BONGO_SRS_SECRET_MAX || + srs_add_secret(srs, line) != SRS_SUCCESS) { + memset(line, 0, sizeof(line)); + fclose(file); + return BONGO_MAILAUTH_PROTOCOL_ERROR; + } + memset(line, 0, sizeof(line)); + count++; + } + if (ferror(file) != 0 || fclose(file) != 0) { + return BONGO_MAILAUTH_IO_ERROR; + } + return count > 0U ? BONGO_MAILAUTH_OK : BONGO_MAILAUTH_PROTOCOL_ERROR; +} + +static BongoMailAuthStatus +CreateSRS(srs_t **result, const char *secret_file) +{ + BongoMailAuthStatus status; + srs_t *srs; + + if (result == NULL || secret_file == NULL || secret_file[0] == '\0') { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + srs = srs_new(); + if (srs == NULL) { + return BONGO_MAILAUTH_NO_MEMORY; + } + status = LoadSecrets(srs, secret_file); + if (status != BONGO_MAILAUTH_OK) { + srs_free(srs); + return status; + } + *result = srs; + return BONGO_MAILAUTH_OK; +} + +int +BongoMailAuthSRSAvailable(void) +{ + return SRS_SUCCESS == 0; +} + +BongoMailAuthStatus +BongoMailAuthSRSForward(const char *secret_file, const char *srs_domain, + const char *sender, char *output, size_t output_size) +{ + BongoMailAuthStatus status; + srs_t *srs = NULL; + int result; + + if (srs_domain == NULL || srs_domain[0] == '\0' || sender == NULL || + sender[0] == '\0' || output == NULL || output_size < 2U || + output_size > (size_t) INT_MAX) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + status = CreateSRS(&srs, secret_file); + if (status != BONGO_MAILAUTH_OK) { + return status; + } + result = srs_forward(srs, output, (int) output_size, sender, srs_domain); + srs_free(srs); + return result == SRS_SUCCESS ? BONGO_MAILAUTH_OK : BONGO_MAILAUTH_PROTOCOL_ERROR; +} + +BongoMailAuthStatus +BongoMailAuthSRSReverse(const char *secret_file, const char *sender, + char *output, size_t output_size) +{ + BongoMailAuthStatus status; + srs_t *srs = NULL; + int result; + + if (sender == NULL || sender[0] == '\0' || output == NULL || + output_size < 2U || output_size > (size_t) INT_MAX) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + status = CreateSRS(&srs, secret_file); + if (status != BONGO_MAILAUTH_OK) { + return status; + } + result = srs_reverse(srs, output, (int) output_size, sender); + srs_free(srs); + return result == SRS_SUCCESS ? BONGO_MAILAUTH_OK : BONGO_MAILAUTH_PROTOCOL_ERROR; +} diff --git a/src/libs/mailauth/tests/dkim-test.c b/src/libs/mailauth/tests/dkim-test.c new file mode 100644 index 0000000..d3801ea --- /dev/null +++ b/src/libs/mailauth/tests/dkim-test.c @@ -0,0 +1,41 @@ +#include "mailauth.h" + +#include +#include +#include + +int +main(int argc, char **argv) +{ + static const char message[] = + "From: sender@example.org\r\n" + "To: recipient@example.net\r\n" + "Subject: DKIM test\r\n" + "Date: Tue, 15 Jul 2026 12:00:00 +0200\r\n" + "Message-ID: \r\n" + "\r\n" + "Test body.\r\n"; + BongoDKIMSigner *signer = NULL; + char signature[8192]; + + if (argc != 2 || chmod(argv[1], S_IRUSR | S_IWUSR) != 0) { + return 1; + } + if (BongoMailAuthDKIMSignerCreate(argv[1], "test", "example.org", + "dkim-test", &signer) != + BONGO_MAILAUTH_OK || + BongoMailAuthDKIMSignerUpdate(signer, message, + sizeof(message) - 1U) != + BONGO_MAILAUTH_OK || + BongoMailAuthDKIMSignerFinish(signer, signature, + sizeof(signature)) != + BONGO_MAILAUTH_OK) { + BongoMailAuthDKIMSignerFree(signer); + return 2; + } + BongoMailAuthDKIMSignerFree(signer); + return strstr(signature, "v=1") != NULL && + strstr(signature, "d=example.org") != NULL && + strstr(signature, "s=test") != NULL + ? 0 : 3; +} diff --git a/src/libs/mailauth/tests/spf-test.c b/src/libs/mailauth/tests/spf-test.c new file mode 100644 index 0000000..2395fb0 --- /dev/null +++ b/src/libs/mailauth/tests/spf-test.c @@ -0,0 +1,12 @@ +#include "mailauth.h" + +int +main(void) +{ + BongoSPFResponse response; + + return BongoMailAuthCheckSPF("receiver.example", "not-an-ip", + "sender.example", "user@sender.example", + &response) == BONGO_MAILAUTH_INVALID_ARGUMENT + ? 0 : 1; +} diff --git a/src/libs/mailauth/tests/srs-test.c b/src/libs/mailauth/tests/srs-test.c new file mode 100644 index 0000000..9648316 --- /dev/null +++ b/src/libs/mailauth/tests/srs-test.c @@ -0,0 +1,37 @@ +#include "mailauth.h" + +#include +#include +#include +#include +#include + +int +main(void) +{ + char path[] = "/tmp/bongo-srs-test-XXXXXX"; + char encoded[1024]; + char decoded[1024]; + const char sender[] = "sender@example.org"; + int descriptor = mkstemp(path); + FILE *file; + + if (descriptor < 0 || fchmod(descriptor, S_IRUSR | S_IWUSR) != 0) { + return 1; + } + file = fdopen(descriptor, "w"); + if (file == NULL || fputs("test-secret-with-at-least-32-bytes\n", file) < 0 || + fclose(file) != 0) { + unlink(path); + return 2; + } + if (BongoMailAuthSRSForward(path, "forward.example.net", sender, + encoded, sizeof(encoded)) != BONGO_MAILAUTH_OK || + BongoMailAuthSRSReverse(path, encoded, decoded, + sizeof(decoded)) != BONGO_MAILAUTH_OK) { + unlink(path); + return 3; + } + unlink(path); + return strcmp(sender, decoded) == 0 ? 0 : 4; +} diff --git a/src/libs/msgapi/auth-backends/sqlite.c b/src/libs/msgapi/auth-backends/sqlite.c index dffb5ab..b75bef8 100644 --- a/src/libs/msgapi/auth-backends/sqlite.c +++ b/src/libs/msgapi/auth-backends/sqlite.c @@ -268,11 +268,12 @@ AuthSqlite_SetPassword(const char *user, const char *password) int AuthSqlite_GetUserStore(const char *user, struct sockaddr_in *store) { - memset(store, 0, sizeof(store)); + UNUSED_PARAMETER(user); + memset(store, 0, sizeof(*store)); store->sin_addr.s_addr = inet_addr("127.0.0.1"); store->sin_family = AF_INET; store->sin_port = htons(689); - return TRUE; + return 0; } int @@ -333,6 +334,8 @@ AuthSqlite_InterfaceVersion(void) int main (int argc, char *argv[]) { + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); printf("This cannot be run directly"); return (-1); } diff --git a/src/libs/msgapi/msgaddr.c b/src/libs/msgapi/msgaddr.c index 3ef02cb..f0da8b3 100644 --- a/src/libs/msgapi/msgaddr.c +++ b/src/libs/msgapi/msgaddr.c @@ -399,9 +399,10 @@ MsgIsComment(unsigned char *base, unsigned char *limit, unsigned char **out) } EXPORT BOOL -MsgParseAddress(unsigned char *addressLine, size_t addressLineLength, unsigned char **local_part, unsigned char **domain ) { +MsgParseAddress(char *addressLine, size_t addressLineLength, char **local_part, char **domain ) { + UNUSED_PARAMETER(addressLineLength); /*TODO: this is a terribly simple parser of an email address into the local_part and the domain. this needs to be better */ - unsigned char *at = strchr(addressLine, '@'); + char *at = strchr(addressLine, '@'); if (at) { *at = '\0'; (*local_part)= MemStrdup(addressLine); diff --git a/src/libs/msgapi/msgapi.c b/src/libs/msgapi/msgapi.c index 3e70d25..56aaecc 100644 --- a/src/libs/msgapi/msgapi.c +++ b/src/libs/msgapi/msgapi.c @@ -42,6 +42,8 @@ #include #include #include +#include +#include #define MSGAPI_FLAG_EXITING (1 << 0) #define MSGAPI_FLAG_STANDALONE (1 << 1) @@ -93,7 +95,7 @@ struct { struct { unsigned long local; - unsigned char string[16]; + char string[INET_ADDRSTRLEN]; } address; struct { @@ -123,24 +125,28 @@ MsgGetUpdateStatus(char *record, int record_length); void MsgNmapChallenge(const unsigned char *response, unsigned char *reply, size_t length) { - unsigned char *ptr; - unsigned char *salt; + const unsigned char *start; + const unsigned char *end; static char access[NMAP_HASH_SIZE] = { '\0' }; xpl_hash_context ctx; MsgGetServerCredential(access); - if (access[0] && reply && (length > 32) && ((ptr = strchr(response, '<')) != NULL)) { - salt = ++ptr; - - if ((ptr = strchr(ptr, '>')) != NULL) { - *ptr = '\0'; - } - + start = response + ? (const unsigned char *)strchr((const char *)response, '<') + : NULL; + if (access[0] && reply && (length > 32) && start != NULL) { + start++; + end = (const unsigned char *)strchr((const char *)start, '>'); + if (end == NULL) { + *reply = '\0'; + return; + } + XplHashNew(&ctx, XPLHASH_MD5); - XplHashWrite(&ctx, salt, strlen(salt)); + XplHashWrite(&ctx, start, (size_t)(end - start)); XplHashWrite(&ctx, access, NMAP_HASH_SIZE); - XplHashFinal(&ctx, XPLHASH_LOWERCASE, reply, XPLHASH_MD5_LENGTH); + XplHashFinal(&ctx, XPLHASH_LOWERCASE, (char *)reply, XPLHASH_MD5_LENGTH); } else if (reply) { *reply = '\0'; } @@ -151,6 +157,7 @@ MsgNmapChallenge(const unsigned char *response, unsigned char *reply, size_t len EXPORT const unsigned char * MsgGetServerDN(unsigned char *buffer) { + UNUSED_PARAMETER(buffer); // DEPRECATED return(NULL); } @@ -158,6 +165,8 @@ MsgGetServerDN(unsigned char *buffer) EXPORT BOOL MsgDomainExists(const unsigned char *domain, unsigned char *domainObjectDN) { + UNUSED_PARAMETER(domain); + UNUSED_PARAMETER(domainObjectDN); // DEPRECATED ? /* FIXME later; when we handle parent objects */ return(FALSE); @@ -308,7 +317,7 @@ MsgGetDir(MsgApiDirectory directory, char *buffer, size_t buffer_size) EXPORT const char * MsgGetFile(MsgApiFile file, char *buffer, size_t buffer_size) { - const unsigned char *path; + const char *path; switch(file) { case MSGAPI_FILE_PUBKEY: path = XPL_DEFAULT_CERT_PATH; @@ -374,7 +383,6 @@ MsgLibraryStop(void) return(TRUE); } -#if 0 /** * Get a config property from global configuration file. Removes dependency * on NCPServer object in the directory (as well as others). See msgapi.h for @@ -383,44 +391,53 @@ MsgLibraryStop(void) BOOL MsgGetConfigProperty(unsigned char *Buffer, unsigned char *Property) { - int result = FALSE; - char line[512]; - char *ptr; - FILE *fh; - int len; - char conf_path[FILENAME_MAX]; + BOOL result = FALSE; + char line[MSGSRV_CONFIG_MAX_PROP_CHARS * 2 + 4]; + char *name; + char *value; + char *end; + FILE *fh; + char conf_path[FILENAME_MAX]; - snprintf(conf_path, FILENAME_MAX, "%s/bongo.conf", XPL_DEFAULT_CONF_DIR); + if (Buffer == NULL || Property == NULL) + return FALSE; + Buffer[0] = '\0'; + if (snprintf(conf_path, sizeof(conf_path), "%s/%s", XPL_DEFAULT_CONF_DIR, + MSGSRV_CONFIG_FILENAME) >= (int)sizeof(conf_path)) + return FALSE; if (!(fh = fopen(conf_path, "r"))) { return result; } - while (fgets(line, 512, fh)) { - if (line[0] != '#') { - if ((ptr = strchr(line, '='))) { - *ptr++ = '\0'; - len = strlen(ptr); - - if (!strcmp(line, Property)) { - if (ptr[len - 1] == '\n') { - ptr[len - 1] = '\0'; - } - - strncpy(Buffer, ptr, MSGSRV_CONFIG_MAX_PROP_CHARS); - result = TRUE; - break; - } - } else { - break; - } - } - } + while (fgets(line, sizeof(line), fh)) { + name = line; + while (isspace((unsigned char)*name)) + name++; + if (*name == '#' || *name == '\0') + continue; + value = strchr(name, '='); + if (value == NULL) + continue; + *value++ = '\0'; + end = name + strlen(name); + while (end > name && isspace((unsigned char)end[-1])) + *--end = '\0'; + if (strcmp(name, (const char *)Property) != 0) + continue; + while (isspace((unsigned char)*value)) + value++; + end = value + strlen(value); + while (end > value && isspace((unsigned char)end[-1])) + *--end = '\0'; + snprintf((char *)Buffer, MSGSRV_CONFIG_MAX_PROP_CHARS + 1, "%s", value); + result = TRUE; + break; + } fclose(fh); return result; } -#endif void MsgGetUid(char *buffer, int buflen) @@ -452,10 +469,7 @@ MsgGetUid(char *buffer, int buflen) static BOOL MsgLibraryStart(void) { - unsigned long i; struct sockaddr_in server_sockaddr; - unsigned char path[XPL_MAX_PATH + 1]; - XplThreadID ID; /* Prepare later config updates */ XplRWLockInit(&MsgGlobal.configLock); @@ -468,12 +482,13 @@ MsgLibraryStart(void) /* set the ip address up correctly */ server_sockaddr.sin_addr.s_addr = XplGetHostIPAddress(); - sprintf(MsgGlobal.address.string, "%d.%d.%d.%d", - server_sockaddr.sin_addr.s_net, - server_sockaddr.sin_addr.s_host, - server_sockaddr.sin_addr.s_lh, - server_sockaddr.sin_addr.s_impno); - MsgGlobal.address.local = inet_addr(MsgGlobal.address.string); + if (inet_ntop(AF_INET, &server_sockaddr.sin_addr, + MsgGlobal.address.string, + sizeof(MsgGlobal.address.string)) == NULL) { + strcpy(MsgGlobal.address.string, "127.0.0.1"); + server_sockaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + } + MsgGlobal.address.local = server_sockaddr.sin_addr.s_addr; return(TRUE); } @@ -504,8 +519,6 @@ MsgGetAvailableVersion(int *version) */ char record[500]; char *ptr; - int v; - if (!MsgGetUpdateStatus(record, 499)) return FALSE; @@ -542,7 +555,6 @@ MsgGetUpdateStatus(char *record, int record_length) static BOOL MsgReadConfiguration(void) { - unsigned long i; struct tm *timeStruct; time_t utcTime; long tmp; @@ -609,6 +621,7 @@ MsgLibraryShutdown(void) MsgGlobal.flags |= MSGAPI_FLAG_EXITING; oldGid = XplSetThreadGroupID(MsgGlobal.groupID); + UNUSED_PARAMETER(oldGid); MsgLibraryStop(); @@ -717,8 +730,8 @@ MsgClearRecoveryFlag(char *agent_name) EXPORT BOOL MsgGetServerCredential(char *buffer) { - unsigned char credential[4097]; - unsigned char file[120]; + char credential[4097]; + char file[120]; FILE *credfile; memset(credential, 0, sizeof(credential)); @@ -738,9 +751,10 @@ MsgGetServerCredential(char *buffer) EXPORT BOOL MsgSetServerCredential() { - unsigned char credential[4097]; - unsigned char path[XPL_MAX_PATH]; + char credential[4097]; + char path[XPL_MAX_PATH]; FILE *credfile; + int fd; const char *posschars = "abcdefghijlkmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWYXZ" @@ -748,18 +762,26 @@ MsgSetServerCredential() int i, range; range = strlen(posschars); - for (i=0; i<4097; i++) { - int ran = rand() % range; - credential[i] = posschars[ran]; + XplRandomData(credential, sizeof(credential) - 1); + for (i = 0; i < (int)sizeof(credential) - 1; i++) { + credential[i] = posschars[(unsigned char)credential[i] % range]; } credential[4096] = '\0'; snprintf(path, XPL_MAX_PATH, "%s/credential.dat", XPL_DEFAULT_DBF_DIR); - credfile = fopen(path, "wb"); + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); + if (fd >= 0 && fchmod(fd, S_IRUSR | S_IWUSR) != 0) { + close(fd); + fd = -1; + } + credfile = fd >= 0 ? fdopen(fd, "wb") : NULL; if (credfile) { - fwrite(credential, sizeof(char), sizeof(credential), credfile); - fclose(credfile); - return TRUE; + BOOL ok = fwrite(credential, sizeof(char), sizeof(credential), credfile) == sizeof(credential); + ok = (fclose(credfile) == 0) && ok; + return ok; + } + if (fd >= 0) { + close(fd); } return FALSE; } diff --git a/src/libs/msgapi/msgauth.c b/src/libs/msgapi/msgauth.c index 8427967..570ef3f 100644 --- a/src/libs/msgapi/msgauth.c +++ b/src/libs/msgapi/msgauth.c @@ -249,7 +249,7 @@ MsgAuthGetUserStore(const char *user, struct sockaddr_in *store) f = (AuthPlugin_GetUserStore *)&function->func; result = (*f)(user, store); } else { - memset(store, 0, sizeof(store)); + memset(store, 0, sizeof(*store)); store->sin_addr.s_addr = inet_addr("127.0.0.1"); store->sin_family = AF_INET; store->sin_port = htons(689); diff --git a/src/libs/msgapi/msgcollector.c b/src/libs/msgapi/msgcollector.c index e79ea6b..9c04076 100644 --- a/src/libs/msgapi/msgcollector.c +++ b/src/libs/msgapi/msgcollector.c @@ -129,7 +129,6 @@ static Connection * ConnectToStore(const char *user) { char buffer[CONN_BUFSIZE + 1]; - struct sockaddr_in addr; Connection *conn; int ret; @@ -304,10 +303,10 @@ RemoveGuidForeach (void *key, void *value, void *data) RemoveGuidForeachData *rgfd = data; EventData *evtData = value; - int ccode; + UNUSED_PARAMETER(key); NMAPSendCommandF(rgfd->conn, "UNLINK /calendars/%s %s\r\n", rgfd->calendarName, evtData->guid); - ccode = NMAPReadAnswer(rgfd->conn, rgfd->buffer, rgfd->bufferLen, TRUE); + NMAPReadAnswer(rgfd->conn, rgfd->buffer, rgfd->bufferLen, TRUE); } static int @@ -427,6 +426,8 @@ MsgImportIcs(FILE *fh, int ret; const char *icsName; char *icsNameDup = NULL; + + UNUSED_PARAMETER(calendarColor); component = ParseIcs(fh); if (!component) { @@ -701,7 +702,7 @@ MsgImportIcsUrl(const char *user, FILE *fh = XplOpenTemp("/tmp", "w+", filename); char cred[1024]; - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, TRUE); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); if (aliasedUrl) { curl_easy_setopt(curl, CURLOPT_URL, aliasedUrl); } else { diff --git a/src/libs/msgapi/msgdate.c b/src/libs/msgapi/msgdate.c index 762d28a..3b68306 100644 --- a/src/libs/msgapi/msgdate.c +++ b/src/libs/msgapi/msgdate.c @@ -176,10 +176,10 @@ MsgGetDate(long rataDie, unsigned long *day, unsigned long *month, unsigned long } EXPORT BOOL -MsgGetRFC822Date(long offset, long utcTime, unsigned char *dateBuffer) +MsgGetRFC822Date(long offset, long utcTime, char *dateBuffer) { struct tm timeStruct; - unsigned char tempBuffer[80]; + char tempBuffer[80]; if (utcTime == 0) { utcTime = time(NULL); @@ -263,16 +263,22 @@ uint64_t MsgParseRFC822Date(const char *date) { uint64_t utc; + char *copy; char *dayPtr; char *monthPtr; char *yearPtr; unsigned long month; - dayPtr = strchr(date, ','); + copy = MemStrdup(date); + if (copy == NULL) { + return 0; + } + + dayPtr = strchr(copy, ','); if (dayPtr) { dayPtr++; } else { - dayPtr = (char *)date; + dayPtr = copy; } while((*dayPtr == ' ') || (*dayPtr == '\t')) { @@ -290,11 +296,13 @@ MsgParseRFC822Date(const char *date) yearPtr++; utc = MsgGetUTC((unsigned long)atol(dayPtr), month, (unsigned long)atol(yearPtr), 0, 0, 0); if (utc < ((unsigned long) -1)) { + MemFree(copy); return(utc); } } } + MemFree(copy); return 0; } @@ -463,22 +471,43 @@ MsgGetDMY(unsigned long utcTime, unsigned long *day, unsigned long *month, unsig * %Y - Year with century as decimal number * %Z - Timezone offset from UTC (+/- 0000) */ -EXPORT unsigned long -MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned long timeIn, MsgDateFormat *dateInfo) +static void +MsgAppendFormat(char **dest, char *limit, const char *format, ...) { - unsigned long rataDie; - unsigned char *src; - unsigned char *dest; - unsigned char *limit; + size_t available; + int written; + va_list args; + + if (*dest >= limit) { + return; + } + available = (size_t)(limit - *dest); + va_start(args, format); + written = vsnprintf(*dest, available, format, args); + va_end(args); + if (written < 0 || (size_t)written >= available) { + *dest = limit; + } else { + *dest += written; + } +} + +EXPORT unsigned long +MsgPrint(char *buffer, int bufSize, const char *format, unsigned long timeIn, const MsgDateFormat *dateInfo) +{ + const char *src; + char *dest; + char *limit; struct tm tms; + time_t utc; unsigned long i; - rataDie = 0; src = format; dest = buffer; limit = buffer + bufSize; - gmtime_r((time_t *)&timeIn, &tms); + utc = (time_t)timeIn; + gmtime_r(&utc, &tms); while (*src && dest < limit) { switch (*src) { @@ -531,13 +560,13 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon } case 'd': { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_mday); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_mday); src++; continue; } case 'H': { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_hour); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_hour); src++; continue; } @@ -545,9 +574,9 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon case 'I': { if (tms.tm_hour!=0) { if (tms.tm_hour>12) { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_hour - 12); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_hour - 12); } else { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_hour); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_hour); } } else { i = limit - dest > 2 ? 2 : limit - dest; @@ -559,19 +588,19 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon } case 'j': { - dest += snprintf(dest, limit - dest, "%d", tms.tm_yday + 1); + MsgAppendFormat(&dest, limit, "%d", tms.tm_yday + 1); src++; continue; } case 'm': { - dest += snprintf (dest, limit - dest, "%02d", tms.tm_mon + 1); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_mon + 1); src++; continue; } case 'M': { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_min); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_min); src++; continue; } @@ -583,7 +612,7 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon } case 'p': { - dest += snprintf(dest, limit - dest, "%s", tms.tm_hour > 11 ? dateInfo->AmPm[1] : dateInfo->AmPm[0]); + MsgAppendFormat(&dest, limit, "%s", tms.tm_hour > 11 ? dateInfo->AmPm[1] : dateInfo->AmPm[0]); src++; continue; } @@ -591,19 +620,19 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon case 'r': { if (tms.tm_hour!=0) { if (tms.tm_hour>12) { - dest += snprintf(dest, limit - dest, "%02d:%02d:%02d %s", tms.tm_hour-12, tms.tm_min, tms.tm_sec, dateInfo->AmPm[1]); + MsgAppendFormat(&dest, limit, "%02d:%02d:%02d %s", tms.tm_hour-12, tms.tm_min, tms.tm_sec, dateInfo->AmPm[1]); } else { - dest += snprintf(dest, limit - dest, "%02d:%02d:%02d %s", tms.tm_hour, tms.tm_min, tms.tm_sec, tms.tm_hour > 11 ? dateInfo->AmPm[1] : dateInfo->AmPm[0]); + MsgAppendFormat(&dest, limit, "%02d:%02d:%02d %s", tms.tm_hour, tms.tm_min, tms.tm_sec, tms.tm_hour > 11 ? dateInfo->AmPm[1] : dateInfo->AmPm[0]); } } else { - dest += snprintf(dest, limit - dest, "12:%02d:%02d %s", tms.tm_min, tms.tm_sec, dateInfo->AmPm[0]); + MsgAppendFormat(&dest, limit, "12:%02d:%02d %s", tms.tm_min, tms.tm_sec, dateInfo->AmPm[0]); } src++; continue; } case 'S': { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_sec); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_sec); src++; continue; } @@ -615,7 +644,7 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon } case 'T': { - dest += snprintf(dest, limit - dest, "%02d:%02d:%02d", tms.tm_hour, tms.tm_min, tms.tm_sec); + MsgAppendFormat(&dest, limit, "%02d:%02d:%02d", tms.tm_hour, tms.tm_min, tms.tm_sec); src++; continue; } @@ -628,34 +657,34 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon rataDieToday = MsgGetRataDie(tms.tm_mday, tms.tm_mon + 1, tms.tm_year + 1900); i = rataDieYearStart % 7; if (i <= dateInfo->wDayStart) { - rataDieYearStart -= abs(dateInfo->wDayStart - 7); + rataDieYearStart -= 7 - dateInfo->wDayStart; } rataDieYearStart -= i; - dest += snprintf(dest, limit - dest, "%d", (int)((rataDieToday-rataDieYearStart) / 7 + 1)); + MsgAppendFormat(&dest, limit, "%d", (int)((rataDieToday-rataDieYearStart) / 7 + 1)); src++; continue; } case 'w': { - dest += snprintf(dest, limit - dest, "%d", tms.tm_wday); + MsgAppendFormat(&dest, limit, "%d", tms.tm_wday); src++; continue; } case 'y': { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_year > 99 ? tms.tm_year - 100 : tms.tm_year); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_year > 99 ? tms.tm_year - 100 : tms.tm_year); src++; continue; } case 'Y': { - dest += snprintf(dest, limit - dest, "%04d", tms.tm_year + 1900); + MsgAppendFormat(&dest, limit, "%04d", tms.tm_year + 1900); src++; continue; } case 'Z': { - dest += snprintf(dest, limit - dest, "%+4.4ld", dateInfo->timezoneOffset/36); + MsgAppendFormat(&dest, limit, "%+4.4ld", dateInfo->timezoneOffset/36); src++; continue; } @@ -984,8 +1013,9 @@ MsgGetUTCOffsetByUTC(unsigned long timezone, unsigned long utc) unsigned long year; unsigned long hour; struct tm tms; + time_t utcTime = (time_t)utc; - gmtime_r(&utc, &tms); + gmtime_r(&utcTime, &tms); year = tms.tm_year+1900; month = tms.tm_mon+1; day = tms.tm_mday; diff --git a/src/libs/msgapi/sqldb.c b/src/libs/msgapi/sqldb.c index 3ce5e92..ef5b623 100644 --- a/src/libs/msgapi/sqldb.c +++ b/src/libs/msgapi/sqldb.c @@ -31,11 +31,20 @@ MsgSQLOpen(char *path, BongoMemStack *memstack, int locktimeoutms) create = access(path, 0); - if (!(handle = MemMalloc(sizeof(MsgSQLHandle))) || - memset(handle, 0, sizeof(MsgSQLHandle)), 0 || - SQLITE_OK != sqlite3_open(path, &handle->db)) { + handle = MemMalloc(sizeof(MsgSQLHandle)); + if (!handle) { + Log(LOG_ERROR, "sql3: Out of memory opening database \"%s\"", path); + return NULL; + } + memset(handle, 0, sizeof(MsgSQLHandle)); + + if (SQLITE_OK != sqlite3_open(path, &handle->db)) { Log(LOG_ERROR, "sql3: Failed to open database \"%s\"", path); - goto fail; + if (handle->db) { + sqlite3_close(handle->db); + } + MemFree(handle); + return NULL; } Log(LOG_TRACE, "open handle %ld", handle->db); @@ -53,11 +62,6 @@ MsgSQLOpen(char *path, BongoMemStack *memstack, int locktimeoutms) return handle; -fail: - if (handle) { - MsgSQLClose(handle); - } - return NULL; } void @@ -135,7 +139,7 @@ int MsgSQLBeginTransaction(MsgSQLHandle *handle) { MsgSQLStatement *stmt; - int result, reset; + int result; int count = 1 + handle->lockTimeoutMs / MSGSQL_STMT_SLEEP_MS; BOOL locked = FALSE; @@ -154,7 +158,7 @@ MsgSQLBeginTransaction(MsgSQLHandle *handle) do { result = sqlite3_step(stmt->stmt); Log(LOG_TRACE, "sql3: BEGIN TRAN stmt %ld", stmt->stmt); - reset = sqlite3_reset(stmt->stmt); + sqlite3_reset(stmt->stmt); Log(LOG_TRACE, "sql: - bt reset stmt %ld", stmt->stmt); } while (SQLITE_BUSY == result && --count && (XplDelay(MSGSQL_STMT_SLEEP_MS), 1)); @@ -423,7 +427,7 @@ MsgSQLResultInt64(MsgSQLStatement *_stmt, int column) int MsgSQLResultText(MsgSQLStatement *_stmt, int column, char *result, size_t result_size) { - const char *out = sqlite3_column_text(_stmt->stmt, column); + const char *out = (const char *)sqlite3_column_text(_stmt->stmt, column); Log(LOG_TRACE, "sql3: - result text %ld", _stmt->stmt); if (out != NULL) strncpy(result, out, result_size); @@ -436,7 +440,7 @@ MsgSQLResultTextPtr(MsgSQLStatement *_stmt, int column, char **ptr) { *ptr = NULL; - const char *result = sqlite3_column_text(_stmt->stmt, column); + const char *result = (const char *)sqlite3_column_text(_stmt->stmt, column); if (result != NULL) *ptr = g_strdup(result); return 0; diff --git a/src/libs/python/bongo/CMakeLists.txt b/src/libs/python/bongo/CMakeLists.txt index 6ae24af..8e52a0f 100644 --- a/src/libs/python/bongo/CMakeLists.txt +++ b/src/libs/python/bongo/CMakeLists.txt @@ -5,7 +5,9 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Xpl.py.in ${CMAKE_CURRENT_BINARY_DIR} install(DIRECTORY ./ DESTINATION "${PYTHON_SITEPACKAGES_PATH}/bongo/" - FILES_MATCHING PATTERN "*.py" + FILES_MATCHING + PATTERN "__pycache__" EXCLUDE + PATTERN "*.py" PATTERN ".svn" EXCLUDE ) diff --git a/src/libs/python/bongo/Contact.py b/src/libs/python/bongo/Contact.py index 9abb594..d6b2e7a 100644 --- a/src/libs/python/bongo/Contact.py +++ b/src/libs/python/bongo/Contact.py @@ -1,4 +1,4 @@ -import bongo.external.simplejson as simplejson +import json as simplejson class Contact: collapsed = ["fn", "note"] @@ -46,4 +46,3 @@ class Contact: def AsVcard(self): pass - diff --git a/src/libs/python/bongo/admin/ManagedSlapd.py b/src/libs/python/bongo/admin/ManagedSlapd.py index be121c8..dbea8da 100644 --- a/src/libs/python/bongo/admin/ManagedSlapd.py +++ b/src/libs/python/bongo/admin/ManagedSlapd.py @@ -6,6 +6,7 @@ import base64 import logging import os import pwd +import subprocess import random import hashlib import signal @@ -168,13 +169,27 @@ surname: Admin cn: admin userpassword: %(userpw)s """ % {"suffix" : self.suffix, "rootdn" : self.rootdn, - "userpw" : self.encodePassword("bongo")}) + "userpw" : self.encodePassword("bongo")}.encode("utf-8")) os.close(fd) - os.system("ldapadd -h 127.0.0.1 -p %d -x -D %s -w %s -f %s" % - (self.port, self.rootdn, self.password, tmpname)) - os.unlink(tmpname) + pwfd, pwname = tempfile.mkstemp(".password") + try: + os.fchmod(pwfd, 0o600) + os.write(pwfd, self.password.encode("utf-8")) + os.close(pwfd) + pwfd = None + subprocess.check_call([ + "ldapadd", "-h", "127.0.0.1", "-p", str(self.port), + "-x", "-D", self.rootdn, "-y", pwname, "-f", tmpname + ]) + finally: + if pwfd is not None: + os.close(pwfd) + if os.path.exists(pwname): + os.unlink(pwname) + if os.path.exists(tmpname): + os.unlink(tmpname) def writeSlapdConf(self, file, writeRootpw=True): ldapdir = "%s/ldap" % Xpl.DEFAULT_STATE_DIR diff --git a/src/libs/python/bongo/cmdparse.py b/src/libs/python/bongo/cmdparse.py index 1f9c636..b628696 100644 --- a/src/libs/python/bongo/cmdparse.py +++ b/src/libs/python/bongo/cmdparse.py @@ -162,7 +162,7 @@ class CommandParser(OptionParser): for group in groups: result.append("\n%s commands:\n" % group) commands = self.groups[group] - commands.sort(lambda x, y: cmp(x.get_name(), y.get_name())) + commands.sort(key=lambda command: command.get_name()) for cmd in commands: result.append(" %s%s %s\n" % (cmd.helpstr, " "*(max_cmd_length-len(cmd.helpstr)), cmd.summary)) diff --git a/src/libs/python/bongo/external/dateutil/__init__.py b/src/libs/python/bongo/external/dateutil/__init__.py deleted file mode 100644 index 84d7c3e..0000000 --- a/src/libs/python/bongo/external/dateutil/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" diff --git a/src/libs/python/bongo/external/dateutil/easter.py b/src/libs/python/bongo/external/dateutil/easter.py deleted file mode 100644 index 6a573e1..0000000 --- a/src/libs/python/bongo/external/dateutil/easter.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -import datetime - -__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"] - -EASTER_JULIAN = 1 -EASTER_ORTHODOX = 2 -EASTER_WESTERN = 3 - -def easter(year, method=EASTER_WESTERN): - """ - This method was ported from the work done by GM Arts, - on top of the algorithm by Claus Tondering, which was - based in part on the algorithm of Ouding (1940), as - quoted in "Explanatory Supplement to the Astronomical - Almanac", P. Kenneth Seidelmann, editor. - - This algorithm implements three different easter - calculation methods: - - 1 - Original calculation in Julian calendar, valid in - dates after 326 AD - 2 - Original method, with date converted to Gregorian - calendar, valid in years 1583 to 4099 - 3 - Revised method, in Gregorian calendar, valid in - years 1583 to 4099 as well - - These methods are represented by the constants: - - EASTER_JULIAN = 1 - EASTER_ORTHODOX = 2 - EASTER_WESTERN = 3 - - The default method is method 3. - - More about the algorithm may be found at: - - http://users.chariot.net.au/~gmarts/eastalg.htm - - and - - http://www.tondering.dk/claus/calendar.html - - """ - - if not (1 <= method <= 3): - raise ValueError("invalid method") - - # g - Golden year - 1 - # c - Century - # h - (23 - Epact) mod 30 - # i - Number of days from March 21 to Paschal Full Moon - # j - Weekday for PFM (0=Sunday, etc) - # p - Number of days from March 21 to Sunday on or before PFM - # (-6 to 28 methods 1 & 3, to 56 for method 2) - # e - Extra days to add for method 2 (converting Julian - # date to Gregorian date) - - y = year - g = y % 19 - e = 0 - if method < 3: - # Old method - i = (19*g+15)%30 - j = (y+y/4+i)%7 - if method == 2: - # Extra dates to convert Julian to Gregorian date - e = 10 - if y > 1600: - e = e+y/100-16-(y/100-16)/4 - else: - # New method - c = y/100 - h = (c-c/4-(8*c+13)/25+19*g+15)%30 - i = h-(h/28)*(1-(h/28)*(29/(h+1))*((21-g)/11)) - j = (y+y/4+i+2-c+c/4)%7 - - # p can be from -6 to 56 corresponding to dates 22 March to 23 May - # (later dates apply to method 2, although 23 May never actually occurs) - p = i-j+e - d = 1+(p+27+(p+6)/40)%31 - m = 3+(p+26)/30 - return datetime.date(y,m,d) - diff --git a/src/libs/python/bongo/external/dateutil/parser.py b/src/libs/python/bongo/external/dateutil/parser.py deleted file mode 100644 index d5eda44..0000000 --- a/src/libs/python/bongo/external/dateutil/parser.py +++ /dev/null @@ -1,875 +0,0 @@ -# -*- coding:iso-8859-1 -*- -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -import os.path -import string -import sys -import time - -import datetime -from . import relativedelta -from . import tz - -__all__ = ["parse", "parserinfo"] - -# Some pointers: -# -# http://www.cl.cam.ac.uk/~mgk25/iso-time.html -# http://www.iso.ch/iso/en/prods-services/popstds/datesandtime.html -# http://www.w3.org/TR/NOTE-datetime -# http://ringmaster.arc.nasa.gov/tools/time_formats.html -# http://search.cpan.org/author/MUIR/Time-modules-2003.0211/lib/Time/ParseDate.pm -# http://stein.cshl.org/jade/distrib/docs/java.text.SimpleDateFormat.html - -try: - from io import StringIO -except ImportError: - from io import StringIO - -class _timelex: - def __init__(self, instream): - if isinstance(instream, str): - instream = StringIO(instream) - self.instream = instream - self.wordchars = ('abcdfeghijklmnopqrstuvwxyz' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ_' - 'ßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ' - 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ') - self.numchars = '0123456789' - self.whitespace = ' \t\r\n' - self.charstack = [] - self.tokenstack = [] - self.eof = False - - def get_token(self): - if self.tokenstack: - return self.tokenstack.pop(0) - seenletters = False - token = None - state = None - wordchars = self.wordchars - numchars = self.numchars - whitespace = self.whitespace - while not self.eof: - if self.charstack: - nextchar = self.charstack.pop(0) - else: - nextchar = self.instream.read(1) - while nextchar == '\x00': - nextchar = self.instream.read(1) - if not nextchar: - self.eof = True - break - elif not state: - token = nextchar - if nextchar in wordchars: - state = 'a' - elif nextchar in numchars: - state = '0' - elif nextchar in whitespace: - token = ' ' - break # emit token - else: - break # emit token - elif state == 'a': - seenletters = True - if nextchar in wordchars: - token += nextchar - elif nextchar == '.': - token += nextchar - state = 'a.' - else: - self.charstack.append(nextchar) - break # emit token - elif state == '0': - if nextchar in numchars: - token += nextchar - elif nextchar == '.': - token += nextchar - state = '0.' - else: - self.charstack.append(nextchar) - break # emit token - elif state == 'a.': - seenletters = True - if nextchar == '.' or nextchar in wordchars: - token += nextchar - elif nextchar in numchars and token[-1] == '.': - token += nextchar - state = '0.' - else: - self.charstack.append(nextchar) - break # emit token - elif state == '0.': - if nextchar == '.' or nextchar in numchars: - token += nextchar - elif nextchar in wordchars and token[-1] == '.': - token += nextchar - state = 'a.' - else: - self.charstack.append(nextchar) - break # emit token - if (state in ('a.', '0.') and - (seenletters or token.count('.') > 1 or token[-1] == '.')): - l = token.split('.') - token = l[0] - for tok in l[1:]: - self.tokenstack.append('.') - if tok: - self.tokenstack.append(tok) - return token - - def __iter__(self): - return self - - def __next__(self): - token = self.get_token() - if token is None: - raise StopIteration - return token - - def split(cls, s): - return list(cls(s)) - split = classmethod(split) - -class _resultbase(object): - - def __init__(self): - for attr in self.__slots__: - setattr(self, attr, None) - - def _repr(self, classname): - l = [] - for attr in self.__slots__: - value = getattr(self, attr) - if value is not None: - l.append("%s=%s" % (attr, repr(value))) - return "%s(%s)" % (classname, ", ".join(l)) - - def __repr__(self): - return self._repr(self.__class__.__name__) - -class parserinfo: - - # m from a.m/p.m, t from ISO T separator - JUMP = [" ", ".", ",", ";", "-", "/", "'", - "at", "on", "and", "ad", "m", "t", "of", - "st", "nd", "rd", "th"] - - WEEKDAYS = [("Mon", "Monday"), - ("Tue", "Tuesday"), - ("Wed", "Wednesday"), - ("Thu", "Thursday"), - ("Fri", "Friday"), - ("Sat", "Saturday"), - ("Sun", "Sunday")] - MONTHS = [("Jan", "January"), - ("Feb", "February"), - ("Mar", "March"), - ("Apr", "April"), - ("May", "May"), - ("Jun", "June"), - ("Jul", "July"), - ("Aug", "August"), - ("Sep", "September"), - ("Oct", "October"), - ("Nov", "November"), - ("Dec", "December")] - HMS = [("h", "hour", "hours"), - ("m", "minute", "minutes"), - ("s", "second", "seconds")] - AMPM = [("am", "a"), - ("pm", "p")] - UTCZONE = ["UTC", "GMT", "Z"] - PERTAIN = ["of"] - TZOFFSET = {} - - def __init__(self, dayfirst=False, yearfirst=False): - self._jump = self._convert(self.JUMP) - self._weekdays = self._convert(self.WEEKDAYS) - self._months = self._convert(self.MONTHS) - self._hms = self._convert(self.HMS) - self._ampm = self._convert(self.AMPM) - self._utczone = self._convert(self.UTCZONE) - self._pertain = self._convert(self.PERTAIN) - - self.dayfirst = dayfirst - self.yearfirst = yearfirst - - self._year = time.localtime().tm_year - self._century = self._year/100*100 - - def _convert(self, lst): - dct = {} - for i in range(len(lst)): - v = lst[i] - if isinstance(v, tuple): - for v in v: - dct[v.lower()] = i - else: - dct[v.lower()] = i - return dct - - def jump(self, name): - return name.lower() in self._jump - - def weekday(self, name): - if len(name) >= 3: - try: - return self._weekdays[name.lower()] - except KeyError: - pass - return None - - def month(self, name): - if len(name) >= 3: - try: - return self._months[name.lower()]+1 - except KeyError: - pass - return None - - def hms(self, name): - try: - return self._hms[name.lower()] - except KeyError: - return None - - def ampm(self, name): - try: - return self._ampm[name.lower()] - except KeyError: - return None - - def pertain(self, name): - return name.lower() in self._pertain - - def utczone(self, name): - return name.lower() in self._utczone - - def tzoffset(self, name): - if name in self._utczone: - return 0 - return self.TZOFFSET.get(name) - - def convertyear(self, year): - if year < 100: - year += self._century - if abs(year-self._year) >= 50: - if year < self._year: - year += 100 - else: - year -= 100 - return year - - def validate(self, res): - # move to info - if res.year is not None: - res.year = self.convertyear(res.year) - if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z': - res.tzname = "UTC" - res.tzoffset = 0 - elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname): - res.tzoffset = 0 - return True - - -class parser: - - def __init__(self, info=parserinfo): - if issubclass(info, parserinfo): - self.info = parserinfo() - elif isinstance(info, parserinfo): - self.info = info - else: - raise TypeError("Unsupported parserinfo type") - - def parse(self, timestr, default=None, - ignoretz=False, tzinfos=None, - **kwargs): - if not default: - default = datetime.datetime.now().replace(hour=0, minute=0, - second=0, microsecond=0) - res = self._parse(timestr, **kwargs) - if res is None: - raise ValueError("unknown string format") - repl = {} - for attr in ["year", "month", "day", "hour", - "minute", "second", "microsecond"]: - value = getattr(res, attr) - if value is not None: - repl[attr] = value - ret = default.replace(**repl) - if res.weekday is not None and not res.day: - ret = ret+relativedelta.relativedelta(weekday=res.weekday) - if not ignoretz: - if callable(tzinfos) or tzinfos and res.tzname in tzinfos: - if callable(tzinfos): - tzdata = tzinfos(res.tzname, res.tzoffset) - else: - tzdata = tzinfos.get(res.tzname) - if isinstance(tzdata, datetime.tzinfo): - tzinfo = tzdata - elif isinstance(tzdata, str): - tzinfo = tz.tzstr(tzdata) - elif isinstance(tzdata, int): - tzinfo = tz.tzoffset(res.tzname, tzdata) - else: - raise ValueError("offset must be tzinfo subclass, " \ - "tz string, or int offset") - ret = ret.replace(tzinfo=tzinfo) - elif res.tzname and res.tzname in time.tzname: - ret = ret.replace(tzinfo=tz.tzlocal()) - elif res.tzoffset == 0: - ret = ret.replace(tzinfo=tz.tzutc()) - elif res.tzoffset: - ret = ret.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) - return ret - - class _result(_resultbase): - __slots__ = ["year", "month", "day", "weekday", - "hour", "minute", "second", "microsecond", - "tzname", "tzoffset"] - - def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False): - info = self.info - if dayfirst is None: - dayfirst = info.dayfirst - if yearfirst is None: - yearfirst = info.yearfirst - res = self._result() - l = _timelex.split(timestr) - try: - - # year/month/day list - ymd = [] - - # Index of the month string in ymd - mstridx = -1 - - len_l = len(l) - i = 0 - while i < len_l: - - # Check if it's a number - try: - value = float(l[i]) - except ValueError: - value = None - if value is not None: - # Token is a number - len_li = len(l[i]) - i += 1 - if (len(ymd) == 3 and len_li in (2, 4) - and (i >= len_l or (l[i] != ':' and - info.hms(l[i]) is None))): - # 19990101T23[59] - s = l[i-1] - res.hour = int(s[:2]) - if len_li == 4: - res.minute = int(s[2:]) - elif len_li == 6 or (len_li > 6 and l[i-1].find('.') == 6): - # YYMMDD or HHMMSS[.ss] - s = l[i-1] - if not ymd and l[i-1].find('.') == -1: - ymd.append(info.convertyear(int(s[:2]))) - ymd.append(int(s[2:4])) - ymd.append(int(s[4:])) - else: - # 19990101T235959[.59] - res.hour = int(s[:2]) - res.minute = int(s[2:4]) - value = float(s[4:]) - res.second = int(value) - if value%1: - res.microsecond = int(1000000*(value%1)) - elif len_li == 8: - # YYYYMMDD - s = l[i-1] - ymd.append(int(s[:4])) - ymd.append(int(s[4:6])) - ymd.append(int(s[6:])) - elif len_li in (12, 14): - # YYYYMMDDhhmm[ss] - s = l[i-1] - ymd.append(int(s[:4])) - ymd.append(int(s[4:6])) - ymd.append(int(s[6:8])) - res.hour = int(s[8:10]) - res.minute = int(s[10:12]) - if len_li == 14: - res.second = int(s[12:]) - elif ((i < len_l and info.hms(l[i]) is not None) or - (i+1 < len_l and l[i] == ' ' and - info.hms(l[i+1]) is not None)): - # HH[ ]h or MM[ ]m or SS[.ss][ ]s - if l[i] == ' ': - i += 1 - idx = info.hms(l[i]) - while True: - if idx == 0: - res.hour = int(value) - if value%1: - res.minute = int(60*(value%1)) - elif idx == 1: - res.minute = int(value) - if value%1: - res.second = int(60*(value%1)) - elif idx == 2: - res.second = int(value) - if value%1: - res.microsecond = int(1000000*(value%1)) - i += 1 - if i >= len_l or idx == 2: - break - # 12h00 - try: - value = float(l[i]) - except ValueError: - break - else: - i += 1 - idx += 1 - if i < len_l: - newidx = info.hms(l[i]) - if newidx is not None: - idx = newidx - elif i+1 < len_l and l[i] == ':': - # HH:MM[:SS[.ss]] - res.hour = int(value) - i += 1 - value = float(l[i]) - res.minute = int(value) - if value%1: - res.second = int(60*(value%1)) - i += 1 - if i < len_l and l[i] == ':': - value = float(l[i+1]) - res.second = int(value) - if value%1: - res.microsecond = int(1000000*(value%1)) - i += 2 - elif i < len_l and l[i] in ('-', '/', '.'): - sep = l[i] - ymd.append(int(value)) - i += 1 - if i < len_l and not info.jump(l[i]): - try: - # 01-01[-01] - ymd.append(int(l[i])) - except ValueError: - # 01-Jan[-01] - value = info.month(l[i]) - if value is not None: - ymd.append(value) - assert mstridx == -1 - mstridx = len(ymd)-1 - else: - return None - i += 1 - if i < len_l and l[i] == sep: - # We have three members - i += 1 - value = info.month(l[i]) - if value is not None: - ymd.append(value) - mstridx = len(ymd)-1 - assert mstridx == -1 - else: - ymd.append(int(l[i])) - i += 1 - elif i >= len_l or info.jump(l[i]): - if i+1 < len_l and info.ampm(l[i+1]) is not None: - # 12 am - res.hour = int(value) - if res.hour < 12 and info.ampm(l[i+1]) == 1: - res.hour += 12 - elif res.hour == 12 and info.ampm(l[i+1]) == 0: - res.hour = 0 - i += 1 - else: - # Year, month or day - ymd.append(int(value)) - i += 1 - elif info.ampm(l[i]) is not None: - # 12am - res.hour = int(value) - if res.hour < 12 and info.ampm(l[i]) == 1: - res.hour += 12 - elif res.hour == 12 and info.ampm(l[i]) == 0: - res.hour = 0 - i += 1 - elif not fuzzy: - return None - else: - i += 1 - continue - - # Check weekday - value = info.weekday(l[i]) - if value is not None: - res.weekday = value - i += 1 - continue - - # Check month name - value = info.month(l[i]) - if value is not None: - ymd.append(value) - assert mstridx == -1 - mstridx = len(ymd)-1 - i += 1 - if i < len_l: - if l[i] in ('-', '/'): - # Jan-01[-99] - sep = l[i] - i += 1 - ymd.append(int(l[i])) - i += 1 - if i < len_l and l[i] == sep: - # Jan-01-99 - i += 1 - ymd.append(int(l[i])) - i += 1 - elif (i+3 < len_l and l[i] == l[i+2] == ' ' - and info.pertain(l[i+1])): - # Jan of 01 - # In this case, 01 is clearly year - try: - value = int(l[i+3]) - except ValueError: - # Wrong guess - pass - else: - # Convert it here to become unambiguous - ymd.append(info.convertyear(value)) - i += 4 - continue - - # Check am/pm - value = info.ampm(l[i]) - if value is not None: - if value == 1 and res.hour < 12: - res.hour += 12 - elif value == 0 and res.hour == 12: - res.hour = 0 - i += 1 - continue - - # Check for a timezone name - if (res.hour is not None and len(l[i]) <= 5 and - res.tzname is None and res.tzoffset is None and - not [x for x in l[i] if x not in string.ascii_uppercase]): - res.tzname = l[i] - res.tzoffset = info.tzoffset(res.tzname) - i += 1 - - # Check for something like GMT+3, or BRST+3. Notice - # that it doesn't mean "I am 3 hours after GMT", but - # "my time +3 is GMT". If found, we reverse the - # logic so that timezone parsing code will get it - # right. - if i < len_l and l[i] in ('+', '-'): - l[i] = ('+', '-')[l[i] == '+'] - res.tzoffset = None - if info.utczone(res.tzname): - # With something like GMT+3, the timezone - # is *not* GMT. - res.tzname = None - - continue - - # Check for a numbered timezone - if res.hour is not None and l[i] in ('+', '-'): - signal = (-1,1)[l[i] == '+'] - i += 1 - len_li = len(l[i]) - if len_li == 4: - # -0300 - res.tzoffset = int(l[i][:2])*3600+int(l[i][2:])*60 - elif i+1 < len_l and l[i+1] == ':': - # -03:00 - res.tzoffset = int(l[i])*3600+int(l[i+2])*60 - i += 2 - elif len_li <= 2: - # -[0]3 - res.tzoffset = int(l[i][:2])*3600 - else: - return None - i += 1 - res.tzoffset *= signal - - # Look for a timezone name between parenthesis - if (i+3 < len_l and - info.jump(l[i]) and l[i+1] == '(' and l[i+3] == ')' and - 3 <= len(l[i+2]) <= 5 and - not [x for x in l[i+2] - if x not in string.ascii_uppercase]): - # -0300 (BRST) - res.tzname = l[i+2] - i += 4 - continue - - # Check jumps - if not (info.jump(l[i]) or fuzzy): - return None - - i += 1 - - # Process year/month/day - len_ymd = len(ymd) - if len_ymd > 3: - # More than three members!? - return None - elif len_ymd == 1 or (mstridx != -1 and len_ymd == 2): - # One member, or two members with a month string - if mstridx != -1: - res.month = ymd[mstridx] - del ymd[mstridx] - if len_ymd > 1 or mstridx == -1: - if ymd[0] > 31: - res.year = ymd[0] - else: - res.day = ymd[0] - elif len_ymd == 2: - # Two members with numbers - if ymd[0] > 31: - # 99-01 - res.year, res.month = ymd - elif ymd[1] > 31: - # 01-99 - res.month, res.year = ymd - elif dayfirst and ymd[1] <= 12: - # 13-01 - res.day, res.month = ymd - else: - # 01-13 - res.month, res.day = ymd - if len_ymd == 3: - # Three members - if mstridx == 0: - res.month, res.day, res.year = ymd - elif mstridx == 1: - if ymd[0] > 31 or (yearfirst and ymd[2] <= 31): - # 99-Jan-01 - res.year, res.month, res.day = ymd - else: - # 01-Jan-01 - # Give precendence to day-first, since - # two-digit years is usually hand-written. - res.day, res.month, res.year = ymd - elif mstridx == 2: - # WTF!? - if ymd[1] > 31: - # 01-99-Jan - res.day, res.year, res.month = ymd - else: - # 99-01-Jan - res.year, res.day, res.month = ymd - else: - if ymd[0] > 31 or \ - (yearfirst and ymd[1] <= 12 and ymd[2] <= 31): - # 99-01-01 - res.year, res.month, res.day = ymd - elif ymd[0] > 12 or (dayfirst and ymd[1] <= 12): - # 13-01-01 - res.day, res.month, res.year = ymd - else: - # 01-13-01 - res.month, res.day, res.year = ymd - - except (IndexError, ValueError, AssertionError): - return None - - if not info.validate(res): - return None - return res - -DEFAULTPARSER = parser() -def parse(timestr, parserinfo=None, **kwargs): - if parserinfo: - return parser(parserinfo).parse(timestr, **kwargs) - else: - return DEFAULTPARSER.parse(timestr, **kwargs) - -class _tzparser: - - class _result(_resultbase): - - __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset", - "start", "end"] - - class _attr(_resultbase): - __slots__ = ["month", "week", "weekday", - "yday", "jyday", "day", "time"] - - def __repr__(self): - return self._repr("") - - def __init__(self): - _resultbase.__init__(self) - self.start = self._attr() - self.end = self._attr() - - def parse(self, tzstr): - res = self._result() - l = _timelex.split(tzstr) - try: - - len_l = len(l) - - i = 0 - while i < len_l: - # BRST+3[BRDT[+2]] - j = i - while j < len_l and not [x for x in l[j] - if x in "0123456789:,-+"]: - j += 1 - if j != i: - if not res.stdabbr: - offattr = "stdoffset" - res.stdabbr = "".join(l[i:j]) - else: - offattr = "dstoffset" - res.dstabbr = "".join(l[i:j]) - i = j - if (i < len_l and - (l[i] in ('+', '-') or l[i][0] in "0123456789")): - if l[i] in ('+', '-'): - signal = (1,-1)[l[i] == '+'] - i += 1 - else: - signal = -1 - len_li = len(l[i]) - if len_li == 4: - # -0300 - setattr(res, offattr, - (int(l[i][:2])*3600+int(l[i][2:])*60)*signal) - elif i+1 < len_l and l[i+1] == ':': - # -03:00 - setattr(res, offattr, - (int(l[i])*3600+int(l[i+2])*60)*signal) - i += 2 - elif len_li <= 2: - # -[0]3 - setattr(res, offattr, - int(l[i][:2])*3600*signal) - else: - return None - i += 1 - if res.dstabbr: - break - else: - break - - if i < len_l: - for j in range(i, len_l): - if l[j] == ';': l[j] = ',' - - assert l[i] == ',' - - i += 1 - - if i >= len_l: - pass - elif (8 <= l.count(',') <= 9 and - not [y for x in l[i:] if x != ',' - for y in x if y not in "0123456789"]): - # GMT0BST,3,0,30,3600,10,0,26,7200[,3600] - for x in (res.start, res.end): - x.month = int(l[i]) - i += 2 - if l[i] == '-': - value = int(l[i+1])*-1 - i += 1 - else: - value = int(l[i]) - i += 2 - if value: - x.week = value - x.weekday = (int(l[i])-1)%7 - else: - x.day = int(l[i]) - i += 2 - x.time = int(l[i]) - i += 2 - if i < len_l: - if l[i] in ('-','+'): - signal = (-1,1)[l[i] == "+"] - i += 1 - else: - signal = 1 - res.dstoffset = (res.stdoffset+int(l[i]))*signal - elif (l.count(',') == 2 and l[i:].count('/') <= 2 and - not [y for x in l[i:] if x not in (',','/','J','M', - '.','-',':') - for y in x if y not in "0123456789"]): - for x in (res.start, res.end): - if l[i] == 'J': - # non-leap year day (1 based) - i += 1 - x.jyday = int(l[i]) - elif l[i] == 'M': - # month[-.]week[-.]weekday - i += 1 - x.month = int(l[i]) - i += 1 - assert l[i] in ('-', '.') - i += 1 - x.week = int(l[i]) - if x.week == 5: - x.week = -1 - i += 1 - assert l[i] in ('-', '.') - i += 1 - x.weekday = (int(l[i])-1)%7 - else: - # year day (zero based) - x.yday = int(l[i])+1 - - i += 1 - - if i < len_l and l[i] == '/': - i += 1 - # start time - len_li = len(l[i]) - if len_li == 4: - # -0300 - x.time = (int(l[i][:2])*3600+int(l[i][2:])*60) - elif i+1 < len_l and l[i+1] == ':': - # -03:00 - x.time = int(l[i])*3600+int(l[i+2])*60 - i += 2 - if i+1 < len_l and l[i+1] == ':': - i += 2 - x.time += int(l[i]) - elif len_li <= 2: - # -[0]3 - x.time = (int(l[i][:2])*3600) - else: - return None - i += 1 - - assert i == len_l or l[i] == ',' - - i += 1 - - assert i >= len_l - - except (IndexError, ValueError, AssertionError): - return None - - return res - -DEFAULTTZPARSER = _tzparser() -def _parsetz(tzstr): - return DEFAULTTZPARSER.parse(tzstr) - -# vim:ts=4:sw=4:et diff --git a/src/libs/python/bongo/external/dateutil/relativedelta.py b/src/libs/python/bongo/external/dateutil/relativedelta.py deleted file mode 100644 index dc6c76e..0000000 --- a/src/libs/python/bongo/external/dateutil/relativedelta.py +++ /dev/null @@ -1,432 +0,0 @@ -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -import datetime -import calendar - -__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] - -class weekday(object): - __slots__ = ["weekday", "n"] - - def __init__(self, weekday, n=None): - self.weekday = weekday - self.n = n - - def __call__(self, n): - if n == self.n: - return self - else: - return self.__class__(self.weekday, n) - - def __eq__(self, other): - try: - if self.weekday != other.weekday or self.n != other.n: - return False - except AttributeError: - return False - return True - - def __repr__(self): - s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] - if not self.n: - return s - else: - return "%s(%+d)" % (s, self.n) - -MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) - -class relativedelta: - """ -The relativedelta type is based on the specification of the excelent -work done by M.-A. Lemburg in his mx.DateTime extension. However, -notice that this type does *NOT* implement the same algorithm as -his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. - -There's two different ways to build a relativedelta instance. The -first one is passing it two date/datetime classes: - - relativedelta(datetime1, datetime2) - -And the other way is to use the following keyword arguments: - - year, month, day, hour, minute, second, microsecond: - Absolute information. - - years, months, weeks, days, hours, minutes, seconds, microseconds: - Relative information, may be negative. - - weekday: - One of the weekday instances (MO, TU, etc). These instances may - receive a parameter N, specifying the Nth weekday, which could - be positive or negative (like MO(+1) or MO(-2). Not specifying - it is the same as specifying +1. You can also use an integer, - where 0=MO. - - leapdays: - Will add given days to the date found, if year is a leap - year, and the date found is post 28 of february. - - yearday, nlyearday: - Set the yearday or the non-leap year day (jump leap days). - These are converted to day/month/leapdays information. - -Here is the behavior of operations with relativedelta: - -1) Calculate the absolute year, using the 'year' argument, or the - original datetime year, if the argument is not present. - -2) Add the relative 'years' argument to the absolute year. - -3) Do steps 1 and 2 for month/months. - -4) Calculate the absolute day, using the 'day' argument, or the - original datetime day, if the argument is not present. Then, - subtract from the day until it fits in the year and month - found after their operations. - -5) Add the relative 'days' argument to the absolute day. Notice - that the 'weeks' argument is multiplied by 7 and added to - 'days'. - -6) Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds, - microsecond/microseconds. - -7) If the 'weekday' argument is present, calculate the weekday, - with the given (wday, nth) tuple. wday is the index of the - weekday (0-6, 0=Mon), and nth is the number of weeks to add - forward or backward, depending on its signal. Notice that if - the calculated date is already Monday, for example, using - (0, 1) or (0, -1) won't change the day. - """ - - def __init__(self, dt1=None, dt2=None, - years=0, months=0, days=0, leapdays=0, weeks=0, - hours=0, minutes=0, seconds=0, microseconds=0, - year=None, month=None, day=None, weekday=None, - yearday=None, nlyearday=None, - hour=None, minute=None, second=None, microsecond=None): - if dt1 and dt2: - if not isinstance(dt1, datetime.date) or \ - not isinstance(dt2, datetime.date): - raise TypeError("relativedelta only diffs datetime/date") - if type(dt1) is not type(dt2): - if not isinstance(dt1, datetime.datetime): - dt1 = datetime.datetime.fromordinal(dt1.toordinal()) - elif not isinstance(dt2, datetime.datetime): - dt2 = datetime.datetime.fromordinal(dt2.toordinal()) - self.years = 0 - self.months = 0 - self.days = 0 - self.leapdays = 0 - self.hours = 0 - self.minutes = 0 - self.seconds = 0 - self.microseconds = 0 - self.year = None - self.month = None - self.day = None - self.weekday = None - self.hour = None - self.minute = None - self.second = None - self.microsecond = None - self._has_time = 0 - - months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month) - self._set_months(months) - dtm = self.__radd__(dt2) - if dt1 < dt2: - while dt1 > dtm: - months += 1 - self._set_months(months) - dtm = self.__radd__(dt2) - else: - while dt1 < dtm: - months -= 1 - self._set_months(months) - dtm = self.__radd__(dt2) - delta = dt1 - dtm - self.seconds = delta.seconds+delta.days*86400 - self.microseconds = delta.microseconds - else: - self.years = years - self.months = months - self.days = days+weeks*7 - self.leapdays = leapdays - self.hours = hours - self.minutes = minutes - self.seconds = seconds - self.microseconds = microseconds - self.year = year - self.month = month - self.day = day - self.hour = hour - self.minute = minute - self.second = second - self.microsecond = microsecond - - if type(weekday) is int: - self.weekday = weekdays[weekday] - else: - self.weekday = weekday - - yday = 0 - if nlyearday: - yday = nlyearday - elif yearday: - yday = yearday - if yearday > 59: - self.leapdays = -1 - if yday: - ydayidx = [31,59,90,120,151,181,212,243,273,304,334,366] - for idx, ydays in enumerate(ydayidx): - if yday <= ydays: - self.month = idx+1 - if idx == 0: - self.day = ydays - else: - self.day = yday-ydayidx[idx-1] - break - else: - raise ValueError("invalid year day (%d)" % yday) - - self._fix() - - def _fix(self): - if abs(self.microseconds) > 999999: - s = self.microseconds/abs(self.microseconds) - div, mod = divmod(self.microseconds*s, 1000000) - self.microseconds = mod*s - self.seconds += div*s - if abs(self.seconds) > 59: - s = self.seconds/abs(self.seconds) - div, mod = divmod(self.seconds*s, 60) - self.seconds = mod*s - self.minutes += div*s - if abs(self.minutes) > 59: - s = self.minutes/abs(self.minutes) - div, mod = divmod(self.minutes*s, 60) - self.minutes = mod*s - self.hours += div*s - if abs(self.hours) > 23: - s = self.hours/abs(self.hours) - div, mod = divmod(self.hours*s, 24) - self.hours = mod*s - self.days += div*s - if abs(self.months) > 11: - s = self.months/abs(self.months) - div, mod = divmod(self.months*s, 12) - self.months = mod*s - self.years += div*s - if (self.hours or self.minutes or self.seconds or self.microseconds or - self.hour is not None or self.minute is not None or - self.second is not None or self.microsecond is not None): - self._has_time = 1 - else: - self._has_time = 0 - - def _set_months(self, months): - self.months = months - if abs(self.months) > 11: - s = self.months/abs(self.months) - div, mod = divmod(self.months*s, 12) - self.months = mod*s - self.years = div*s - else: - self.years = 0 - - def __radd__(self, other): - if not isinstance(other, datetime.date): - raise TypeError("unsupported type for add operation") - elif self._has_time and not isinstance(other, datetime.datetime): - other = datetime.datetime.fromordinal(other.toordinal()) - year = (self.year or other.year)+self.years - month = self.month or other.month - if self.months: - assert 1 <= abs(self.months) <= 12 - month += self.months - if month > 12: - year += 1 - month -= 12 - elif month < 1: - year -= 1 - month += 12 - day = min(calendar.monthrange(year, month)[1], - self.day or other.day) - repl = {"year": year, "month": month, "day": day} - for attr in ["hour", "minute", "second", "microsecond"]: - value = getattr(self, attr) - if value is not None: - repl[attr] = value - days = self.days - if self.leapdays and month > 2 and calendar.isleap(year): - days += self.leapdays - ret = (other.replace(**repl) - + datetime.timedelta(days=days, - hours=self.hours, - minutes=self.minutes, - seconds=self.seconds, - microseconds=self.microseconds)) - if self.weekday: - weekday, nth = self.weekday.weekday, self.weekday.n or 1 - jumpdays = (abs(nth)-1)*7 - if nth > 0: - jumpdays += (7-ret.weekday()+weekday)%7 - else: - jumpdays += (ret.weekday()-weekday)%7 - jumpdays *= -1 - ret += datetime.timedelta(days=jumpdays) - return ret - - def __rsub__(self, other): - return self.__neg__().__radd__(other) - - def __add__(self, other): - if not isinstance(other, relativedelta): - raise TypeError("unsupported type for add operation") - return relativedelta(years=other.years+self.years, - months=other.months+self.months, - days=other.days+self.days, - hours=other.hours+self.hours, - minutes=other.minutes+self.minutes, - seconds=other.seconds+self.seconds, - microseconds=other.microseconds+self.microseconds, - leapdays=other.leapdays or self.leapdays, - year=other.year or self.year, - month=other.month or self.month, - day=other.day or self.day, - weekday=other.weekday or self.weekday, - hour=other.hour or self.hour, - minute=other.minute or self.minute, - second=other.second or self.second, - microsecond=other.second or self.microsecond) - - def __sub__(self, other): - if not isinstance(other, relativedelta): - raise TypeError("unsupported type for sub operation") - return relativedelta(years=other.years-self.years, - months=other.months-self.months, - days=other.days-self.days, - hours=other.hours-self.hours, - minutes=other.minutes-self.minutes, - seconds=other.seconds-self.seconds, - microseconds=other.microseconds-self.microseconds, - leapdays=other.leapdays or self.leapdays, - year=other.year or self.year, - month=other.month or self.month, - day=other.day or self.day, - weekday=other.weekday or self.weekday, - hour=other.hour or self.hour, - minute=other.minute or self.minute, - second=other.second or self.second, - microsecond=other.second or self.microsecond) - - def __neg__(self): - return relativedelta(years=-self.years, - months=-self.months, - days=-self.days, - hours=-self.hours, - minutes=-self.minutes, - seconds=-self.seconds, - microseconds=-self.microseconds, - leapdays=self.leapdays, - year=self.year, - month=self.month, - day=self.day, - weekday=self.weekday, - hour=self.hour, - minute=self.minute, - second=self.second, - microsecond=self.microsecond) - - def __bool__(self): - return not (not self.years and - not self.months and - not self.days and - not self.hours and - not self.minutes and - not self.seconds and - not self.microseconds and - not self.leapdays and - self.year is None and - self.month is None and - self.day is None and - self.weekday is None and - self.hour is None and - self.minute is None and - self.second is None and - self.microsecond is None) - - def __mul__(self, other): - f = float(other) - return relativedelta(years=self.years*f, - months=self.months*f, - days=self.days*f, - hours=self.hours*f, - minutes=self.minutes*f, - seconds=self.seconds*f, - microseconds=self.microseconds*f, - leapdays=self.leapdays, - year=self.year, - month=self.month, - day=self.day, - weekday=self.weekday, - hour=self.hour, - minute=self.minute, - second=self.second, - microsecond=self.microsecond) - - def __eq__(self, other): - if not isinstance(other, relativedelta): - return False - if self.weekday or other.weekday: - if not self.weekday or not other.weekday: - return False - if self.weekday.weekday != other.weekday.weekday: - return False - n1, n2 = self.weekday.n, other.weekday.n - if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): - return False - return (self.years == other.years and - self.months == other.months and - self.days == other.days and - self.hours == other.hours and - self.minutes == other.minutes and - self.seconds == other.seconds and - self.leapdays == other.leapdays and - self.year == other.year and - self.month == other.month and - self.day == other.day and - self.hour == other.hour and - self.minute == other.minute and - self.second == other.second and - self.microsecond == other.microsecond) - - def __ne__(self, other): - return not self.__eq__(other) - - def __div__(self, other): - return self.__mul__(1/float(other)) - - def __repr__(self): - l = [] - for attr in ["years", "months", "days", "leapdays", - "hours", "minutes", "seconds", "microseconds"]: - value = getattr(self, attr) - if value: - l.append("%s=%+d" % (attr, value)) - for attr in ["year", "month", "day", "weekday", - "hour", "minute", "second", "microsecond"]: - value = getattr(self, attr) - if value is not None: - l.append("%s=%s" % (attr, repr(value))) - return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) - -# vim:ts=4:sw=4:et diff --git a/src/libs/python/bongo/external/dateutil/rrule.py b/src/libs/python/bongo/external/dateutil/rrule.py deleted file mode 100644 index ec80b81..0000000 --- a/src/libs/python/bongo/external/dateutil/rrule.py +++ /dev/null @@ -1,1090 +0,0 @@ -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -import itertools -import datetime -import calendar -import _thread -import sys - -__all__ = ["rrule", "rruleset", "rrulestr", - "YEARLY", "MONTHLY", "WEEKLY", "DAILY", - "HOURLY", "MINUTELY", "SECONDLY", - "MO", "TU", "WE", "TH", "FR", "SA", "SU"] - -# Every mask is 7 days longer to handle cross-year weekly periods. -M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30+ - [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) -M365MASK = list(M366MASK) -M29, M30, M31 = list(range(1,30)), list(range(1,31)), list(range(1,32)) -MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) -MDAY365MASK = list(MDAY366MASK) -M29, M30, M31 = list(range(-29,0)), list(range(-30,0)), list(range(-31,0)) -NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) -NMDAY365MASK = list(NMDAY366MASK) -M366RANGE = (0,31,60,91,121,152,182,213,244,274,305,335,366) -M365RANGE = (0,31,59,90,120,151,181,212,243,273,304,334,365) -WDAYMASK = [0,1,2,3,4,5,6]*55 -del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] -MDAY365MASK = tuple(MDAY365MASK) -M365MASK = tuple(M365MASK) - -(YEARLY, - MONTHLY, - WEEKLY, - DAILY, - HOURLY, - MINUTELY, - SECONDLY) = list(range(7)) - -# Imported on demand. -easter = None -parser = None - -class weekday(object): - __slots__ = ["weekday", "n"] - - def __init__(self, weekday, n=None): - if n == 0: - raise ValueError("Can't create weekday with n == 0") - self.weekday = weekday - self.n = n - - def __call__(self, n): - if n == self.n: - return self - else: - return self.__class__(self.weekday, n) - - def __eq__(self, other): - try: - if self.weekday != other.weekday or self.n != other.n: - return False - except AttributeError: - return False - return True - - def __repr__(self): - s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] - if not self.n: - return s - else: - return "%s(%+d)" % (s, self.n) - -MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) - -class rrulebase: - def __init__(self, cache=False): - if cache: - self._cache = [] - self._cache_lock = _thread.allocate_lock() - self._cache_gen = self._iter() - self._cache_complete = False - else: - self._cache = None - self._cache_complete = False - self._len = None - - def __iter__(self): - if self._cache_complete: - return iter(self._cache) - elif self._cache is None: - return self._iter() - else: - return self._iter_cached() - - def _iter_cached(self): - i = 0 - gen = self._cache_gen - cache = self._cache - acquire = self._cache_lock.acquire - release = self._cache_lock.release - while gen: - if i == len(cache): - acquire() - if self._cache_complete: - break - try: - for j in range(10): - cache.append(next(gen)) - except StopIteration: - self._cache_gen = gen = None - self._cache_complete = True - break - release() - yield cache[i] - i += 1 - while i < self._len: - yield cache[i] - i += 1 - - def __getitem__(self, item): - if self._cache_complete: - return self._cache[item] - elif isinstance(item, slice): - if item.step and item.step < 0: - return list(iter(self))[item] - else: - return list(itertools.islice(self, - item.start or 0, - item.stop or sys.maxsize, - item.step or 1)) - elif item >= 0: - gen = iter(self) - try: - for i in range(item+1): - res = next(gen) - except StopIteration: - raise IndexError - return res - else: - return list(iter(self))[item] - - def __contains__(self, item): - if self._cache_complete: - return item in self._cache - else: - for i in self: - if i == item: - return True - elif i > item: - return False - return False - - # __len__() introduces a large performance penality. - def count(self): - if self._len is None: - for x in self: pass - return self._len - - def before(self, dt, inc=False): - if self._cache_complete: - gen = self._cache - else: - gen = self - last = None - if inc: - for i in gen: - if i > dt: - break - last = i - else: - for i in gen: - if i >= dt: - break - last = i - return last - - def after(self, dt, inc=False): - if self._cache_complete: - gen = self._cache - else: - gen = self - if inc: - for i in gen: - if i >= dt: - return i - else: - for i in gen: - if i > dt: - return i - return None - - def between(self, after, before, inc=False): - if self._cache_complete: - gen = self._cache - else: - gen = self - started = False - l = [] - if inc: - for i in gen: - if i > before: - break - elif not started: - if i >= after: - started = True - l.append(i) - else: - l.append(i) - else: - for i in gen: - if i >= before: - break - elif not started: - if i > after: - started = True - l.append(i) - else: - l.append(i) - return l - -class rrule(rrulebase): - def __init__(self, freq, dtstart=None, - interval=1, wkst=None, count=None, until=None, bysetpos=None, - bymonth=None, bymonthday=None, byyearday=None, byeaster=None, - byweekno=None, byweekday=None, - byhour=None, byminute=None, bysecond=None, - cache=False): - rrulebase.__init__(self, cache) - global easter - if not dtstart: - dtstart = datetime.datetime.now().replace(microsecond=0) - elif not isinstance(dtstart, datetime.datetime): - dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) - else: - dtstart = dtstart.replace(microsecond=0) - self._dtstart = dtstart - self._tzinfo = dtstart.tzinfo - self._freq = freq - self._interval = interval - self._count = count - if until and not isinstance(until, datetime.datetime): - until = datetime.datetime.fromordinal(until.toordinal()) - self._until = until - if wkst is None: - self._wkst = calendar.firstweekday() - elif type(wkst) is int: - self._wkst = wkst - else: - self._wkst = wkst.weekday - if bysetpos is None: - self._bysetpos = None - elif type(bysetpos) is int: - self._bysetpos = (bysetpos,) - else: - self._bysetpos = tuple(bysetpos) - if not (byweekno or byyearday or bymonthday or - byweekday is not None or byeaster is not None): - if freq == YEARLY: - if not bymonth: - bymonth = dtstart.month - bymonthday = dtstart.day - elif freq == MONTHLY: - bymonthday = dtstart.day - elif freq == WEEKLY: - byweekday = dtstart.weekday() - # bymonth - if not bymonth: - self._bymonth = None - elif type(bymonth) is int: - self._bymonth = (bymonth,) - else: - self._bymonth = tuple(bymonth) - # byyearday - if not byyearday: - self._byyearday = None - elif type(byyearday) is int: - self._byyearday = (byyearday,) - else: - self._byyearday = tuple(byyearday) - # byeaster - if byeaster is not None: - if not easter: - from dateutil import easter - if type(byeaster) is int: - self._byeaster = (byeaster,) - else: - self._byeaster = tuple(byeaster) - else: - self._byeaster = None - # bymonthay - if not bymonthday: - self._bymonthday = () - self._bynmonthday = () - elif type(bymonthday) is int: - if bymonthday < 0: - self._bynmonthday = (bymonthday,) - self._bymonthday = () - else: - self._bymonthday = (bymonthday,) - self._bynmonthday = () - else: - self._bymonthday = tuple([x for x in bymonthday if x > 0]) - self._bynmonthday = tuple([x for x in bymonthday if x < 0]) - # byweekno - if byweekno is None: - self._byweekno = None - elif type(byweekno) is int: - self._byweekno = (byweekno,) - else: - self._byweekno = tuple(byweekno) - # byweekday / bynweekday - if byweekday is None: - self._byweekday = None - self._bynweekday = None - elif type(byweekday) is int: - self._byweekday = (byweekday,) - self._bynweekday = None - elif hasattr(byweekday, "n"): - if not byweekday.n or freq > MONTHLY: - self._byweekday = (byweekday.weekday,) - self._bynweekday = None - else: - self._bynweekday = ((byweekday.weekday, byweekday.n),) - self._byweekday = None - else: - self._byweekday = [] - self._bynweekday = [] - for wday in byweekday: - if type(wday) is int: - self._byweekday.append(wday) - elif not wday.n or freq > MONTHLY: - self._byweekday.append(wday.weekday) - else: - self._bynweekday.append((wday.weekday, wday.n)) - self._byweekday = tuple(self._byweekday) - self._bynweekday = tuple(self._bynweekday) - if not self._byweekday: - self._byweekday = None - elif not self._bynweekday: - self._bynweekday = None - # byhour - if byhour is None: - if freq < HOURLY: - self._byhour = (dtstart.hour,) - else: - self._byhour = None - elif type(byhour) is int: - self._byhour = (byhour,) - else: - self._byhour = tuple(byhour) - # byminute - if byminute is None: - if freq < MINUTELY: - self._byminute = (dtstart.minute,) - else: - self._byminute = None - elif type(byminute) is int: - self._byminute = (byminute,) - else: - self._byminute = tuple(byminute) - # bysecond - if bysecond is None: - if freq < SECONDLY: - self._bysecond = (dtstart.second,) - else: - self._bysecond = None - elif type(bysecond) is int: - self._bysecond = (bysecond,) - else: - self._bysecond = tuple(bysecond) - - if self._freq >= HOURLY: - self._timeset = None - else: - self._timeset = [] - for hour in self._byhour: - for minute in self._byminute: - for second in self._bysecond: - self._timeset.append( - datetime.time(hour, minute, second, - tzinfo=self._tzinfo)) - self._timeset.sort() - self._timeset = tuple(self._timeset) - - def _iter(self): - year, month, day, hour, minute, second, weekday, yearday, _ = \ - self._dtstart.timetuple() - - # Some local variables to speed things up a bit - freq = self._freq - interval = self._interval - wkst = self._wkst - until = self._until - bymonth = self._bymonth - byweekno = self._byweekno - byyearday = self._byyearday - byweekday = self._byweekday - byeaster = self._byeaster - bymonthday = self._bymonthday - bynmonthday = self._bynmonthday - bysetpos = self._bysetpos - byhour = self._byhour - byminute = self._byminute - bysecond = self._bysecond - - ii = _iterinfo(self) - ii.rebuild(year, month) - - getdayset = {YEARLY:ii.ydayset, - MONTHLY:ii.mdayset, - WEEKLY:ii.wdayset, - DAILY:ii.ddayset, - HOURLY:ii.ddayset, - MINUTELY:ii.ddayset, - SECONDLY:ii.ddayset}[freq] - - if freq < HOURLY: - timeset = self._timeset - else: - gettimeset = {HOURLY:ii.htimeset, - MINUTELY:ii.mtimeset, - SECONDLY:ii.stimeset}[freq] - if ((freq >= HOURLY and - self._byhour and hour not in self._byhour) or - (freq >= MINUTELY and - self._byminute and minute not in self._byminute) or - (freq >= SECONDLY and - self._bysecond and minute not in self._bysecond)): - timeset = () - else: - timeset = gettimeset(hour, minute, second) - - total = 0 - count = self._count - while True: - # Get dayset with the right frequency - dayset, start, end = getdayset(year, month, day) - - # Do the "hard" work ;-) - filtered = False - for i in dayset[start:end]: - if ((bymonth and ii.mmask[i] not in bymonth) or - (byweekno and not ii.wnomask[i]) or - (byweekday and ii.wdaymask[i] not in byweekday) or - (ii.nwdaymask and not ii.nwdaymask[i]) or - (byeaster and not ii.eastermask[i]) or - ((bymonthday or bynmonthday) and - ii.mdaymask[i] not in bymonthday and - ii.nmdaymask[i] not in bynmonthday) or - (byyearday and - ((i < ii.yearlen and i+1 not in byyearday - and -ii.yearlen+i not in byyearday) or - (i >= ii.yearlen and i+1-ii.yearlen not in byyearday - and -ii.nextyearlen+i-ii.yearlen - not in byyearday)))): - dayset[i] = None - filtered = True - - # Output results - if bysetpos and timeset: - poslist = [] - for pos in bysetpos: - if pos < 0: - daypos, timepos = divmod(pos, len(timeset)) - else: - daypos, timepos = divmod(pos-1, len(timeset)) - try: - i = [x for x in dayset[start:end] - if x is not None][daypos] - time = timeset[timepos] - except IndexError: - pass - else: - date = datetime.date.fromordinal(ii.yearordinal+i) - res = datetime.datetime.combine(date, time) - if res not in poslist: - poslist.append(res) - poslist.sort() - for res in poslist: - if until and res > until: - self._len = total - return - elif res >= self._dtstart: - total += 1 - yield res - if count: - count -= 1 - if not count: - self._len = total - return - else: - for i in dayset[start:end]: - if i is not None: - date = datetime.date.fromordinal(ii.yearordinal+i) - for time in timeset: - res = datetime.datetime.combine(date, time) - if until and res > until: - self._len = total - return - elif res >= self._dtstart: - total += 1 - yield res - if count: - count -= 1 - if not count: - self._len = total - return - - # Handle frequency and interval - fixday = False - if freq == YEARLY: - year += interval - if year > datetime.MAXYEAR: - self._len = total - return - ii.rebuild(year, month) - elif freq == MONTHLY: - month += interval - if month > 12: - div, mod = divmod(month, 12) - month = mod - year += div - if month == 0: - month = 12 - year -= 1 - if year > datetime.MAXYEAR: - self._len = total - return - ii.rebuild(year, month) - elif freq == WEEKLY: - if wkst > weekday: - day += -(weekday+1+(6-wkst))+self._interval*7 - else: - day += -(weekday-wkst)+self._interval*7 - weekday = wkst - fixday = True - elif freq == DAILY: - day += interval - fixday = True - elif freq == HOURLY: - if filtered: - # Jump to one iteration before next day - hour += ((23-hour)//interval)*interval - while True: - hour += interval - div, mod = divmod(hour, 24) - if div: - hour = mod - day += div - fixday = True - if not byhour or hour in byhour: - break - timeset = gettimeset(hour, minute, second) - elif freq == MINUTELY: - if filtered: - # Jump to one iteration before next day - minute += ((1439-(hour*60+minute))//interval)*interval - while True: - minute += interval - div, mod = divmod(minute, 60) - if div: - minute = mod - hour += div - div, mod = divmod(hour, 24) - if div: - hour = mod - day += div - fixday = True - filtered = False - if ((not byhour or hour in byhour) and - (not byminute or minute in byminute)): - break - timeset = gettimeset(hour, minute, second) - elif freq == SECONDLY: - if filtered: - # Jump to one iteration before next day - second += (((86399-(hour*3600+minute*60+second)) - //interval)*interval) - while True: - second += self._interval - div, mod = divmod(second, 60) - if div: - second = mod - minute += div - div, mod = divmod(minute, 60) - if div: - minute = mod - hour += div - div, mod = divmod(hour, 24) - if div: - hour = mod - day += div - fixday = True - if ((not byhour or hour in byhour) and - (not byminute or minute in byminute) and - (not bysecond or second in bysecond)): - break - timeset = gettimeset(hour, minute, second) - - if fixday and day > 28: - daysinmonth = calendar.monthrange(year, month)[1] - if day > daysinmonth: - while day > daysinmonth: - day -= daysinmonth - month += 1 - if month == 13: - month = 1 - year += 1 - if year > datetime.MAXYEAR: - self._len = total - return - daysinmonth = calendar.monthrange(year, month)[1] - ii.rebuild(year, month) - -class _iterinfo(object): - __slots__ = ["rrule", "lastyear", "lastmonth", - "yearlen", "nextyearlen", "yearordinal", "yearweekday", - "mmask", "mrange", "mdaymask", "nmdaymask", - "wdaymask", "wnomask", "nwdaymask", "eastermask"] - - def __init__(self, rrule): - for attr in self.__slots__: - setattr(self, attr, None) - self.rrule = rrule - - def rebuild(self, year, month): - # Every mask is 7 days longer to handle cross-year weekly periods. - rr = self.rrule - if year != self.lastyear: - self.yearlen = 365+calendar.isleap(year) - self.nextyearlen = 365+calendar.isleap(year+1) - firstyday = datetime.date(year, 1, 1) - self.yearordinal = firstyday.toordinal() - self.yearweekday = firstyday.weekday() - - wday = datetime.date(year, 1, 1).weekday() - if self.yearlen == 365: - self.mmask = M365MASK - self.mdaymask = MDAY365MASK - self.nmdaymask = NMDAY365MASK - self.wdaymask = WDAYMASK[wday:] - self.mrange = M365RANGE - else: - self.mmask = M366MASK - self.mdaymask = MDAY366MASK - self.nmdaymask = NMDAY366MASK - self.wdaymask = WDAYMASK[wday:] - self.mrange = M366RANGE - - if not rr._byweekno: - self.wnomask = None - else: - self.wnomask = [0]*(self.yearlen+7) - #no1wkst = firstwkst = self.wdaymask.index(rr._wkst) - no1wkst = firstwkst = (7-self.yearweekday+rr._wkst)%7 - if no1wkst >= 4: - no1wkst = 0 - # Number of days in the year, plus the days we got - # from last year. - wyearlen = self.yearlen+(self.yearweekday-rr._wkst)%7 - else: - # Number of days in the year, minus the days we - # left in last year. - wyearlen = self.yearlen-no1wkst - div, mod = divmod(wyearlen, 7) - numweeks = div+mod//4 - for n in rr._byweekno: - if n < 0: - n += numweeks+1 - if not (0 < n <= numweeks): - continue - if n > 1: - i = no1wkst+(n-1)*7 - if no1wkst != firstwkst: - i -= 7-firstwkst - else: - i = no1wkst - for j in range(7): - self.wnomask[i] = 1 - i += 1 - if self.wdaymask[i] == rr._wkst: - break - if 1 in rr._byweekno: - # Check week number 1 of next year as well - # TODO: Check -numweeks for next year. - i = no1wkst+numweeks*7 - if no1wkst != firstwkst: - i -= 7-firstwkst - if i < self.yearlen: - # If week starts in next year, we - # don't care about it. - for j in range(7): - self.wnomask[i] = 1 - i += 1 - if self.wdaymask[i] == rr._wkst: - break - if no1wkst: - # Check last week number of last year as - # well. If no1wkst is 0, either the year - # started on week start, or week number 1 - # got days from last year, so there are no - # days from last year's last week number in - # this year. - if -1 not in rr._byweekno: - lyearweekday = datetime.date(year-1,1,1).weekday() - lno1wkst = (7-lyearweekday+rr._wkst)%7 - lyearlen = 365+calendar.isleap(year-1) - if lno1wkst >= 4: - lno1wkst = 0 - lnumweeks = 52+(lyearlen+ - (lyearweekday-rr._wkst)%7)%7//4 - else: - lnumweeks = 52+(self.yearlen-no1wkst)%7//4 - else: - lnumweeks = -1 - if lnumweeks in rr._byweekno: - for i in range(no1wkst): - self.wnomask[i] = 1 - - if (rr._bynweekday and - (month != self.lastmonth or year != self.lastyear)): - ranges = [] - if rr._freq == YEARLY: - if rr._bymonth: - for month in rr._bymonth: - ranges.append(self.mrange[month-1:month+1]) - else: - ranges = [(0, self.yearlen)] - elif rr._freq == MONTHLY: - ranges = [self.mrange[month-1:month+1]] - if ranges: - # Weekly frequency won't get here, so we may not - # care about cross-year weekly periods. - self.nwdaymask = [0]*self.yearlen - for first, last in ranges: - last -= 1 - for wday, n in rr._bynweekday: - if n < 0: - i = last+(n+1)*7 - i -= (self.wdaymask[i]-wday)%7 - else: - i = first+(n-1)*7 - i += (7-self.wdaymask[i]+wday)%7 - if first <= i <= last: - self.nwdaymask[i] = 1 - - if rr._byeaster: - self.eastermask = [0]*(self.yearlen+7) - eyday = easter.easter(year).toordinal()-self.yearordinal - for offset in rr._byeaster: - self.eastermask[eyday+offset] = 1 - - self.lastyear = year - self.lastmonth = month - - def ydayset(self, year, month, day): - return list(range(self.yearlen)), 0, self.yearlen - - def mdayset(self, year, month, day): - set = [None]*self.yearlen - start, end = self.mrange[month-1:month+1] - for i in range(start, end): - set[i] = i - return set, start, end - - def wdayset(self, year, month, day): - # We need to handle cross-year weeks here. - set = [None]*(self.yearlen+7) - i = datetime.date(year, month, day).toordinal()-self.yearordinal - start = i - for j in range(7): - set[i] = i - i += 1 - #if (not (0 <= i < self.yearlen) or - # self.wdaymask[i] == self.rrule._wkst): - # This will cross the year boundary, if necessary. - if self.wdaymask[i] == self.rrule._wkst: - break - return set, start, i - - def ddayset(self, year, month, day): - set = [None]*self.yearlen - i = datetime.date(year, month, day).toordinal()-self.yearordinal - set[i] = i - return set, i, i+1 - - def htimeset(self, hour, minute, second): - set = [] - rr = self.rrule - for minute in rr._byminute: - for second in rr._bysecond: - set.append(datetime.time(hour, minute, second, - tzinfo=rr._tzinfo)) - set.sort() - return set - - def mtimeset(self, hour, minute, second): - set = [] - rr = self.rrule - for second in rr._bysecond: - set.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) - set.sort() - return set - - def stimeset(self, hour, minute, second): - return (datetime.time(hour, minute, second, - tzinfo=self.rrule._tzinfo),) - - -class rruleset(rrulebase): - - class _genitem: - def __init__(self, genlist, gen): - try: - self.dt = gen() - genlist.append(self) - except StopIteration: - pass - self.genlist = genlist - self.gen = gen - - def __next__(self): - try: - self.dt = self.gen() - except StopIteration: - self.genlist.remove(self) - - def __cmp__(self, other): - return cmp(self.dt, other.dt) - - def __init__(self, cache=False): - rrulebase.__init__(self, cache) - self._rrule = [] - self._rdate = [] - self._exrule = [] - self._exdate = [] - - def rrule(self, rrule): - self._rrule.append(rrule) - - def rdate(self, rdate): - self._rdate.append(rdate) - - def exrule(self, exrule): - self._exrule.append(exrule) - - def exdate(self, exdate): - self._exdate.append(exdate) - - def _iter(self): - rlist = [] - self._rdate.sort() - self._genitem(rlist, iter(self._rdate).__next__) - for gen in [iter(x).__next__ for x in self._rrule]: - self._genitem(rlist, gen) - rlist.sort() - exlist = [] - self._exdate.sort() - self._genitem(exlist, iter(self._exdate).__next__) - for gen in [iter(x).__next__ for x in self._exrule]: - self._genitem(exlist, gen) - exlist.sort() - lastdt = None - total = 0 - while rlist: - ritem = rlist[0] - if not lastdt or lastdt != ritem.dt: - while exlist and exlist[0] < ritem: - next(exlist[0]) - exlist.sort() - if not exlist or ritem != exlist[0]: - total += 1 - yield ritem.dt - lastdt = ritem.dt - next(ritem) - rlist.sort() - self._len = total - -class _rrulestr: - - _freq_map = {"YEARLY": YEARLY, - "MONTHLY": MONTHLY, - "WEEKLY": WEEKLY, - "DAILY": DAILY, - "HOURLY": HOURLY, - "MINUTELY": MINUTELY, - "SECONDLY": SECONDLY} - - _weekday_map = {"MO":0,"TU":1,"WE":2,"TH":3,"FR":4,"SA":5,"SU":6} - - def _handle_int(self, rrkwargs, name, value, **kwargs): - rrkwargs[name.lower()] = int(value) - - def _handle_int_list(self, rrkwargs, name, value, **kwargs): - rrkwargs[name.lower()] = [int(x) for x in value.split(',')] - - _handle_INTERVAL = _handle_int - _handle_COUNT = _handle_int - _handle_BYSETPOS = _handle_int_list - _handle_BYMONTH = _handle_int_list - _handle_BYMONTHDAY = _handle_int_list - _handle_BYYEARDAY = _handle_int_list - _handle_BYEASTER = _handle_int_list - _handle_BYWEEKNO = _handle_int_list - _handle_BYHOUR = _handle_int_list - _handle_BYMINUTE = _handle_int_list - _handle_BYSECOND = _handle_int_list - - def _handle_FREQ(self, rrkwargs, name, value, **kwargs): - rrkwargs["freq"] = self._freq_map[value] - - def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): - global parser - if not parser: - from dateutil import parser - try: - rrkwargs["until"] = parser.parse(value, - ignoretz=kwargs.get("ignoretz"), - tzinfos=kwargs.get("tzinfos")) - except ValueError: - raise ValueError("invalid until date") - - def _handle_WKST(self, rrkwargs, name, value, **kwargs): - rrkwargs["wkst"] = self._weekday_map[value] - - def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwarsg): - l = [] - for wday in value.split(','): - for i in range(len(wday)): - if wday[i] not in '+-0123456789': - break - n = wday[:i] or None - w = wday[i:] - if n: n = int(n) - l.append(weekdays[self._weekday_map[w]](n)) - rrkwargs["byweekday"] = l - - _handle_BYDAY = _handle_BYWEEKDAY - - def _parse_rfc_rrule(self, line, - dtstart=None, - cache=False, - ignoretz=False, - tzinfos=None): - if line.find(':') != -1: - name, value = line.split(':') - if name != "RRULE": - raise ValueError("unknown parameter name") - else: - value = line - rrkwargs = {} - for pair in value.split(';'): - name, value = pair.split('=') - name = name.upper() - value = value.upper() - try: - getattr(self, "_handle_"+name)(rrkwargs, name, value, - ignoretz=ignoretz, - tzinfos=tzinfos) - except AttributeError: - raise "unknown parameter '%s'" % name - except (KeyError, ValueError): - raise "invalid '%s': %s" % (name, value) - return rrule(dtstart=dtstart, cache=cache, **rrkwargs) - - def _parse_rfc(self, s, - dtstart=None, - cache=False, - unfold=False, - forceset=False, - compatible=False, - ignoretz=False, - tzinfos=None): - global parser - if compatible: - forceset = True - unfold = True - s = s.upper() - if not s.strip(): - raise ValueError("empty string") - if unfold: - lines = s.splitlines() - i = 0 - while i < len(lines): - line = lines[i].rstrip() - if not line: - del lines[i] - elif i > 0 and line[0] == " ": - lines[i-1] += line[1:] - del lines[i] - else: - i += 1 - else: - lines = s.split() - if (not forceset and len(lines) == 1 and - (s.find(':') == -1 or s.startswith('RRULE:'))): - return self._parse_rfc_rrule(lines[0], cache=cache, - dtstart=dtstart, ignoretz=ignoretz, - tzinfos=tzinfos) - else: - rrulevals = [] - rdatevals = [] - exrulevals = [] - exdatevals = [] - for line in lines: - if not line: - continue - if line.find(':') == -1: - name = "RRULE" - value = line - else: - name, value = line.split(':', 1) - parms = name.split(';') - if not parms: - raise ValueError("empty property name") - name = parms[0] - parms = parms[1:] - if name == "RRULE": - for parm in parms: - raise ValueError("unsupported RRULE parm: "+parm) - rrulevals.append(value) - elif name == "RDATE": - for parm in parms: - if parm != "VALUE=DATE-TIME": - raise ValueError("unsupported RDATE parm: "+parm) - rdatevals.append(value) - elif name == "EXRULE": - for parm in parms: - raise ValueError("unsupported EXRULE parm: "+parm) - exrulevals.append(value) - elif name == "EXDATE": - for parm in parms: - if parm != "VALUE=DATE-TIME": - raise ValueError("unsupported RDATE parm: "+parm) - exdatevals.append(value) - elif name == "DTSTART": - for parm in parms: - raise ValueError("unsupported DTSTART parm: "+parm) - if not parser: - from dateutil import parser - dtstart = parser.parse(value, ignoretz=ignoretz, - tzinfos=tzinfos) - else: - raise ValueError("unsupported property: "+name) - if (forceset or len(rrulevals) > 1 or - rdatevals or exrulevals or exdatevals): - if not parser and (rdatevals or exdatevals): - from dateutil import parser - set = rruleset(cache=cache) - for value in rrulevals: - set.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, - ignoretz=ignoretz, - tzinfos=tzinfos)) - for value in rdatevals: - for datestr in value.split(','): - set.rdate(parser.parse(datestr, - ignoretz=ignoretz, - tzinfos=tzinfos)) - for value in exrulevals: - set.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, - ignoretz=ignoretz, - tzinfos=tzinfos)) - for value in exdatevals: - for datestr in value.split(','): - set.exdate(parser.parse(datestr, - ignoretz=ignoretz, - tzinfos=tzinfos)) - if compatible and dtstart: - set.rdate(dtstart) - return set - else: - return self._parse_rfc_rrule(rrulevals[0], - dtstart=dtstart, - cache=cache, - ignoretz=ignoretz, - tzinfos=tzinfos) - - def __call__(self, s, **kwargs): - return self._parse_rfc(s, **kwargs) - -rrulestr = _rrulestr() - -# vim:ts=4:sw=4:et diff --git a/src/libs/python/bongo/external/dateutil/tz.py b/src/libs/python/bongo/external/dateutil/tz.py deleted file mode 100644 index 39839e3..0000000 --- a/src/libs/python/bongo/external/dateutil/tz.py +++ /dev/null @@ -1,924 +0,0 @@ -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -import datetime -import struct -import time -import sys -import os - -relativedelta = None -parser = None -rrule = None - -__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", - "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"] - -try: - from dateutil.tzwin import tzwin, tzwinlocal -except (ImportError, OSError): - tzwin, tzwinlocal = None, None - -ZERO = datetime.timedelta(0) -EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal() - -class tzutc(datetime.tzinfo): - - def utcoffset(self, dt): - return ZERO - - def dst(self, dt): - return ZERO - - def tzname(self, dt): - return "UTC" - - def __eq__(self, other): - return (isinstance(other, tzutc) or - (isinstance(other, tzoffset) and other._offset == ZERO)) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s()" % self.__class__.__name__ - - __reduce__ = object.__reduce__ - -class tzoffset(datetime.tzinfo): - - def __init__(self, name, offset): - self._name = name - self._offset = datetime.timedelta(seconds=offset) - - def utcoffset(self, dt): - return self._offset - - def dst(self, dt): - return ZERO - - def tzname(self, dt): - return self._name - - def __eq__(self, other): - return (isinstance(other, tzoffset) and - self._offset == other._offset) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s(%s, %s)" % (self.__class__.__name__, - repr(self._name), - self._offset.days*86400+self._offset.seconds) - - __reduce__ = object.__reduce__ - -class tzlocal(datetime.tzinfo): - - _std_offset = datetime.timedelta(seconds=-time.timezone) - if time.daylight: - _dst_offset = datetime.timedelta(seconds=-time.altzone) - else: - _dst_offset = _std_offset - - def utcoffset(self, dt): - if self._isdst(dt): - return self._dst_offset - else: - return self._std_offset - - def dst(self, dt): - if self._isdst(dt): - return self._dst_offset-self._std_offset - else: - return ZERO - - def tzname(self, dt): - return time.tzname[self._isdst(dt)] - - def _isdst(self, dt): - # We can't use mktime here. It is unstable when deciding if - # the hour near to a change is DST or not. - # - # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, - # dt.minute, dt.second, dt.weekday(), 0, -1)) - # return time.localtime(timestamp).tm_isdst - # - # The code above yields the following result: - # - #>>> import tz, datetime - #>>> t = tz.tzlocal() - #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() - #'BRDT' - #>>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() - #'BRST' - #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() - #'BRST' - #>>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() - #'BRDT' - #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() - #'BRDT' - # - # Here is a more stable implementation: - # - timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 - + dt.hour * 3600 - + dt.minute * 60 - + dt.second) - return time.localtime(timestamp+time.timezone).tm_isdst - - def __eq__(self, other): - if not isinstance(other, tzlocal): - return False - return (self._std_offset == other._std_offset and - self._dst_offset == other._dst_offset) - return True - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s()" % self.__class__.__name__ - - __reduce__ = object.__reduce__ - -class _ttinfo(object): - __slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"] - - def __init__(self): - for attr in self.__slots__: - setattr(self, attr, None) - - def __repr__(self): - l = [] - for attr in self.__slots__: - value = getattr(self, attr) - if value is not None: - l.append("%s=%s" % (attr, repr(value))) - return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) - - def __eq__(self, other): - if not isinstance(other, _ttinfo): - return False - return (self.offset == other.offset and - self.delta == other.delta and - self.isdst == other.isdst and - self.abbr == other.abbr and - self.isstd == other.isstd and - self.isgmt == other.isgmt) - - def __ne__(self, other): - return not self.__eq__(other) - - def __getstate__(self): - state = {} - for name in self.__slots__: - state[name] = getattr(self, name, None) - return state - - def __setstate__(self, state): - for name in self.__slots__: - if name in state: - setattr(self, name, state[name]) - -class tzfile(datetime.tzinfo): - - # http://www.twinsun.com/tz/tz-link.htm - # ftp://elsie.nci.nih.gov/pub/tz*.tar.gz - - def __init__(self, fileobj): - if isinstance(fileobj, str): - self._filename = fileobj - fileobj = open(fileobj) - elif hasattr(fileobj, "name"): - self._filename = fileobj.name - else: - self._filename = repr(fileobj) - - # From tzfile(5): - # - # The time zone information files used by tzset(3) - # begin with the magic characters "TZif" to identify - # them as time zone information files, followed by - # sixteen bytes reserved for future use, followed by - # six four-byte values of type long, written in a - # ``standard'' byte order (the high-order byte - # of the value is written first). - - if fileobj.read(4) != "TZif": - raise ValueError("magic not found") - - fileobj.read(16) - - ( - # The number of UTC/local indicators stored in the file. - ttisgmtcnt, - - # The number of standard/wall indicators stored in the file. - ttisstdcnt, - - # The number of leap seconds for which data is - # stored in the file. - leapcnt, - - # The number of "transition times" for which data - # is stored in the file. - timecnt, - - # The number of "local time types" for which data - # is stored in the file (must not be zero). - typecnt, - - # The number of characters of "time zone - # abbreviation strings" stored in the file. - charcnt, - - ) = struct.unpack(">6l", fileobj.read(24)) - - # The above header is followed by tzh_timecnt four-byte - # values of type long, sorted in ascending order. - # These values are written in ``standard'' byte order. - # Each is used as a transition time (as returned by - # time(2)) at which the rules for computing local time - # change. - - if timecnt: - self._trans_list = struct.unpack(">%dl" % timecnt, - fileobj.read(timecnt*4)) - else: - self._trans_list = [] - - # Next come tzh_timecnt one-byte values of type unsigned - # char; each one tells which of the different types of - # ``local time'' types described in the file is associated - # with the same-indexed transition time. These values - # serve as indices into an array of ttinfo structures that - # appears next in the file. - - if timecnt: - self._trans_idx = struct.unpack(">%dB" % timecnt, - fileobj.read(timecnt)) - else: - self._trans_idx = [] - - # Each ttinfo structure is written as a four-byte value - # for tt_gmtoff of type long, in a standard byte - # order, followed by a one-byte value for tt_isdst - # and a one-byte value for tt_abbrind. In each - # structure, tt_gmtoff gives the number of - # seconds to be added to UTC, tt_isdst tells whether - # tm_isdst should be set by localtime(3), and - # tt_abbrind serves as an index into the array of - # time zone abbreviation characters that follow the - # ttinfo structure(s) in the file. - - ttinfo = [] - - for i in range(typecnt): - ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) - - abbr = fileobj.read(charcnt) - - # Then there are tzh_leapcnt pairs of four-byte - # values, written in standard byte order; the - # first value of each pair gives the time (as - # returned by time(2)) at which a leap second - # occurs; the second gives the total number of - # leap seconds to be applied after the given time. - # The pairs of values are sorted in ascending order - # by time. - - # Not used, for now - if leapcnt: - leap = struct.unpack(">%dl" % leapcnt*2, - fileobj.read(leapcnt*8)) - - # Then there are tzh_ttisstdcnt standard/wall - # indicators, each stored as a one-byte value; - # they tell whether the transition times associated - # with local time types were specified as standard - # time or wall clock time, and are used when - # a time zone file is used in handling POSIX-style - # time zone environment variables. - - if ttisstdcnt: - isstd = struct.unpack(">%db" % ttisstdcnt, - fileobj.read(ttisstdcnt)) - - # Finally, there are tzh_ttisgmtcnt UTC/local - # indicators, each stored as a one-byte value; - # they tell whether the transition times associated - # with local time types were specified as UTC or - # local time, and are used when a time zone file - # is used in handling POSIX-style time zone envi- - # ronment variables. - - if ttisgmtcnt: - isgmt = struct.unpack(">%db" % ttisgmtcnt, - fileobj.read(ttisgmtcnt)) - - # ** Everything has been read ** - - # Build ttinfo list - self._ttinfo_list = [] - for i in range(typecnt): - tti = _ttinfo() - tti.offset = ttinfo[i][0] - tti.delta = datetime.timedelta(seconds=ttinfo[i][0]) - tti.isdst = ttinfo[i][1] - tti.abbr = abbr[ttinfo[i][2]:abbr.find('\x00', ttinfo[i][2])] - tti.isstd = (ttisstdcnt > i and isstd[i] != 0) - tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) - self._ttinfo_list.append(tti) - - # Replace ttinfo indexes for ttinfo objects. - trans_idx = [] - for idx in self._trans_idx: - trans_idx.append(self._ttinfo_list[idx]) - self._trans_idx = tuple(trans_idx) - - # Set standard, dst, and before ttinfos. before will be - # used when a given time is before any transitions, - # and will be set to the first non-dst ttinfo, or to - # the first dst, if all of them are dst. - self._ttinfo_std = None - self._ttinfo_dst = None - self._ttinfo_before = None - if self._ttinfo_list: - if not self._trans_list: - self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0] - else: - for i in range(timecnt-1,-1,-1): - tti = self._trans_idx[i] - if not self._ttinfo_std and not tti.isdst: - self._ttinfo_std = tti - elif not self._ttinfo_dst and tti.isdst: - self._ttinfo_dst = tti - if self._ttinfo_std and self._ttinfo_dst: - break - else: - if self._ttinfo_dst and not self._ttinfo_std: - self._ttinfo_std = self._ttinfo_dst - - for tti in self._ttinfo_list: - if not tti.isdst: - self._ttinfo_before = tti - break - else: - self._ttinfo_before = self._ttinfo_list[0] - - # Now fix transition times to become relative to wall time. - # - # I'm not sure about this. In my tests, the tz source file - # is setup to wall time, and in the binary file isstd and - # isgmt are off, so it should be in wall time. OTOH, it's - # always in gmt time. Let me know if you have comments - # about this. - laststdoffset = 0 - self._trans_list = list(self._trans_list) - for i in range(len(self._trans_list)): - tti = self._trans_idx[i] - if not tti.isdst: - # This is std time. - self._trans_list[i] += tti.offset - laststdoffset = tti.offset - else: - # This is dst time. Convert to std. - self._trans_list[i] += laststdoffset - self._trans_list = tuple(self._trans_list) - - def _find_ttinfo(self, dt, laststd=0): - timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 - + dt.hour * 3600 - + dt.minute * 60 - + dt.second) - idx = 0 - for trans in self._trans_list: - if timestamp < trans: - break - idx += 1 - else: - return self._ttinfo_std - if idx == 0: - return self._ttinfo_before - if laststd: - while idx > 0: - tti = self._trans_idx[idx-1] - if not tti.isdst: - return tti - idx -= 1 - else: - return self._ttinfo_std - else: - return self._trans_idx[idx-1] - - def utcoffset(self, dt): - if not self._ttinfo_std: - return ZERO - return self._find_ttinfo(dt).delta - - def dst(self, dt): - if not self._ttinfo_dst: - return ZERO - tti = self._find_ttinfo(dt) - if not tti.isdst: - return ZERO - - # The documentation says that utcoffset()-dst() must - # be constant for every dt. - return self._find_ttinfo(dt, laststd=1).delta-tti.delta - - # An alternative for that would be: - # - # return self._ttinfo_dst.offset-self._ttinfo_std.offset - # - # However, this class stores historical changes in the - # dst offset, so I belive that this wouldn't be the right - # way to implement this. - - def tzname(self, dt): - if not self._ttinfo_std: - return None - return self._find_ttinfo(dt).abbr - - def __eq__(self, other): - if not isinstance(other, tzfile): - return False - return (self._trans_list == other._trans_list and - self._trans_idx == other._trans_idx and - self._ttinfo_list == other._ttinfo_list) - - def __ne__(self, other): - return not self.__eq__(other) - - - def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, repr(self._filename)) - - def __reduce__(self): - if not os.path.isfile(self._filename): - raise ValueError("Unpickable %s class" % self.__class__.__name__) - return (self.__class__, (self._filename,)) - -class tzrange(datetime.tzinfo): - - def __init__(self, stdabbr, stdoffset=None, - dstabbr=None, dstoffset=None, - start=None, end=None): - global relativedelta - if not relativedelta: - from dateutil import relativedelta - self._std_abbr = stdabbr - self._dst_abbr = dstabbr - if stdoffset is not None: - self._std_offset = datetime.timedelta(seconds=stdoffset) - else: - self._std_offset = ZERO - if dstoffset is not None: - self._dst_offset = datetime.timedelta(seconds=dstoffset) - elif dstabbr and stdoffset is not None: - self._dst_offset = self._std_offset+datetime.timedelta(hours=+1) - else: - self._dst_offset = ZERO - if start is None: - self._start_delta = relativedelta.relativedelta( - hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) - else: - self._start_delta = start - if end is None: - self._end_delta = relativedelta.relativedelta( - hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) - else: - self._end_delta = end - - def utcoffset(self, dt): - if self._isdst(dt): - return self._dst_offset - else: - return self._std_offset - - def dst(self, dt): - if self._isdst(dt): - return self._dst_offset-self._std_offset - else: - return ZERO - - def tzname(self, dt): - if self._isdst(dt): - return self._dst_abbr - else: - return self._std_abbr - - def _isdst(self, dt): - if not self._start_delta: - return False - year = datetime.date(dt.year,1,1) - start = year+self._start_delta - end = year+self._end_delta - dt = dt.replace(tzinfo=None) - if start < end: - return dt >= start and dt < end - else: - return dt >= start or dt < end - - def __eq__(self, other): - if not isinstance(other, tzrange): - return False - return (self._std_abbr == other._std_abbr and - self._dst_abbr == other._dst_abbr and - self._std_offset == other._std_offset and - self._dst_offset == other._dst_offset and - self._start_delta == other._start_delta and - self._end_delta == other._end_delta) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s(...)" % self.__class__.__name__ - - __reduce__ = object.__reduce__ - -class tzstr(tzrange): - - def __init__(self, s): - global parser - if not parser: - from dateutil import parser - self._s = s - - res = parser._parsetz(s) - if res is None: - raise ValueError("unknown string format") - - # We must initialize it first, since _delta() needs - # _std_offset and _dst_offset set. Use False in start/end - # to avoid building it two times. - tzrange.__init__(self, res.stdabbr, res.stdoffset, - res.dstabbr, res.dstoffset, - start=False, end=False) - - self._start_delta = self._delta(res.start) - if self._start_delta: - self._end_delta = self._delta(res.end, isend=1) - - def _delta(self, x, isend=0): - kwargs = {} - if x.month is not None: - kwargs["month"] = x.month - if x.weekday is not None: - kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) - if x.week > 0: - kwargs["day"] = 1 - else: - kwargs["day"] = 31 - elif x.day: - kwargs["day"] = x.day - elif x.yday is not None: - kwargs["yearday"] = x.yday - elif x.jyday is not None: - kwargs["nlyearday"] = x.jyday - if not kwargs: - # Default is to start on first sunday of april, and end - # on last sunday of october. - if not isend: - kwargs["month"] = 4 - kwargs["day"] = 1 - kwargs["weekday"] = relativedelta.SU(+1) - else: - kwargs["month"] = 10 - kwargs["day"] = 31 - kwargs["weekday"] = relativedelta.SU(-1) - if x.time is not None: - kwargs["seconds"] = x.time - else: - # Default is 2AM. - kwargs["seconds"] = 7200 - if isend: - # Convert to standard time, to follow the documented way - # of working with the extra hour. See the documentation - # of the tzinfo class. - delta = self._dst_offset-self._std_offset - kwargs["seconds"] -= delta.seconds+delta.days*86400 - return relativedelta.relativedelta(**kwargs) - - def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, repr(self._s)) - -class _tzicalvtzcomp: - def __init__(self, tzoffsetfrom, tzoffsetto, isdst, - tzname=None, rrule=None): - self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) - self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) - self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom - self.isdst = isdst - self.tzname = tzname - self.rrule = rrule - -class _tzicalvtz(datetime.tzinfo): - def __init__(self, tzid, comps=[]): - self._tzid = tzid - self._comps = comps - self._cachedate = [] - self._cachecomp = [] - - def _find_comp(self, dt): - if len(self._comps) == 1: - return self._comps[0] - dt = dt.replace(tzinfo=None) - try: - return self._cachecomp[self._cachedate.index(dt)] - except ValueError: - pass - lastcomp = None - lastcompdt = None - for comp in self._comps: - if not comp.isdst: - # Handle the extra hour in DST -> STD - compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True) - else: - compdt = comp.rrule.before(dt, inc=True) - if compdt and (not lastcompdt or lastcompdt < compdt): - lastcompdt = compdt - lastcomp = comp - if not lastcomp: - # RFC says nothing about what to do when a given - # time is before the first onset date. We'll look for the - # first standard component, or the first component, if - # none is found. - for comp in self._comps: - if not comp.isdst: - lastcomp = comp - break - else: - lastcomp = comp[0] - self._cachedate.insert(0, dt) - self._cachecomp.insert(0, lastcomp) - if len(self._cachedate) > 10: - self._cachedate.pop() - self._cachecomp.pop() - return lastcomp - - def utcoffset(self, dt): - return self._find_comp(dt).tzoffsetto - - def dst(self, dt): - comp = self._find_comp(dt) - if comp.isdst: - return comp.tzoffsetdiff - else: - return ZERO - - def tzname(self, dt): - return self._find_comp(dt).tzname - - def __repr__(self): - return "" % repr(self._tzid) - - __reduce__ = object.__reduce__ - -class tzical: - def __init__(self, fileobj): - global rrule - if not rrule: - from dateutil import rrule - - if isinstance(fileobj, str): - self._s = fileobj - fileobj = open(fileobj) - elif hasattr(fileobj, "name"): - self._s = fileobj.name - else: - self._s = repr(fileobj) - - self._vtz = {} - - self._parse_rfc(fileobj.read()) - - def keys(self): - return list(self._vtz.keys()) - - def get(self, tzid=None): - if tzid is None: - keys = list(self._vtz.keys()) - if len(keys) == 0: - raise "no timezones defined" - elif len(keys) > 1: - raise "more than one timezone available" - tzid = keys[0] - return self._vtz.get(tzid) - - def _parse_offset(self, s): - s = s.strip() - if not s: - raise ValueError("empty offset") - if s[0] in ('+', '-'): - signal = (-1,+1)[s[0]=='+'] - s = s[1:] - else: - signal = +1 - if len(s) == 4: - return (int(s[:2])*3600+int(s[2:])*60)*signal - elif len(s) == 6: - return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal - else: - raise ValueError("invalid offset: "+s) - - def _parse_rfc(self, s): - lines = s.splitlines() - if not lines: - raise ValueError("empty string") - - # Unfold - i = 0 - while i < len(lines): - line = lines[i].rstrip() - if not line: - del lines[i] - elif i > 0 and line[0] == " ": - lines[i-1] += line[1:] - del lines[i] - else: - i += 1 - - invtz = False - comptype = None - for line in lines: - if not line: - continue - name, value = line.split(':', 1) - parms = name.split(';') - if not parms: - raise ValueError("empty property name") - name = parms[0].upper() - parms = parms[1:] - if invtz: - if name == "BEGIN": - if value in ("STANDARD", "DAYLIGHT"): - # Process component - pass - else: - raise ValueError("unknown component: "+value) - comptype = value - founddtstart = False - tzoffsetfrom = None - tzoffsetto = None - rrulelines = [] - tzname = None - elif name == "END": - if value == "VTIMEZONE": - if comptype: - raise ValueError("component not closed: "+comptype) - if not tzid: - raise ValueError("mandatory TZID not found") - if not comps: - raise ValueError("at least one component is needed") - # Process vtimezone - self._vtz[tzid] = _tzicalvtz(tzid, comps) - invtz = False - elif value == comptype: - if not founddtstart: - raise ValueError("mandatory DTSTART not found") - if tzoffsetfrom is None: - raise ValueError("mandatory TZOFFSETFROM not found") - if tzoffsetto is None: - raise ValueError("mandatory TZOFFSETFROM not found") - # Process component - rr = None - if rrulelines: - rr = rrule.rrulestr("\n".join(rrulelines), - compatible=True, - ignoretz=True, - cache=True) - comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, - (comptype == "DAYLIGHT"), - tzname, rr) - comps.append(comp) - comptype = None - else: - raise ValueError("invalid component end: "+value) - elif comptype: - if name == "DTSTART": - rrulelines.append(line) - founddtstart = True - elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): - rrulelines.append(line) - elif name == "TZOFFSETFROM": - if parms: - raise ValueError("unsupported %s parm: %s "%(name, parms[0])) - tzoffsetfrom = self._parse_offset(value) - elif name == "TZOFFSETTO": - if parms: - raise ValueError("unsupported TZOFFSETTO parm: "+parms[0]) - tzoffsetto = self._parse_offset(value) - elif name == "TZNAME": - if parms: - raise ValueError("unsupported TZNAME parm: "+parms[0]) - tzname = value - elif name == "COMMENT": - pass - else: - raise ValueError("unsupported property: "+name) - else: - if name == "TZID": - if parms: - raise ValueError("unsupported TZID parm: "+parms[0]) - tzid = value - elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): - pass - else: - raise ValueError("unsupported property: "+name) - elif name == "BEGIN" and value == "VTIMEZONE": - tzid = None - comps = [] - invtz = True - - def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, repr(self._s)) - -if sys.platform != "win32": - TZFILES = ["/etc/localtime", "localtime"] - TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"] -else: - TZFILES = [] - TZPATHS = [] - -def gettz(name=None): - tz = None - if not name: - try: - name = os.environ["TZ"] - except KeyError: - pass - if name is None or name == ":": - for filepath in TZFILES: - if not os.path.isabs(filepath): - filename = filepath - for path in TZPATHS: - filepath = os.path.join(path, filename) - if os.path.isfile(filepath): - break - else: - continue - if os.path.isfile(filepath): - try: - tz = tzfile(filepath) - break - except (IOError, OSError, ValueError): - pass - else: - tz = tzlocal() - else: - if name.startswith(":"): - name = name[:-1] - if os.path.isabs(name): - if os.path.isfile(name): - tz = tzfile(name) - else: - tz = None - else: - for path in TZPATHS: - filepath = os.path.join(path, name) - if not os.path.isfile(filepath): - filepath = filepath.replace(' ','_') - if not os.path.isfile(filepath): - continue - try: - tz = tzfile(filepath) - break - except (IOError, OSError, ValueError): - pass - else: - tz = None - if tzwin: - try: - tz = tzwin(name) - except OSError: - pass - if not tz: - from dateutil.zoneinfo import gettz - tz = gettz(name) - if not tz: - for c in name: - # name must have at least one offset to be a tzstr - if c in "0123456789": - try: - tz = tzstr(name) - except ValueError: - pass - break - else: - if name in ("GMT", "UTC"): - tz = tzutc() - elif name in time.tzname: - tz = tzlocal() - return tz - -# vim:ts=4:sw=4:et diff --git a/src/libs/python/bongo/external/dateutil/tzwin.py b/src/libs/python/bongo/external/dateutil/tzwin.py deleted file mode 100644 index b378f59..0000000 --- a/src/libs/python/bongo/external/dateutil/tzwin.py +++ /dev/null @@ -1,180 +0,0 @@ -# This code was originally contributed by Jeffrey Harris. -import datetime -import struct -import winreg - -__author__ = "Jeffrey Harris & Gustavo Niemeyer " - -__all__ = ["tzwin", "tzwinlocal"] - -ONEWEEK = datetime.timedelta(7) - -TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" -TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones" -TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" - -def _settzkeyname(): - global TZKEYNAME - handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) - try: - winreg.OpenKey(handle, TZKEYNAMENT).Close() - TZKEYNAME = TZKEYNAMENT - except WindowsError: - TZKEYNAME = TZKEYNAME9X - handle.Close() - -_settzkeyname() - -class tzwinbase(datetime.tzinfo): - """tzinfo class based on win32's timezones available in the registry.""" - - def utcoffset(self, dt): - if self._isdst(dt): - return datetime.timedelta(minutes=self._dstoffset) - else: - return datetime.timedelta(minutes=self._stdoffset) - - def dst(self, dt): - if self._isdst(dt): - minutes = self._dstoffset - self._stdoffset - return datetime.timedelta(minutes=minutes) - else: - return datetime.timedelta(0) - - def tzname(self, dt): - if self._isdst(dt): - return self._dstname - else: - return self._stdname - - def list(): - """Return a list of all time zones known to the system.""" - handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) - tzkey = winreg.OpenKey(handle, TZKEYNAME) - result = [winreg.EnumKey(tzkey, i) - for i in range(winreg.QueryInfoKey(tzkey)[0])] - tzkey.Close() - handle.Close() - return result - list = staticmethod(list) - - def display(self): - return self._display - - def _isdst(self, dt): - dston = picknthweekday(dt.year, self._dstmonth, self._dstdayofweek, - self._dsthour, self._dstminute, - self._dstweeknumber) - dstoff = picknthweekday(dt.year, self._stdmonth, self._stddayofweek, - self._stdhour, self._stdminute, - self._stdweeknumber) - if dston < dstoff: - return dston <= dt.replace(tzinfo=None) < dstoff - else: - return not dstoff <= dt.replace(tzinfo=None) < dston - - -class tzwin(tzwinbase): - - def __init__(self, name): - self._name = name - - handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) - tzkey = winreg.OpenKey(handle, "%s\%s" % (TZKEYNAME, name)) - keydict = valuestodict(tzkey) - tzkey.Close() - handle.Close() - - self._stdname = keydict["Std"].encode("iso-8859-1") - self._dstname = keydict["Dlt"].encode("iso-8859-1") - - self._display = keydict["Display"] - - # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm - tup = struct.unpack("=3l16h", keydict["TZI"]) - self._stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 - self._dstoffset = self._stdoffset-tup[2] # + DaylightBias * -1 - - (self._stdmonth, - self._stddayofweek, # Sunday = 0 - self._stdweeknumber, # Last = 5 - self._stdhour, - self._stdminute) = tup[4:9] - - (self._dstmonth, - self._dstdayofweek, # Sunday = 0 - self._dstweeknumber, # Last = 5 - self._dsthour, - self._dstminute) = tup[12:17] - - def __repr__(self): - return "tzwin(%s)" % repr(self._name) - - def __reduce__(self): - return (self.__class__, (self._name,)) - - -class tzwinlocal(tzwinbase): - - def __init__(self): - - handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) - - tzlocalkey = winreg.OpenKey(handle, TZLOCALKEYNAME) - keydict = valuestodict(tzlocalkey) - tzlocalkey.Close() - - self._stdname = keydict["StandardName"].encode("iso-8859-1") - self._dstname = keydict["DaylightName"].encode("iso-8859-1") - - try: - tzkey = winreg.OpenKey(handle, "%s\%s"%(TZKEYNAME, self._stdname)) - _keydict = valuestodict(tzkey) - self._display = _keydict["Display"] - tzkey.Close() - except OSError: - self._display = None - - handle.Close() - - self._stdoffset = -keydict["Bias"]-keydict["StandardBias"] - self._dstoffset = self._stdoffset-keydict["DaylightBias"] - - - # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm - tup = struct.unpack("=8h", keydict["StandardStart"]) - - (self._stdmonth, - self._stddayofweek, # Sunday = 0 - self._stdweeknumber, # Last = 5 - self._stdhour, - self._stdminute) = tup[1:6] - - tup = struct.unpack("=8h", keydict["DaylightStart"]) - - (self._dstmonth, - self._dstdayofweek, # Sunday = 0 - self._dstweeknumber, # Last = 5 - self._dsthour, - self._dstminute) = tup[1:6] - - def __reduce__(self): - return (self.__class__, ()) - -def picknthweekday(year, month, dayofweek, hour, minute, whichweek): - """dayofweek == 0 means Sunday, whichweek 5 means last instance""" - first = datetime.datetime(year, month, 1, hour, minute) - weekdayone = first.replace(day=((dayofweek-first.isoweekday())%7+1)) - for n in range(whichweek): - dt = weekdayone+(whichweek-n)*ONEWEEK - if dt.month == month: - return dt - -def valuestodict(key): - """Convert a registry key's values to a dictionary.""" - dict = {} - size = winreg.QueryInfoKey(key)[1] - for i in range(size): - data = winreg.EnumValue(key, i) - dict[data[0]] = data[1] - return dict diff --git a/src/libs/python/bongo/external/dateutil/zoneinfo/__init__.py b/src/libs/python/bongo/external/dateutil/zoneinfo/__init__.py deleted file mode 100644 index 856fcf1..0000000 --- a/src/libs/python/bongo/external/dateutil/zoneinfo/__init__.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -from dateutil.tz import tzfile -from tarfile import TarFile -import os - -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -__all__ = ["setcachesize", "gettz", "rebuild"] - -CACHE = [] -CACHESIZE = 10 - -class tzfile(tzfile): - def __reduce__(self): - return (gettz, (self._filename,)) - -ZONEINFOFILE = None -for entry in os.listdir(os.path.dirname(__file__)): - if entry.startswith("zoneinfo") and ".tar." in entry: - ZONEINFOFILE = os.path.join(os.path.dirname(__file__), entry) - break - -def setcachesize(size): - global CACHESIZE, CACHE - CACHESIZE = size - del CACHE[size:] - -def gettz(name): - tzinfo = None - if ZONEINFOFILE: - for cachedname, tzinfo in CACHE: - if cachedname == name: - break - else: - tf = TarFile.open(ZONEINFOFILE) - try: - zonefile = tf.extractfile(name) - except KeyError: - tzinfo = None - else: - tzinfo = tzfile(zonefile) - tf.close() - CACHE.insert(0, (name, tzinfo)) - del CACHE[CACHESIZE:] - return tzinfo - -def rebuild(filename, tag=None, format="gz"): - import tempfile, shutil - tmpdir = tempfile.mkdtemp() - zonedir = os.path.join(tmpdir, "zoneinfo") - moduledir = os.path.dirname(__file__) - if tag: tag = "-"+tag - targetname = "zoneinfo%s.tar.%s" % (tag, format) - try: - tf = TarFile.open(filename) - for name in tf.getnames(): - if not (name.endswith(".sh") or - name.endswith(".tab") or - name == "leapseconds"): - tf.extract(name, tmpdir) - filepath = os.path.join(tmpdir, name) - os.system("zic -d %s %s" % (zonedir, filepath)) - tf.close() - target = os.path.join(moduledir, targetname) - for entry in os.listdir(moduledir): - if entry.startswith("zoneinfo") and ".tar." in entry: - os.unlink(os.path.join(moduledir, entry)) - tf = TarFile.open(target, "w:%s" % format) - for entry in os.listdir(zonedir): - entrypath = os.path.join(zonedir, entry) - tf.add(entrypath, entry) - tf.close() - finally: - shutil.rmtree(tmpdir) diff --git a/src/libs/python/bongo/external/dateutil/zoneinfo/zoneinfo-2005q.tar.gz b/src/libs/python/bongo/external/dateutil/zoneinfo/zoneinfo-2005q.tar.gz deleted file mode 100644 index 4f4457e..0000000 Binary files a/src/libs/python/bongo/external/dateutil/zoneinfo/zoneinfo-2005q.tar.gz and /dev/null differ diff --git a/src/libs/python/bongo/external/email/__init__.py b/src/libs/python/bongo/external/email/__init__.py deleted file mode 100644 index 2b94fd5..0000000 --- a/src/libs/python/bongo/external/email/__init__.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""A package for parsing, handling, and generating email messages.""" - -__version__ = '4.0a2' - -__all__ = [ - # Old names - 'base64MIME', - 'Charset', - 'Encoders', - 'Errors', - 'Generator', - 'Header', - 'Iterators', - 'Message', - 'MIMEAudio', - 'MIMEBase', - 'MIMEImage', - 'MIMEMessage', - 'MIMEMultipart', - 'MIMENonMultipart', - 'MIMEText', - 'Parser', - 'quopriMIME', - 'Utils', - 'message_from_string', - 'message_from_file', - # new names - 'base64mime', - 'charset', - 'encoders', - 'errors', - 'generator', - 'header', - 'iterators', - 'message', - 'mime', - 'parser', - 'quoprimime', - 'utils', - ] - - - -# Some convenience routines. Don't import Parser and Message as side-effects -# of importing email since those cascadingly import most of the rest of the -# email package. -def message_from_string(s, *args, **kws): - """Parse a string into a Message object model. - - Optional _class and strict are passed to the Parser constructor. - """ - from bongo.external.email.parser import Parser - return Parser(*args, **kws).parsestr(s) - - -def message_from_file(fp, *args, **kws): - """Read a file and parse its contents into a Message object model. - - Optional _class and strict are passed to the Parser constructor. - """ - from bongo.external.email.parser import Parser - return Parser(*args, **kws).parse(fp) - - - -# Lazy loading to provide name mapping from new-style names (PEP 8 compatible -# email 4.0 module names), to old-style names (email 3.0 module names). -import sys - -class LazyImporter(object): - def __init__(self, module_name): - self.__name__ = 'bongo.external.email.' + module_name - - def __getattr__(self, name): - __import__(self.__name__) - mod = sys.modules[self.__name__] - self.__dict__.update(mod.__dict__) - return getattr(mod, name) - - -_LOWERNAMES = [ - # email. -> email. - 'Charset', - 'Encoders', - 'Errors', - 'FeedParser', - 'Generator', - 'Header', - 'Iterators', - 'Message', - 'Parser', - 'Utils', - 'base64MIME', - 'quopriMIME', - ] - -_MIMENAMES = [ - # email.MIME -> email.mime. - 'Audio', - 'Base', - 'Image', - 'Message', - 'Multipart', - 'NonMultipart', - 'Text', - ] - -for _name in _LOWERNAMES: - importer = LazyImporter(_name.lower()) - sys.modules['bongo.external.email.' + _name] = importer - setattr(sys.modules['bongo.external.email'], _name, importer) - - -import bongo.external.email.mime -for _name in _MIMENAMES: - importer = LazyImporter('mime.' + _name.lower()) - sys.modules['bongo.external.email.MIME' + _name] = importer - setattr(sys.modules['bongo.external.email'], 'MIME' + _name, importer) - setattr(sys.modules['bongo.external.email.mime'], _name, importer) diff --git a/src/libs/python/bongo/external/email/_parseaddr.py b/src/libs/python/bongo/external/email/_parseaddr.py deleted file mode 100644 index 8047df2..0000000 --- a/src/libs/python/bongo/external/email/_parseaddr.py +++ /dev/null @@ -1,479 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Contact: email-sig@python.org - -"""Email address parsing code. - -Lifted directly from rfc822.py. This should eventually be rewritten. -""" - -__all__ = [ - 'mktime_tz', - 'parsedate', - 'parsedate_tz', - 'quote', - ] - -import time - -SPACE = ' ' -EMPTYSTRING = '' -COMMASPACE = ', ' - -# Parse a date field -_monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', - 'aug', 'sep', 'oct', 'nov', 'dec', - 'january', 'february', 'march', 'april', 'may', 'june', 'july', - 'august', 'september', 'october', 'november', 'december'] - -_daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] - -# The timezone table does not include the military time zones defined -# in RFC822, other than Z. According to RFC1123, the description in -# RFC822 gets the signs wrong, so we can't rely on any such time -# zones. RFC1123 recommends that numeric timezone indicators be used -# instead of timezone names. - -_timezones = {'UT':0, 'UTC':0, 'GMT':0, 'Z':0, - 'AST': -400, 'ADT': -300, # Atlantic (used in Canada) - 'EST': -500, 'EDT': -400, # Eastern - 'CST': -600, 'CDT': -500, # Central - 'MST': -700, 'MDT': -600, # Mountain - 'PST': -800, 'PDT': -700 # Pacific - } - - -def parsedate_tz(data): - """Convert a date string to a time tuple. - - Accounts for military timezones. - """ - data = data.split() - # The FWS after the comma after the day-of-week is optional, so search and - # adjust for this. - if data[0].endswith(',') or data[0].lower() in _daynames: - # There's a dayname here. Skip it - del data[0] - else: - i = data[0].rfind(',') - if i >= 0: - data[0] = data[0][i+1:] - if len(data) == 3: # RFC 850 date, deprecated - stuff = data[0].split('-') - if len(stuff) == 3: - data = stuff + data[1:] - if len(data) == 4: - s = data[3] - i = s.find('+') - if i > 0: - data[3:] = [s[:i], s[i+1:]] - else: - data.append('') # Dummy tz - if len(data) < 5: - return None - data = data[:5] - [dd, mm, yy, tm, tz] = data - mm = mm.lower() - if mm not in _monthnames: - dd, mm = mm, dd.lower() - if mm not in _monthnames: - return None - mm = _monthnames.index(mm) + 1 - if mm > 12: - mm -= 12 - if dd[-1] == ',': - dd = dd[:-1] - i = yy.find(':') - if i > 0: - yy, tm = tm, yy - if yy[-1] == ',': - yy = yy[:-1] - if not yy[0].isdigit(): - yy, tz = tz, yy - if tm[-1] == ',': - tm = tm[:-1] - tm = tm.split(':') - if len(tm) == 2: - [thh, tmm] = tm - tss = '0' - elif len(tm) == 3: - [thh, tmm, tss] = tm - else: - return None - try: - yy = int(yy) - dd = int(dd) - thh = int(thh) - tmm = int(tmm) - tss = int(tss) - except ValueError: - return None - tzoffset = None - tz = tz.upper() - if tz in _timezones: - tzoffset = _timezones[tz] - else: - try: - tzoffset = int(tz) - except ValueError: - pass - # Convert a timezone offset into seconds ; -0500 -> -18000 - if tzoffset: - if tzoffset < 0: - tzsign = -1 - tzoffset = -tzoffset - else: - tzsign = 1 - tzoffset = tzsign * ( (tzoffset//100)*3600 + (tzoffset % 100)*60) - # Daylight Saving Time flag is set to -1, since DST is unknown. - return yy, mm, dd, thh, tmm, tss, 0, 1, -1, tzoffset - - -def parsedate(data): - """Convert a time string to a time tuple.""" - t = parsedate_tz(data) - if isinstance(t, tuple): - return t[:9] - else: - return t - - -def mktime_tz(data): - """Turn a 10-tuple as returned by parsedate_tz() into a UTC timestamp.""" - if data[9] is None: - # No zone info, so localtime is better assumption than GMT - return time.mktime(data[:8] + (-1,)) - else: - t = time.mktime(data[:8] + (0,)) - return t - data[9] - time.timezone - - -def quote(str): - """Add quotes around a string.""" - return str.replace('\\', '\\\\').replace('"', '\\"') - - -class AddrlistClass: - """Address parser class by Ben Escoto. - - To understand what this class does, it helps to have a copy of RFC 2822 in - front of you. - - Note: this class interface is deprecated and may be removed in the future. - Use rfc822.AddressList instead. - """ - - def __init__(self, field): - """Initialize a new instance. - - `field' is an unparsed address header field, containing - one or more addresses. - """ - self.specials = '()<>@,:;.\"[]' - self.pos = 0 - self.LWS = ' \t' - self.CR = '\r\n' - self.atomends = self.specials + self.LWS + self.CR - # Note that RFC 2822 now specifies `.' as obs-phrase, meaning that it - # is obsolete syntax. RFC 2822 requires that we recognize obsolete - # syntax, so allow dots in phrases. - self.phraseends = self.atomends.replace('.', '') - self.field = field - self.commentlist = [] - - def gotonext(self): - """Parse up to the start of the next address.""" - while self.pos < len(self.field): - if self.field[self.pos] in self.LWS + '\n\r': - self.pos += 1 - elif self.field[self.pos] == '(': - self.commentlist.append(self.getcomment()) - else: - break - - def getaddrlist(self): - """Parse all addresses. - - Returns a list containing all of the addresses. - """ - result = [] - while self.pos < len(self.field): - ad = self.getaddress() - if ad: - result += ad - else: - result.append(('', '')) - return result - - def getaddress(self): - """Parse the next address.""" - self.commentlist = [] - self.gotonext() - - oldpos = self.pos - oldcl = self.commentlist - plist = self.getphraselist() - - self.gotonext() - returnlist = [] - - if self.pos >= len(self.field): - # Bad email address technically, no domain. - if plist: - returnlist = [(SPACE.join(self.commentlist), plist[0])] - - elif self.field[self.pos] in '.@': - # email address is just an addrspec - # this isn't very efficient since we start over - self.pos = oldpos - self.commentlist = oldcl - addrspec = self.getaddrspec() - returnlist = [(SPACE.join(self.commentlist), addrspec)] - - elif self.field[self.pos] == ':': - # address is a group - returnlist = [] - - fieldlen = len(self.field) - self.pos += 1 - while self.pos < len(self.field): - self.gotonext() - if self.pos < fieldlen and self.field[self.pos] == ';': - self.pos += 1 - break - returnlist = returnlist + self.getaddress() - - elif self.field[self.pos] == '<': - # Address is a phrase then a route addr - routeaddr = self.getrouteaddr() - - if self.commentlist: - returnlist = [(SPACE.join(plist) + ' (' + - ' '.join(self.commentlist) + ')', routeaddr)] - else: - returnlist = [(SPACE.join(plist), routeaddr)] - - else: - if plist: - returnlist = [(SPACE.join(self.commentlist), plist[0])] - elif self.field[self.pos] in self.specials: - self.pos += 1 - - self.gotonext() - if self.pos < len(self.field) and self.field[self.pos] == ',': - self.pos += 1 - return returnlist - - def getrouteaddr(self): - """Parse a route address (Return-path value). - - This method just skips all the route stuff and returns the addrspec. - """ - if self.field[self.pos] != '<': - return - - expectroute = False - self.pos += 1 - self.gotonext() - adlist = '' - while self.pos < len(self.field): - if expectroute: - self.getdomain() - expectroute = False - elif self.field[self.pos] == '>': - self.pos += 1 - break - elif self.field[self.pos] == '@': - self.pos += 1 - expectroute = True - elif self.field[self.pos] == ':': - self.pos += 1 - else: - adlist = self.getaddrspec() - self.pos += 1 - break - self.gotonext() - - return adlist - - def getaddrspec(self): - """Parse an RFC 2822 addr-spec.""" - aslist = [] - - self.gotonext() - while self.pos < len(self.field): - if self.field[self.pos] == '.': - aslist.append('.') - self.pos += 1 - elif self.field[self.pos] == '"': - aslist.append('"%s"' % self.getquote()) - elif self.field[self.pos] in self.atomends: - break - else: - aslist.append(self.getatom()) - self.gotonext() - - if self.pos >= len(self.field) or self.field[self.pos] != '@': - return EMPTYSTRING.join(aslist) - - aslist.append('@') - self.pos += 1 - self.gotonext() - return EMPTYSTRING.join(aslist) + self.getdomain() - - def getdomain(self): - """Get the complete domain name from an address.""" - sdlist = [] - while self.pos < len(self.field): - if self.field[self.pos] in self.LWS: - self.pos += 1 - elif self.field[self.pos] == '(': - self.commentlist.append(self.getcomment()) - elif self.field[self.pos] == '[': - sdlist.append(self.getdomainliteral()) - elif self.field[self.pos] == '.': - self.pos += 1 - sdlist.append('.') - elif self.field[self.pos] in self.atomends: - break - else: - sdlist.append(self.getatom()) - return EMPTYSTRING.join(sdlist) - - def getdelimited(self, beginchar, endchars, allowcomments=True): - """Parse a header fragment delimited by special characters. - - `beginchar' is the start character for the fragment. - If self is not looking at an instance of `beginchar' then - getdelimited returns the empty string. - - `endchars' is a sequence of allowable end-delimiting characters. - Parsing stops when one of these is encountered. - - If `allowcomments' is non-zero, embedded RFC 2822 comments are allowed - within the parsed fragment. - """ - if self.field[self.pos] != beginchar: - return '' - - slist = [''] - quote = False - self.pos += 1 - while self.pos < len(self.field): - if quote: - slist.append(self.field[self.pos]) - quote = False - elif self.field[self.pos] in endchars: - self.pos += 1 - break - elif allowcomments and self.field[self.pos] == '(': - slist.append(self.getcomment()) - continue # have already advanced pos from getcomment - elif self.field[self.pos] == '\\': - quote = True - else: - slist.append(self.field[self.pos]) - self.pos += 1 - - return EMPTYSTRING.join(slist) - - def getquote(self): - """Get a quote-delimited fragment from self's field.""" - return self.getdelimited('"', '"\r', False) - - def getcomment(self): - """Get a parenthesis-delimited fragment from self's field.""" - return self.getdelimited('(', ')\r', True) - - def getdomainliteral(self): - """Parse an RFC 2822 domain-literal.""" - return '[%s]' % self.getdelimited('[', ']\r', False) - - def getatom(self, atomends=None): - """Parse an RFC 2822 atom. - - Optional atomends specifies a different set of end token delimiters - (the default is to use self.atomends). This is used e.g. in - getphraselist() since phrase endings must not include the `.' (which - is legal in phrases).""" - atomlist = [''] - if atomends is None: - atomends = self.atomends - - while self.pos < len(self.field): - if self.field[self.pos] in atomends: - break - else: - atomlist.append(self.field[self.pos]) - self.pos += 1 - - return EMPTYSTRING.join(atomlist) - - def getphraselist(self): - """Parse a sequence of RFC 2822 phrases. - - A phrase is a sequence of words, which are in turn either RFC 2822 - atoms or quoted-strings. Phrases are canonicalized by squeezing all - runs of continuous whitespace into one space. - """ - plist = [] - - while self.pos < len(self.field): - if self.field[self.pos] in self.LWS: - self.pos += 1 - elif self.field[self.pos] == '"': - plist.append(self.getquote()) - elif self.field[self.pos] == '(': - self.commentlist.append(self.getcomment()) - elif self.field[self.pos] in self.phraseends: - break - else: - plist.append(self.getatom(self.phraseends)) - - return plist - -class AddressList(AddrlistClass): - """An AddressList encapsulates a list of parsed RFC 2822 addresses.""" - def __init__(self, field): - AddrlistClass.__init__(self, field) - if field: - self.addresslist = self.getaddrlist() - else: - self.addresslist = [] - - def __len__(self): - return len(self.addresslist) - - def __add__(self, other): - # Set union - newaddr = AddressList(None) - newaddr.addresslist = self.addresslist[:] - for x in other.addresslist: - if not x in self.addresslist: - newaddr.addresslist.append(x) - return newaddr - - def __iadd__(self, other): - # Set union, in-place - for x in other.addresslist: - if not x in self.addresslist: - self.addresslist.append(x) - return self - - def __sub__(self, other): - # Set difference - newaddr = AddressList(None) - for x in self.addresslist: - if not x in other.addresslist: - newaddr.addresslist.append(x) - return newaddr - - def __isub__(self, other): - # Set difference, in-place - for x in other.addresslist: - if x in self.addresslist: - self.addresslist.remove(x) - return self - - def __getitem__(self, index): - # Make indexing, slices, and 'in' work - return self.addresslist[index] diff --git a/src/libs/python/bongo/external/email/base64mime.py b/src/libs/python/bongo/external/email/base64mime.py deleted file mode 100644 index 4fa8563..0000000 --- a/src/libs/python/bongo/external/email/base64mime.py +++ /dev/null @@ -1,184 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Author: Ben Gertzfield -# Contact: email-sig@python.org - -"""Base64 content transfer encoding per RFCs 2045-2047. - -This module handles the content transfer encoding method defined in RFC 2045 -to encode arbitrary 8-bit data using the three 8-bit bytes in four 7-bit -characters encoding known as Base64. - -It is used in the MIME standards for email to attach images, audio, and text -using some 8-bit character sets to messages. - -This module provides an interface to encode and decode both headers and bodies -with Base64 encoding. - -RFC 2045 defines a method for including character set information in an -`encoded-word' in a header. This method is commonly used for 8-bit real names -in To:, From:, Cc:, etc. fields, as well as Subject: lines. - -This module does not do the line wrapping or end-of-line character conversion -necessary for proper internationalized headers; it only does dumb encoding and -decoding. To deal with the various line wrapping issues, use the email.Header -module. -""" - -__all__ = [ - 'base64_len', - 'body_decode', - 'body_encode', - 'decode', - 'decodestring', - 'encode', - 'encodestring', - 'header_encode', - ] - -import re - -from binascii import b2a_base64, a2b_base64 -from bongo.external.email.utils import fix_eols - -CRLF = '\r\n' -NL = '\n' -EMPTYSTRING = '' - -# See also Charset.py -MISC_LEN = 7 - - - -# Helpers -def base64_len(s): - """Return the length of s when it is encoded with base64.""" - groups_of_3, leftover = divmod(len(s), 3) - # 4 bytes out for each 3 bytes (or nonzero fraction thereof) in. - # Thanks, Tim! - n = groups_of_3 * 4 - if leftover: - n += 4 - return n - - - -def header_encode(header, charset='iso-8859-1', keep_eols=False, - maxlinelen=76, eol=NL): - """Encode a single header line with Base64 encoding in a given charset. - - Defined in RFC 2045, this Base64 encoding is identical to normal Base64 - encoding, except that each line must be intelligently wrapped (respecting - the Base64 encoding), and subsequent lines must start with a space. - - charset names the character set to use to encode the header. It defaults - to iso-8859-1. - - End-of-line characters (\\r, \\n, \\r\\n) will be automatically converted - to the canonical email line separator \\r\\n unless the keep_eols - parameter is True (the default is False). - - Each line of the header will be terminated in the value of eol, which - defaults to "\\n". Set this to "\\r\\n" if you are using the result of - this function directly in email. - - The resulting string will be in the form: - - "=?charset?b?WW/5ciBtYXp66XLrIHf8eiBhIGhhbXBzdGHuciBBIFlv+XIgbWF6euly?=\\n - =?charset?b?6yB3/HogYSBoYW1wc3Rh7nIgQkMgWW/5ciBtYXp66XLrIHf8eiBhIGhh?=" - - with each line wrapped at, at most, maxlinelen characters (defaults to 76 - characters). - """ - # Return empty headers unchanged - if not header: - return header - - if not keep_eols: - header = fix_eols(header) - - # Base64 encode each line, in encoded chunks no greater than maxlinelen in - # length, after the RFC chrome is added in. - base64ed = [] - max_encoded = maxlinelen - len(charset) - MISC_LEN - max_unencoded = max_encoded * 3 // 4 - - for i in range(0, len(header), max_unencoded): - base64ed.append(b2a_base64(header[i:i+max_unencoded])) - - # Now add the RFC chrome to each encoded chunk - lines = [] - for line in base64ed: - # Ignore the last character of each line if it is a newline - if line.endswith(NL): - line = line[:-1] - # Add the chrome - lines.append('=?%s?b?%s?=' % (charset, line)) - # Glue the lines together and return it. BAW: should we be able to - # specify the leading whitespace in the joiner? - joiner = eol + ' ' - return joiner.join(lines) - - - -def encode(s, binary=True, maxlinelen=76, eol=NL): - """Encode a string with base64. - - Each line will be wrapped at, at most, maxlinelen characters (defaults to - 76 characters). - - If binary is False, end-of-line characters will be converted to the - canonical email end-of-line sequence \\r\\n. Otherwise they will be left - verbatim (this is the default). - - Each line of encoded text will end with eol, which defaults to "\\n". Set - this to "\r\n" if you will be using the result of this function directly - in an email. - """ - if not s: - return s - - if not binary: - s = fix_eols(s) - - encvec = [] - max_unencoded = maxlinelen * 3 // 4 - for i in range(0, len(s), max_unencoded): - # BAW: should encode() inherit b2a_base64()'s dubious behavior in - # adding a newline to the encoded string? - enc = b2a_base64(s[i:i + max_unencoded]) - if enc.endswith(NL) and eol != NL: - enc = enc[:-1] + eol - encvec.append(enc) - return EMPTYSTRING.join(encvec) - - -# For convenience and backwards compatibility w/ standard base64 module -body_encode = encode -encodestring = encode - - - -def decode(s, convert_eols=None): - """Decode a raw base64 string. - - If convert_eols is set to a string value, all canonical email linefeeds, - e.g. "\\r\\n", in the decoded text will be converted to the value of - convert_eols. os.linesep is a good choice for convert_eols if you are - decoding a text attachment. - - This function does not parse a full MIME header value encoded with - base64 (like =?iso-8895-1?b?bmloISBuaWgh?=) -- please use the high - level email.Header class for that functionality. - """ - if not s: - return s - - dec = a2b_base64(s) - if convert_eols: - return dec.replace(CRLF, convert_eols) - return dec - - -# For convenience and backwards compatibility w/ standard base64 module -body_decode = decode -decodestring = decode diff --git a/src/libs/python/bongo/external/email/charset.py b/src/libs/python/bongo/external/email/charset.py deleted file mode 100644 index 3205b50..0000000 --- a/src/libs/python/bongo/external/email/charset.py +++ /dev/null @@ -1,398 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Ben Gertzfield, Barry Warsaw -# Contact: email-sig@python.org - -__all__ = [ - 'Charset', - 'add_alias', - 'add_charset', - 'add_codec', - ] - -import bongo.external.email.base64mime -import bongo.external.email.quoprimime - -from bongo.external.email import errors -from bongo.external.email.encoders import encode_7or8bit - - - -# Flags for types of header encodings -QP = 1 # Quoted-Printable -BASE64 = 2 # Base64 -SHORTEST = 3 # the shorter of QP and base64, but only for headers - -# In "=?charset?q?hello_world?=", the =?, ?q?, and ?= add up to 7 -MISC_LEN = 7 - -DEFAULT_CHARSET = 'us-ascii' - - - -# Defaults -CHARSETS = { - # input header enc body enc output conv - 'iso-8859-1': (QP, QP, None), - 'iso-8859-2': (QP, QP, None), - 'iso-8859-3': (QP, QP, None), - 'iso-8859-4': (QP, QP, None), - # iso-8859-5 is Cyrillic, and not especially used - # iso-8859-6 is Arabic, also not particularly used - # iso-8859-7 is Greek, QP will not make it readable - # iso-8859-8 is Hebrew, QP will not make it readable - 'iso-8859-9': (QP, QP, None), - 'iso-8859-10': (QP, QP, None), - # iso-8859-11 is Thai, QP will not make it readable - 'iso-8859-13': (QP, QP, None), - 'iso-8859-14': (QP, QP, None), - 'iso-8859-15': (QP, QP, None), - 'windows-1252':(QP, QP, None), - 'viscii': (QP, QP, None), - 'us-ascii': (None, None, None), - 'big5': (BASE64, BASE64, None), - 'gb2312': (BASE64, BASE64, None), - 'euc-jp': (BASE64, None, 'iso-2022-jp'), - 'shift_jis': (BASE64, None, 'iso-2022-jp'), - 'iso-2022-jp': (BASE64, None, None), - 'koi8-r': (BASE64, BASE64, None), - 'utf-8': (SHORTEST, BASE64, 'utf-8'), - # We're making this one up to represent raw unencoded 8-bit - '8bit': (None, BASE64, None), - } - -# Aliases for other commonly-used names for character sets. Map -# them to the real ones used in email. -ALIASES = { - 'latin_1': 'iso-8859-1', - 'latin-1': 'iso-8859-1', - 'latin_2': 'iso-8859-2', - 'latin-2': 'iso-8859-2', - 'latin_3': 'iso-8859-3', - 'latin-3': 'iso-8859-3', - 'latin_4': 'iso-8859-4', - 'latin-4': 'iso-8859-4', - 'latin_5': 'iso-8859-9', - 'latin-5': 'iso-8859-9', - 'latin_6': 'iso-8859-10', - 'latin-6': 'iso-8859-10', - 'latin_7': 'iso-8859-13', - 'latin-7': 'iso-8859-13', - 'latin_8': 'iso-8859-14', - 'latin-8': 'iso-8859-14', - 'latin_9': 'iso-8859-15', - 'latin-9': 'iso-8859-15', - 'cp949': 'ks_c_5601-1987', - 'euc_jp': 'euc-jp', - 'euc_kr': 'euc-kr', - 'ascii': 'us-ascii', - } - - -# Map charsets to their Unicode codec strings. -CODEC_MAP = { - 'gb2312': 'eucgb2312_cn', - 'big5': 'big5_tw', - # Hack: We don't want *any* conversion for stuff marked us-ascii, as all - # sorts of garbage might be sent to us in the guise of 7-bit us-ascii. - # Let that stuff pass through without conversion to/from Unicode. - 'us-ascii': None, - } - - - -# Convenience functions for extending the above mappings -def add_charset(charset, header_enc=None, body_enc=None, output_charset=None): - """Add character set properties to the global registry. - - charset is the input character set, and must be the canonical name of a - character set. - - Optional header_enc and body_enc is either Charset.QP for - quoted-printable, Charset.BASE64 for base64 encoding, Charset.SHORTEST for - the shortest of qp or base64 encoding, or None for no encoding. SHORTEST - is only valid for header_enc. It describes how message headers and - message bodies in the input charset are to be encoded. Default is no - encoding. - - Optional output_charset is the character set that the output should be - in. Conversions will proceed from input charset, to Unicode, to the - output charset when the method Charset.convert() is called. The default - is to output in the same character set as the input. - - Both input_charset and output_charset must have Unicode codec entries in - the module's charset-to-codec mapping; use add_codec(charset, codecname) - to add codecs the module does not know about. See the codecs module's - documentation for more information. - """ - if body_enc == SHORTEST: - raise ValueError('SHORTEST not allowed for body_enc') - CHARSETS[charset] = (header_enc, body_enc, output_charset) - - -def add_alias(alias, canonical): - """Add a character set alias. - - alias is the alias name, e.g. latin-1 - canonical is the character set's canonical name, e.g. iso-8859-1 - """ - ALIASES[alias] = canonical - - -def add_codec(charset, codecname): - """Add a codec that map characters in the given charset to/from Unicode. - - charset is the canonical name of a character set. codecname is the name - of a Python codec, as appropriate for the second argument to the unicode() - built-in, or to the encode() method of a Unicode string. - """ - CODEC_MAP[charset] = codecname - - - -class Charset: - """Map character sets to their email properties. - - This class provides information about the requirements imposed on email - for a specific character set. It also provides convenience routines for - converting between character sets, given the availability of the - applicable codecs. Given a character set, it will do its best to provide - information on how to use that character set in an email in an - RFC-compliant way. - - Certain character sets must be encoded with quoted-printable or base64 - when used in email headers or bodies. Certain character sets must be - converted outright, and are not allowed in email. Instances of this - module expose the following information about a character set: - - input_charset: The initial character set specified. Common aliases - are converted to their `official' email names (e.g. latin_1 - is converted to iso-8859-1). Defaults to 7-bit us-ascii. - - header_encoding: If the character set must be encoded before it can be - used in an email header, this attribute will be set to - Charset.QP (for quoted-printable), Charset.BASE64 (for - base64 encoding), or Charset.SHORTEST for the shortest of - QP or BASE64 encoding. Otherwise, it will be None. - - body_encoding: Same as header_encoding, but describes the encoding for the - mail message's body, which indeed may be different than the - header encoding. Charset.SHORTEST is not allowed for - body_encoding. - - output_charset: Some character sets must be converted before the can be - used in email headers or bodies. If the input_charset is - one of them, this attribute will contain the name of the - charset output will be converted to. Otherwise, it will - be None. - - input_codec: The name of the Python codec used to convert the - input_charset to Unicode. If no conversion codec is - necessary, this attribute will be None. - - output_codec: The name of the Python codec used to convert Unicode - to the output_charset. If no conversion codec is necessary, - this attribute will have the same value as the input_codec. - """ - def __init__(self, input_charset=DEFAULT_CHARSET, body_encoding=None): - # RFC 2046, $4.1.2 says charsets are not case sensitive. We coerce to - # unicode because its .lower() is locale insensitive. If the argument - # is already a unicode, we leave it at that, but ensure that the - # charset is ASCII, as the standard (RFC XXX) requires. - try: - if isinstance(input_charset, str): - input_charset.encode('ascii') - else: - input_charset = str(input_charset, 'ascii') - except UnicodeError: - raise errors.CharsetError(input_charset) - input_charset = input_charset.lower() - # Set the input charset after filtering through the aliases - self.input_charset = ALIASES.get(input_charset, input_charset) - # We can try to guess which encoding and conversion to use by the - # charset_map dictionary. Try that first, but let the user override - # it. - henc, benc, conv = CHARSETS.get(self.input_charset, - (SHORTEST, BASE64, None)) - if not conv: - conv = self.input_charset - # Set the attributes, allowing the arguments to override the default. - self.header_encoding = henc - - if body_encoding : - body_encoding = body_encoding.lower() - if body_encoding == 'base64' : - self.body_encoding = BASE64 - elif body_encoding == 'quoted-printable' : - self.body_encoding = QP - else : - self.body_encoding = benc - else : - self.body_encoding = benc - self.output_charset = ALIASES.get(conv, conv) - # Now set the codecs. If one isn't defined for input_charset, - # guess and try a Unicode codec with the same name as input_codec. - self.input_codec = CODEC_MAP.get(self.input_charset, - self.input_charset) - self.output_codec = CODEC_MAP.get(self.output_charset, - self.output_charset) - - def __str__(self): - return self.input_charset.lower() - - __repr__ = __str__ - - def __eq__(self, other): - return str(self) == str(other).lower() - - def __ne__(self, other): - return not self.__eq__(other) - - def get_body_encoding(self): - """Return the content-transfer-encoding used for body encoding. - - This is either the string `quoted-printable' or `base64' depending on - the encoding used, or it is a function in which case you should call - the function with a single argument, the Message object being - encoded. The function should then set the Content-Transfer-Encoding - header itself to whatever is appropriate. - - Returns "quoted-printable" if self.body_encoding is QP. - Returns "base64" if self.body_encoding is BASE64. - Returns "7bit" otherwise. - """ - assert self.body_encoding != SHORTEST - if self.body_encoding == QP: - return 'quoted-printable' - elif self.body_encoding == BASE64: - return 'base64' - else: - return encode_7or8bit - - def convert(self, s): - """Convert a string from the input_codec to the output_codec.""" - if self.input_codec != self.output_codec: - return str(s, self.input_codec).encode(self.output_codec) - else: - return s - - def to_splittable(self, s): - """Convert a possibly multibyte string to a safely splittable format. - - Uses the input_codec to try and convert the string to Unicode, so it - can be safely split on character boundaries (even for multibyte - characters). - - Returns the string as-is if it isn't known how to convert it to - Unicode with the input_charset. - - Characters that could not be converted to Unicode will be replaced - with the Unicode replacement character U+FFFD. - """ - if isinstance(s, str) or self.input_codec is None: - return s - try: - return str(s, self.input_codec, 'replace') - except LookupError: - # Input codec not installed on system, so return the original - # string unchanged. - return s - - def from_splittable(self, ustr, to_output=True): - """Convert a splittable string back into an encoded string. - - Uses the proper codec to try and convert the string from Unicode back - into an encoded format. Return the string as-is if it is not Unicode, - or if it could not be converted from Unicode. - - Characters that could not be converted from Unicode will be replaced - with an appropriate character (usually '?'). - - If to_output is True (the default), uses output_codec to convert to an - encoded format. If to_output is False, uses input_codec. - """ - if to_output: - codec = self.output_codec - else: - codec = self.input_codec - if not isinstance(ustr, str) or codec is None: - return ustr - try: - return ustr.encode(codec, 'replace') - except LookupError: - # Output codec not installed - return ustr - - def get_output_charset(self): - """Return the output character set. - - This is self.output_charset if that is not None, otherwise it is - self.input_charset. - """ - return self.output_charset or self.input_charset - - def encoded_header_len(self, s): - """Return the length of the encoded header string.""" - cset = self.get_output_charset() - # The len(s) of a 7bit encoding is len(s) - if self.header_encoding == BASE64: - return bongo.external.email.base64mime.base64_len(s) + len(cset) + MISC_LEN - elif self.header_encoding == QP: - return bongo.external.email.quoprimime.header_quopri_len(s) + len(cset) + MISC_LEN - elif self.header_encoding == SHORTEST: - lenb64 = bongo.external.email.base64mime.base64_len(s) - lenqp = bongo.external.email.quoprimime.header_quopri_len(s) - return min(lenb64, lenqp) + len(cset) + MISC_LEN - else: - return len(s) - - def header_encode(self, s, convert=False): - """Header-encode a string, optionally converting it to output_charset. - - If convert is True, the string will be converted from the input - charset to the output charset automatically. This is not useful for - multibyte character sets, which have line length issues (multibyte - characters must be split on a character, not a byte boundary); use the - high-level Header class to deal with these issues. convert defaults - to False. - - The type of encoding (base64 or quoted-printable) will be based on - self.header_encoding. - """ - cset = self.get_output_charset() - if convert: - s = self.convert(s) - # 7bit/8bit encodings return the string unchanged (modulo conversions) - if self.header_encoding == BASE64: - return bongo.external.email.base64mime.header_encode(s, cset) - elif self.header_encoding == QP: - return bongo.external.email.quoprimime.header_encode(s, cset, maxlinelen=None) - elif self.header_encoding == SHORTEST: - lenb64 = bongo.external.email.base64mime.base64_len(s) - lenqp = bongo.external.email.quoprimime.header_quopri_len(s) - if lenb64 < lenqp: - return bongo.external.email.base64mime.header_encode(s, cset) - else: - return bongo.external.email.quoprimime.header_encode(s, cset, maxlinelen=None) - else: - return s - - def body_encode(self, s, convert=True): - """Body-encode a string and convert it to output_charset. - - If convert is True (the default), the string will be converted from - the input charset to output charset automatically. Unlike - header_encode(), there are no issues with byte boundaries and - multibyte charsets in email bodies, so this is usually pretty safe. - - The type of encoding (base64 or quoted-printable) will be based on - self.body_encoding. - """ - if convert: - s = self.convert(s) - # 7bit/8bit encodings return the string unchanged (module conversions) - if self.body_encoding is BASE64: - return bongo.external.email.base64mime.body_encode(s) - elif self.body_encoding is QP: - return bongo.external.email.quoprimime.body_encode(s) - else: - return s diff --git a/src/libs/python/bongo/external/email/encoders.py b/src/libs/python/bongo/external/email/encoders.py deleted file mode 100644 index 06016cd..0000000 --- a/src/libs/python/bongo/external/email/encoders.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Encodings and related functions.""" - -__all__ = [ - 'encode_7or8bit', - 'encode_base64', - 'encode_noop', - 'encode_quopri', - ] - -import base64 - -from quopri import encodestring as _encodestring - - - -def _qencode(s): - enc = _encodestring(s, quotetabs=True) - # Must encode spaces, which quopri.encodestring() doesn't do - return enc.replace(' ', '=20') - - -def _bencode(s): - # We can't quite use base64.encodestring() since it tacks on a "courtesy - # newline". Blech! - if not s: - return s - hasnewline = (s[-1] == '\n') - value = base64.encodestring(s) - if not hasnewline and value[-1] == '\n': - return value[:-1] - return value - - - -def encode_base64(msg): - """Encode the message's payload in Base64. - - Also, add an appropriate Content-Transfer-Encoding header. - """ - orig = msg.get_payload() - encdata = _bencode(orig) - msg.set_payload(encdata) - msg['Content-Transfer-Encoding'] = 'base64' - - - -def encode_quopri(msg): - """Encode the message's payload in quoted-printable. - - Also, add an appropriate Content-Transfer-Encoding header. - """ - orig = msg.get_payload() - encdata = _qencode(orig) - msg.set_payload(encdata) - msg['Content-Transfer-Encoding'] = 'quoted-printable' - - - -def encode_7or8bit(msg): - """Set the Content-Transfer-Encoding header to 7bit or 8bit.""" - orig = msg.get_payload() - if orig is None: - # There's no payload. For backwards compatibility we use 7bit - msg['Content-Transfer-Encoding'] = '7bit' - return - # We play a trick to make this go fast. If encoding to ASCII succeeds, we - # know the data must be 7bit, otherwise treat it as 8bit. - try: - orig.encode('ascii') - except UnicodeError: - # iso-2022-* is non-ASCII but still 7-bit - charset = msg.get_charset() - output_cset = charset and charset.output_charset - if output_cset and output_cset.lower().startswith('iso-2202-'): - msg['Content-Transfer-Encoding'] = '7bit' - else: - msg['Content-Transfer-Encoding'] = '8bit' - else: - msg['Content-Transfer-Encoding'] = '7bit' - - - -def encode_noop(msg): - """Do nothing.""" diff --git a/src/libs/python/bongo/external/email/errors.py b/src/libs/python/bongo/external/email/errors.py deleted file mode 100644 index d52a624..0000000 --- a/src/libs/python/bongo/external/email/errors.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""email package exception classes.""" - - - -class MessageError(Exception): - """Base class for errors in the email package.""" - - -class MessageParseError(MessageError): - """Base class for message parsing errors.""" - - -class HeaderParseError(MessageParseError): - """Error while parsing headers.""" - - -class BoundaryError(MessageParseError): - """Couldn't find terminating boundary.""" - - -class MultipartConversionError(MessageError, TypeError): - """Conversion to a multipart is prohibited.""" - - -class CharsetError(MessageError): - """An illegal charset was given.""" - - - -# These are parsing defects which the parser was able to work around. -class MessageDefect: - """Base class for a message defect.""" - - def __init__(self, line=None): - self.line = line - -class NoBoundaryInMultipartDefect(MessageDefect): - """A message claimed to be a multipart but had no boundary parameter.""" - -class StartBoundaryNotFoundDefect(MessageDefect): - """The claimed start boundary was never found.""" - -class FirstHeaderLineIsContinuationDefect(MessageDefect): - """A message had a continuation line as its first header line.""" - -class MisplacedEnvelopeHeaderDefect(MessageDefect): - """A 'Unix-from' header was found in the middle of a header block.""" - -class MalformedHeaderDefect(MessageDefect): - """Found a header that was missing a colon, or was otherwise malformed.""" - -class MultipartInvariantViolationDefect(MessageDefect): - """A message claimed to be a multipart but no subparts were found.""" diff --git a/src/libs/python/bongo/external/email/feedparser.py b/src/libs/python/bongo/external/email/feedparser.py deleted file mode 100644 index 22e8211..0000000 --- a/src/libs/python/bongo/external/email/feedparser.py +++ /dev/null @@ -1,479 +0,0 @@ -# Copyright (C) 2004-2006 Python Software Foundation -# Authors: Baxter, Wouters and Warsaw -# Contact: email-sig@python.org - -"""FeedParser - An email feed parser. - -The feed parser implements an interface for incrementally parsing an email -message, line by line. This has advantages for certain applications, such as -those reading email messages off a socket. - -FeedParser.feed() is the primary interface for pushing new data into the -parser. It returns when there's nothing more it can do with the available -data. When you have no more data to push into the parser, call .close(). -This completes the parsing and returns the root message object. - -The other advantage of this parser is that it will never throw a parsing -exception. Instead, when it finds something unexpected, it adds a 'defect' to -the current message. Defects are just instances that live on the message -object's .defects attribute. -""" - -__all__ = ['FeedParser'] - -import re - -from bongo.external.email import errors -from bongo.external.email import message -from bongo.external.email import payloads - -NLCRE = re.compile('\r\n|\r|\n') -NLCRE_bol = re.compile('(\r\n|\r|\n)') -NLCRE_eol = re.compile('(\r\n|\r|\n)$') -NLCRE_crack = re.compile('(\r\n|\r|\n)') -# RFC 2822 $3.6.8 Optional fields. ftext is %d33-57 / %d59-126, Any character -# except controls, SP, and ":". -headerRE = re.compile(r'^(From |[\041-\071\073-\176]{1,}:|[\t ])') -EMPTYSTRING = '' -NL = '\r\n' - -NeedMoreData = object() - - - -class BufferedSubFile(object): - """A file-ish object that can have new data loaded into it. - - You can also push and pop line-matching predicates onto a stack. When the - current predicate matches the current line, a false EOF response - (i.e. empty string) is returned instead. This lets the parser adhere to a - simple abstraction -- it parses until EOF closes the current message. - """ - def __init__(self): - # The last partial line pushed into this object. - self._partial = '' - # The list of full, pushed lines, in reverse order - self._lines = [] - # The stack of false-EOF checking predicates. - self._eofstack = [] - # A flag indicating whether the file has been closed or not. - self._closed = False - - def push_eof_matcher(self, pred): - self._eofstack.append(pred) - - def pop_eof_matcher(self): - return self._eofstack.pop() - - def close(self): - # Don't forget any trailing partial line. - self._lines.append(self._partial) - self._partial = '' - self._closed = True - - def readline(self): - if not self._lines: - if self._closed: - return '' - return NeedMoreData - # Pop the line off the stack and see if it matches the current - # false-EOF predicate. - line = self._lines.pop() - # RFC 2046, section 5.1.2 requires us to recognize outer level - # boundaries at any level of inner nesting. Do this, but be sure it's - # in the order of most to least nested. - for ateof in self._eofstack[::-1]: - if ateof(line): - # We're at the false EOF. But push the last line back first. - self._lines.append(line) - return '' - return line - - def unreadline(self, line): - # Let the consumer push a line back into the buffer. - assert line is not NeedMoreData - self._lines.append(line) - - def push(self, data): - """Push some new data into this object.""" - # Handle any previous leftovers - data, self._partial = self._partial + data, '' - # Crack into lines, but preserve the newlines on the end of each - parts = NLCRE_crack.split(data) - # The *ahem* interesting behaviour of re.split when supplied grouping - # parentheses is that the last element of the resulting list is the - # data after the final RE. In the case of a NL/CR terminated string, - # this is the empty string. - self._partial = parts.pop() - # parts is a list of strings, alternating between the line contents - # and the eol character(s). Gather up a list of lines after - # re-attaching the newlines. - lines = [] - for i in range(len(parts) // 2): - lines.append(parts[i*2] + parts[i*2+1]) - self.pushlines(lines) - - def pushlines(self, lines): - # Reverse and insert at the front of the lines. - self._lines[:0] = lines[::-1] - - def is_closed(self): - return self._closed - - def __iter__(self): - return self - - def __next__(self): - line = self.readline() - if line == '': - raise StopIteration - return line - - - -class FeedParser: - """A feed-style parser of email.""" - - def __init__(self, _factory=message.Message, _payloadfactory=payloads.MemoryPayload): - """_factory is called with no arguments to create a new message obj""" - self._factory = _factory - self._payloadfactory = _payloadfactory - self._input = BufferedSubFile() - self._msgstack = [] - self._parse = self._parsegen().__next__ - self._cur = None - self._last = None - self._headersonly = False - - # Non-public interface for supporting Parser's headersonly flag - def _set_headersonly(self): - self._headersonly = True - - def feed(self, data): - """Push more data into the parser.""" - self._input.push(data) - self._call_parse() - - def _call_parse(self): - try: - self._parse() - except StopIteration: - pass - - def close(self): - """Parse all remaining data and return the root message object.""" - self._input.close() - self._call_parse() - root = self._pop_message() - assert not self._msgstack - # Look for final set of defects - if root.get_content_maintype() == 'multipart' \ - and not root.is_multipart(): - root.defects.append(errors.MultipartInvariantViolationDefect()) - return root - - def _new_message(self): - msg = self._factory() - if self._cur and self._cur.get_content_type() == 'multipart/digest': - msg.set_default_type('message/rfc822') - if self._msgstack: - self._msgstack[-1].attach(msg) - self._msgstack.append(msg) - self._cur = msg - self._last = msg - - def _pop_message(self): - retval = self._msgstack.pop() - if self._msgstack: - self._cur = self._msgstack[-1] - else: - self._cur = None - return retval - - def _parsegen(self): - # Create a new message and start by parsing headers. - self._new_message() - headers = [] - # Collect the headers, searching for a line that doesn't match the RFC - # 2822 header or continuation pattern (including an empty line). - for line in self._input: - if line is NeedMoreData: - yield NeedMoreData - continue - if not headerRE.match(line): - # If we saw the RFC defined header/body separator - # (i.e. newline), just throw it away. Otherwise the line is - # part of the body so push it back. - if not NLCRE.match(line): - self._input.unreadline(line) - break - headers.append(line) - # Done with the headers, so parse them and figure out what we're - # supposed to see in the body of the message. - self._parse_headers(headers) - # Headers-only parsing is a backwards compatibility hack, which was - # necessary in the older parser, which could throw errors. All - # remaining lines in the input are thrown into the message body. - if self._headersonly: - payload = self._payloadfactory(self._cur["Content-Transfer-Encoding"]) - while True: - line = self._input.readline() - if line is NeedMoreData: - yield NeedMoreData - continue - if line == '': - break - payload.add(line) - self._cur.set_payload(payload) - return - if self._cur.get_content_type() == 'message/delivery-status': - # message/delivery-status contains blocks of headers separated by - # a blank line. We'll represent each header block as a separate - # nested message object, but the processing is a bit different - # than standard message/* types because there is no body for the - # nested messages. A blank line separates the subparts. - while True: - self._input.push_eof_matcher(NLCRE.match) - for retval in self._parsegen(): - if retval is NeedMoreData: - yield NeedMoreData - continue - break - msg = self._pop_message() - # We need to pop the EOF matcher in order to tell if we're at - # the end of the current file, not the end of the last block - # of message headers. - self._input.pop_eof_matcher() - # The input stream must be sitting at the newline or at the - # EOF. We want to see if we're at the end of this subpart, so - # first consume the blank line, then test the next line to see - # if we're at this subpart's EOF. - while True: - line = self._input.readline() - if line is NeedMoreData: - yield NeedMoreData - continue - break - while True: - line = self._input.readline() - if line is NeedMoreData: - yield NeedMoreData - continue - break - if line == '': - break - # Not at EOF so this is a line we're going to need. - self._input.unreadline(line) - return - if self._cur.get_content_maintype() == 'message': - # The message claims to be a message/* type, then what follows is - # another RFC 2822 message. - for retval in self._parsegen(): - if retval is NeedMoreData: - yield NeedMoreData - continue - break - self._pop_message() - return - if self._cur.get_content_maintype() == 'multipart': - boundary = self._cur.get_boundary() - if boundary is None: - # The message /claims/ to be a multipart but it has not - # defined a boundary. That's a problem which we'll handle by - # reading everything until the EOF and marking the message as - # defective. - self._cur.defects.append(errors.NoBoundaryInMultipartDefect()) - payload = self._payloadfactory(self._cur["Content-Transfer-Encoding"]) - for line in self._input: - if line is NeedMoreData: - yield NeedMoreData - continue - payload.add(line) - self._cur.set_payload(payload) - return - # Create a line match predicate which matches the inter-part - # boundary as well as the end-of-multipart boundary. Don't push - # this onto the input stream until we've scanned past the - # preamble. - separator = '--' + boundary - boundaryre = re.compile( - '(?P' + re.escape(separator) + - r')(?P--)?(?P[ \t]*)(?P\r\n|\r|\n)?$') - capturing_preamble = True - preamble = [] - linesep = False - while True: - line = self._input.readline() - if line is NeedMoreData: - yield NeedMoreData - continue - if line == '': - break - mo = boundaryre.match(line) - if mo: - # If we're looking at the end boundary, we're done with - # this multipart. If there was a newline at the end of - # the closing boundary, then we need to initialize the - # epilogue with the empty string (see below). - if mo.group('end'): - linesep = mo.group('linesep') - break - # We saw an inter-part boundary. Were we in the preamble? - if capturing_preamble: - if preamble: - # According to RFC 2046, the last newline belongs - # to the boundary. - lastline = preamble[-1] - eolmo = NLCRE_eol.search(lastline) - if eolmo: - preamble[-1] = lastline[:-len(eolmo.group(0))] - self._cur.preamble = EMPTYSTRING.join(preamble) - capturing_preamble = False - self._input.unreadline(line) - continue - # We saw a boundary separating two parts. Consume any - # multiple boundary lines that may be following. Our - # interpretation of RFC 2046 BNF grammar does not produce - # body parts within such double boundaries. - while True: - line = self._input.readline() - if line is NeedMoreData: - yield NeedMoreData - continue - mo = boundaryre.match(line) - if not mo: - self._input.unreadline(line) - break - # Recurse to parse this subpart; the input stream points - # at the subpart's first line. - self._input.push_eof_matcher(boundaryre.match) - for retval in self._parsegen(): - if retval is NeedMoreData: - yield NeedMoreData - continue - break - # Because of RFC 2046, the newline preceding the boundary - # separator actually belongs to the boundary, not the - # previous subpart's payload (or epilogue if the previous - # part is a multipart). - if self._last.get_content_maintype() == 'multipart': - epilogue = self._last.epilogue - if epilogue == '': - self._last.epilogue = None - elif epilogue is not None: - mo = NLCRE_eol.search(epilogue) - if mo: - end = len(mo.group(0)) - self._last.epilogue = epilogue[:-end] - else: - # FIXME - payload = self._last.get_payload_obj() - payload.strip_eol() - self._input.pop_eof_matcher() - self._pop_message() - # Set the multipart up for newline cleansing, which will - # happen if we're in a nested multipart. - self._last = self._cur - else: - # I think we must be in the preamble - assert capturing_preamble - preamble.append(line) - # We've seen either the EOF or the end boundary. If we're still - # capturing the preamble, we never saw the start boundary. Note - # that as a defect and store the captured text as the payload. - # Everything from here to the EOF is epilogue. - if capturing_preamble: - self._cur.defects.append(errors.StartBoundaryNotFoundDefect()) - self._cur.set_payload(EMPTYSTRING.join(preamble)) - epilogue = [] - for line in self._input: - if line is NeedMoreData: - yield NeedMoreData - continue - self._cur.epilogue = EMPTYSTRING.join(epilogue) - return - # If the end boundary ended in a newline, we'll need to make sure - # the epilogue isn't None - if linesep: - epilogue = [''] - else: - epilogue = [] - for line in self._input: - if line is NeedMoreData: - yield NeedMoreData - continue - epilogue.append(line) - # Any CRLF at the front of the epilogue is not technically part of - # the epilogue. Also, watch out for an empty string epilogue, - # which means a single newline. - if epilogue: - firstline = epilogue[0] - bolmo = NLCRE_bol.match(firstline) - if bolmo: - epilogue[0] = firstline[len(bolmo.group(0)):] - self._cur.epilogue = EMPTYSTRING.join(epilogue) - return - # Otherwise, it's some non-multipart type, so the entire rest of the - # file contents becomes the payload. - payload = self._payloadfactory(self._cur["Content-Transfer-Encoding"]) - for line in self._input: - if line is NeedMoreData: - yield NeedMoreData - continue - payload.add(line) - self._cur.set_payload(payload) - - def _parse_headers(self, lines): - # Passed a list of lines that make up the headers for the current msg - lastheader = '' - lastvalue = [] - for lineno, line in enumerate(lines): - # Check for continuation - if line[0] in ' \t': - if not lastheader: - # The first line of the headers was a continuation. This - # is illegal, so let's note the defect, store the illegal - # line, and ignore it for purposes of headers. - defect = errors.FirstHeaderLineIsContinuationDefect(line) - self._cur.defects.append(defect) - continue - lastvalue.append(line) - continue - if lastheader: - # XXX reconsider the joining of folded lines - lhdr = EMPTYSTRING.join(lastvalue)[:-1].rstrip('\r\n') - self._cur[lastheader] = lhdr - lastheader, lastvalue = '', [] - # Check for envelope header, i.e. unix-from - if line.startswith('From '): - if lineno == 0: - # Strip off the trailing newline - mo = NLCRE_eol.search(line) - if mo: - line = line[:-len(mo.group(0))] - self._cur.set_unixfrom(line) - continue - elif lineno == len(lines) - 1: - # Something looking like a unix-from at the end - it's - # probably the first line of the body, so push back the - # line and stop. - self._input.unreadline(line) - return - else: - # Weirdly placed unix-from line. Note this as a defect - # and ignore it. - defect = errors.MisplacedEnvelopeHeaderDefect(line) - self._cur.defects.append(defect) - continue - # Split the line on the colon separating field name from value. - i = line.find(':') - if i < 0: - defect = errors.MalformedHeaderDefect(line) - self._cur.defects.append(defect) - continue - lastheader = line[:i] - lastvalue = [line[i+1:].lstrip()] - # Done with all the lines, so handle the last header. - if lastheader: - # XXX reconsider the joining of folded lines - self._cur[lastheader] = EMPTYSTRING.join(lastvalue).rstrip('\r\n') diff --git a/src/libs/python/bongo/external/email/filegenerator.py b/src/libs/python/bongo/external/email/filegenerator.py deleted file mode 100644 index 1e5a43e..0000000 --- a/src/libs/python/bongo/external/email/filegenerator.py +++ /dev/null @@ -1,203 +0,0 @@ -__all__ = ['FileGenerator'] - -import re -import sys -import time -import random -import warnings - -from bongo.external.email.header import Header -from bongo.external.email.payloads import Payload -import bongo.external.email.generator - -from io import StringIO -UNDERSCORE = '_' -NL = '\n' - -fcre = re.compile(r'^From ', re.MULTILINE) - -def _is8bitstring(s): - if isinstance(s, str): - try: - str(s, 'us-ascii') - except UnicodeError: - return True - return False - -class FileGenerator : - def __init__(self, outfp, manglefrom, maxheaderlen=78) : - self._fp = outfp - self._maxheaderlen = maxheaderlen - self._manglefrom = manglefrom - self._extra = None - - def clone(self, fp) : - return self.__class__(fp, self._manglefrom, self._maxheaderlen) - - def flatten(self, msg, unixfrom=False) : - if unixfrom : - ufrom = msg.get_unixfrom() - if not ufrom : - ufrom = 'From nobody ' + time.ctime(time.time()) - print(ufrom, file=self._fp) - self._write(msg) - - def write(self, buf, manglefrom=True) : - if self._extra : - buf = self._extra + buf - self._extra = None - - if manglefrom and self._manglefrom : - buf = fcre.sub('>From ', buf) - - if len(buf) > 4 : - extra = buf[-4:] - if extra != 'From' : - self._extra = extra - buf = buf[:-4] - - self._fp.write(buf) - - def flush(self) : - if self._extra : - self._fp.write(self._extra) - self._extra = None - - def _write(self, msg) : - self._write_headers(msg) - self._dispatch(msg) - - def _dispatch(self, msg): - # Get the Content-Type: for the message, then try to dispatch to - # self._handle__(). If there's no handler for the - # full MIME type, then dispatch to self._handle_(). If - # that's missing too, then dispatch to self._writeBody(). - main = msg.get_content_maintype() - sub = msg.get_content_subtype() - specific = UNDERSCORE.join((main, sub)).replace('-', '_') - meth = getattr(self, '_handle_' + specific, None) - if meth is None: - generic = main.replace('-', '_') - meth = getattr(self, '_handle_' + generic, None) - if meth is None: - meth = self._writeBody - meth(msg) - self.flush() - - # - # Default handlers - # - - def _write_headers(self, msg, trailing_newline=True): - self.flush() - if msg.get_content_maintype() == "multipart" : - boundary = msg.get_boundary() - if not boundary : - msg.set_boundary(bongo.external.email.generator._make_boundary()) - - for h, v in list(msg.items()): - print('%s:' % h, end=' ', file=self._fp) - if self._maxheaderlen == 0: - # Explicit no-wrapping - print(v, file=self._fp) - elif isinstance(v, Header): - # Header instances know what to do - print(v.encode(), file=self._fp) - elif _is8bitstring(v): - # If we have raw 8bit data in a byte string, we have no idea - # what the encoding is. There is no safe way to split this - # string. If it's ascii-subset, then we could do a normal - # ascii split, but if it's multibyte then we could break the - # string. There's no way to know so the least harm seems to - # be to not split the string and risk it being too long. - print(v, file=self._fp) - else: - # Header's got lots of smarts, so use it. - print(Header( - v, maxlinelen=self._maxheaderlen, - header_name=h, continuation_ws='\t').encode(), file=self._fp) - # A blank line always separates headers from body - if trailing_newline : - print(file=self._fp) - - # - # Handlers for writing types and subtypes - # - - def _handle_text(self, msg): - for buf in msg.iter_payload() : - self.write(buf, True) - - # Default body handler - _writeBody = _handle_text - - def _handle_multipart(self, msg): - subparts = msg.get_payload_obj() - - boundary = msg.get_boundary(failobj=bongo.external.email.generator._make_boundary()) - - if subparts is None : - subparts = [] - elif isinstance(subparts, Payload) : - self._handle_text(msg) - return - - # FIXME: fix bad boundaries - start = self._fp.tell() - - self.flush() - if msg.preamble is not None : - print(msg.preamble, file=self._fp) - - print('--' + boundary, file=self._fp) - - if isinstance(subparts, list) : - first = True - for part in subparts : - if not first: - self.flush() - print(NL + '--' + boundary, file=self._fp) - first = False - - g = self.clone(self._fp) - g.flatten(part) - self.write(NL + '--' + boundary + '--') - - if msg.epilogue is not None : - self.flush() - print(file=self._fp) - self.write(msg.epilogue, True) - - def _strip_newline(self) : - pass - - def _handle_message_delivery_status(self, msg): - # We can't just write the headers directly to self's file object - # because this will leave an extra newline between the last header - # block and the boundary. Sigh. - blocks = [] - for part in msg.get_payload(): - s = StringIO() - g = self.clone(s) - g.flatten(part, unixfrom=False) - text = s.getvalue() - lines = text.split('\n') - # Strip off the unnecessary trailing empty line - if lines and lines[-1] == '': - blocks.append(NL.join(lines[:-1])) - else: - blocks.append(text) - # Now join all the blocks with an empty line. This has the lovely - # effect of separating each block with an empty line, but not adding - # an extra one after the last one. - self.write(NL.join(blocks), True) - - def _handle_message(self, msg): - g = self.clone(self._fp) - # The payload of a message/rfc822 part should be a multipart sequence - # of length 1. The zeroth element of the list should be the Message - # object for the subpart. Extract that object, stringify it, and - # write it out. - g.flatten(msg.get_payload(0), unixfrom=False) - - diff --git a/src/libs/python/bongo/external/email/generator.py b/src/libs/python/bongo/external/email/generator.py deleted file mode 100644 index a3a0505..0000000 --- a/src/libs/python/bongo/external/email/generator.py +++ /dev/null @@ -1,352 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Classes to generate plain text from a message object tree.""" - -__all__ = ['Generator', 'DecodedGenerator'] - -import re -import sys -import time -import random -import warnings - -from io import StringIO -from bongo.external.email.header import Header -from bongo.external.email.filegenerator import FileGenerator - -UNDERSCORE = '_' -NL = '\n' - -fcre = re.compile(r'^From ', re.MULTILINE) - -def _is8bitstring(s): - if isinstance(s, str): - try: - str(s, 'us-ascii') - except UnicodeError: - return True - return False - -class Generator(FileGenerator) : - def __init__(self, outfp, mangle_from_=True, maxheaderlen=78) : - FileGenerator.__init__(self, outfp, mangle_from_, maxheaderlen) - -class OldGenerator: - """Generates output from a Message object tree. - - This basic generator writes the message to the given file object as plain - text. - """ - # - # Public interface - # - - def __init__(self, outfp, mangle_from_=True, maxheaderlen=78): - """Create the generator for message flattening. - - outfp is the output file-like object for writing the message to. It - must have a write() method. - - Optional mangle_from_ is a flag that, when True (the default), escapes - From_ lines in the body of the message by putting a `>' in front of - them. - - Optional maxheaderlen specifies the longest length for a non-continued - header. When a header line is longer (in characters, with tabs - expanded to 8 spaces) than maxheaderlen, the header will split as - defined in the Header class. Set maxheaderlen to zero to disable - header wrapping. The default is 78, as recommended (but not required) - by RFC 2822. - """ - self._fp = outfp - self._mangle_from_ = mangle_from_ - self._maxheaderlen = maxheaderlen - - def write(self, s): - # Just delegate to the file object - self._fp.write(s) - - def flatten(self, msg, unixfrom=False): - """Print the message object tree rooted at msg to the output file - specified when the Generator instance was created. - - unixfrom is a flag that forces the printing of a Unix From_ delimiter - before the first object in the message tree. If the original message - has no From_ delimiter, a `standard' one is crafted. By default, this - is False to inhibit the printing of any From_ delimiter. - - Note that for subobjects, no From_ line is printed. - """ - if unixfrom: - ufrom = msg.get_unixfrom() - if not ufrom: - ufrom = 'From nobody ' + time.ctime(time.time()) - print(ufrom, file=self._fp) - self._write(msg) - - def clone(self, fp): - """Clone this generator with the exact same options.""" - return self.__class__(fp, self._mangle_from_, self._maxheaderlen) - - # - # Protected interface - undocumented ;/ - # - - def _write(self, msg): - # We can't write the headers yet because of the following scenario: - # say a multipart message includes the boundary string somewhere in - # its body. We'd have to calculate the new boundary /before/ we write - # the headers so that we can write the correct Content-Type: - # parameter. - # - # The way we do this, so as to make the _handle_*() methods simpler, - # is to cache any subpart writes into a StringIO. The we write the - # headers and the StringIO contents. That way, subpart handlers can - # Do The Right Thing, and can still modify the Content-Type: header if - # necessary. - oldfp = self._fp - try: - self._fp = sfp = StringIO() - self._dispatch(msg) - finally: - self._fp = oldfp - # Write the headers. First we see if the message object wants to - # handle that itself. If not, we'll do it generically. - meth = getattr(msg, '_write_headers', None) - if meth is None: - self._write_headers(msg) - else: - meth(self) - self._fp.write(sfp.getvalue()) - - def _dispatch(self, msg): - # Get the Content-Type: for the message, then try to dispatch to - # self._handle__(). If there's no handler for the - # full MIME type, then dispatch to self._handle_(). If - # that's missing too, then dispatch to self._writeBody(). - main = msg.get_content_maintype() - sub = msg.get_content_subtype() - specific = UNDERSCORE.join((main, sub)).replace('-', '_') - meth = getattr(self, '_handle_' + specific, None) - if meth is None: - generic = main.replace('-', '_') - meth = getattr(self, '_handle_' + generic, None) - if meth is None: - meth = self._writeBody - meth(msg) - - # - # Default handlers - # - - def _write_headers(self, msg): - for h, v in list(msg.items()): - print('%s:' % h, end=' ', file=self._fp) - if self._maxheaderlen == 0: - # Explicit no-wrapping - print(v, file=self._fp) - elif isinstance(v, Header): - # Header instances know what to do - print(v.encode(), file=self._fp) - elif _is8bitstring(v): - # If we have raw 8bit data in a byte string, we have no idea - # what the encoding is. There is no safe way to split this - # string. If it's ascii-subset, then we could do a normal - # ascii split, but if it's multibyte then we could break the - # string. There's no way to know so the least harm seems to - # be to not split the string and risk it being too long. - print(v, file=self._fp) - else: - # Header's got lots of smarts, so use it. - print(Header( - v, maxlinelen=self._maxheaderlen, - header_name=h, continuation_ws='\t').encode(), file=self._fp) - # A blank line always separates headers from body - print(file=self._fp) - - # - # Handlers for writing types and subtypes - # - - def _handle_text(self, msg): - payload = msg.get_payload() - if payload is None: - return - if not isinstance(payload, str): - raise TypeError('string payload expected: %s' % type(payload)) - if self._mangle_from_: - payload = fcre.sub('>From ', payload) - self._fp.write(payload) - - # Default body handler - _writeBody = _handle_text - - def _handle_multipart(self, msg): - # The trick here is to write out each part separately, merge them all - # together, and then make sure that the boundary we've chosen isn't - # present in the payload. - msgtexts = [] - subparts = msg.get_payload() - if subparts is None: - subparts = [] - elif isinstance(subparts, str): - # e.g. a non-strict parse of a message with no starting boundary. - self._fp.write(subparts) - return - elif not isinstance(subparts, list): - # Scalar payload - subparts = [subparts] - for part in subparts: - s = StringIO() - g = self.clone(s) - g.flatten(part, unixfrom=False) - msgtexts.append(s.getvalue()) - # Now make sure the boundary we've selected doesn't appear in any of - # the message texts. - alltext = NL.join(msgtexts) - # BAW: What about boundaries that are wrapped in double-quotes? - boundary = msg.get_boundary(failobj=_make_boundary(alltext)) - # If we had to calculate a new boundary because the body text - # contained that string, set the new boundary. We don't do it - # unconditionally because, while set_boundary() preserves order, it - # doesn't preserve newlines/continuations in headers. This is no big - # deal in practice, but turns out to be inconvenient for the unittest - # suite. - if msg.get_boundary() != boundary: - msg.set_boundary(boundary) - # If there's a preamble, write it out, with a trailing CRLF - if msg.preamble is not None: - print(msg.preamble, file=self._fp) - # dash-boundary transport-padding CRLF - print('--' + boundary, file=self._fp) - # body-part - if msgtexts: - self._fp.write(msgtexts.pop(0)) - # *encapsulation - # --> delimiter transport-padding - # --> CRLF body-part - for body_part in msgtexts: - # delimiter transport-padding CRLF - print(NL + '--' + boundary, file=self._fp) - # body-part - self._fp.write(body_part) - # close-delimiter transport-padding - self._fp.write(NL + '--' + boundary + '--') - if msg.epilogue is not None: - print(file=self._fp) - self._fp.write(msg.epilogue) - - def _handle_message_delivery_status(self, msg): - # We can't just write the headers directly to self's file object - # because this will leave an extra newline between the last header - # block and the boundary. Sigh. - blocks = [] - for part in msg.get_payload(): - s = StringIO() - g = self.clone(s) - g.flatten(part, unixfrom=False) - text = s.getvalue() - lines = text.split('\n') - # Strip off the unnecessary trailing empty line - if lines and lines[-1] == '': - blocks.append(NL.join(lines[:-1])) - else: - blocks.append(text) - # Now join all the blocks with an empty line. This has the lovely - # effect of separating each block with an empty line, but not adding - # an extra one after the last one. - self._fp.write(NL.join(blocks)) - - def _handle_message(self, msg): - s = StringIO() - g = self.clone(s) - # The payload of a message/rfc822 part should be a multipart sequence - # of length 1. The zeroth element of the list should be the Message - # object for the subpart. Extract that object, stringify it, and - # write it out. - g.flatten(msg.get_payload(0), unixfrom=False) - self._fp.write(s.getvalue()) - - - -_FMT = '[Non-text (%(type)s) part of message omitted, filename %(filename)s]' - -class DecodedGenerator(Generator): - """Generator a text representation of a message. - - Like the Generator base class, except that non-text parts are substituted - with a format string representing the part. - """ - def __init__(self, outfp, mangle_from_=True, maxheaderlen=78, fmt=None): - """Like Generator.__init__() except that an additional optional - argument is allowed. - - Walks through all subparts of a message. If the subpart is of main - type `text', then it prints the decoded payload of the subpart. - - Otherwise, fmt is a format string that is used instead of the message - payload. fmt is expanded with the following keywords (in - %(keyword)s format): - - type : Full MIME type of the non-text part - maintype : Main MIME type of the non-text part - subtype : Sub-MIME type of the non-text part - filename : Filename of the non-text part - description: Description associated with the non-text part - encoding : Content transfer encoding of the non-text part - - The default value for fmt is None, meaning - - [Non-text (%(type)s) part of message omitted, filename %(filename)s] - """ - Generator.__init__(self, outfp, mangle_from_, maxheaderlen) - if fmt is None: - self._fmt = _FMT - else: - self._fmt = fmt - - def _dispatch(self, msg): - self.flush() - for part in msg.walk(): - maintype = part.get_content_maintype() - if maintype == 'text': - print(part.get_payload(decode=True), file=self) - elif maintype == 'multipart': - # Just skip this - pass - else: - print(self._fmt % { - 'type' : part.get_content_type(), - 'maintype' : part.get_content_maintype(), - 'subtype' : part.get_content_subtype(), - 'filename' : part.get_filename('[no filename]'), - 'description': part.get('Content-Description', - '[no description]'), - 'encoding' : part.get('Content-Transfer-Encoding', - '[no encoding]'), - }, file=self) - self.flush() - - -# Helper -_width = len(repr(sys.maxsize-1)) -_fmt = '%%0%dd' % _width - -def _make_boundary(text=None): - # Craft a random boundary. If text is given, ensure that the chosen - # boundary doesn't appear in the text. - token = random.randrange(sys.maxsize) - boundary = ('=' * 15) + (_fmt % token) + '==' - if text is None: - return boundary - b = boundary - counter = 0 - while True: - cre = re.compile('^--' + re.escape(b) + '(--)?$', re.MULTILINE) - if not cre.search(text): - break - b = boundary + '.' + str(counter) - counter += 1 - return b diff --git a/src/libs/python/bongo/external/email/header.py b/src/libs/python/bongo/external/email/header.py deleted file mode 100644 index ce34a31..0000000 --- a/src/libs/python/bongo/external/email/header.py +++ /dev/null @@ -1,502 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Author: Ben Gertzfield, Barry Warsaw -# Contact: email-sig@python.org - -"""Header encoding and decoding functionality.""" - -__all__ = [ - 'Header', - 'decode_header', - 'make_header', - ] - -import re -import binascii - -import bongo.external.email.quoprimime -import bongo.external.email.base64mime - -from bongo.external.email.errors import HeaderParseError -from bongo.external.email.charset import Charset - -NL = '\n' -SPACE = ' ' -USPACE = ' ' -SPACE8 = ' ' * 8 -UEMPTYSTRING = '' - -MAXLINELEN = 76 - -USASCII = Charset('us-ascii') -UTF8 = Charset('utf-8') - -# Match encoded-word strings in the form =?charset?q?Hello_World?= -ecre = re.compile(r''' - =\? # literal =? - (?P[^?]*?) # non-greedy up to the next ? is the charset - \? # literal ? - (?P[qb]) # either a "q" or a "b", case insensitive - \? # literal ? - (?P.*?) # non-greedy up to the next ?= is the encoded string - \?= # literal ?= - ''', re.VERBOSE | re.IGNORECASE) - -# Field name regexp, including trailing colon, but not separating whitespace, -# according to RFC 2822. Character range is from tilde to exclamation mark. -# For use with .match() -fcre = re.compile(r'[\041-\176]+:$') - - - -# Helpers -_max_append = bongo.external.email.quoprimime._max_append - - - -def decode_header(header): - """Decode a message header value without converting charset. - - Returns a list of (decoded_string, charset) pairs containing each of the - decoded parts of the header. Charset is None for non-encoded parts of the - header, otherwise a lower-case string containing the name of the character - set specified in the encoded string. - - An email.Errors.HeaderParseError may be raised when certain decoding error - occurs (e.g. a base64 decoding exception). - """ - # If no encoding, just return the header - header = str(header) - if not ecre.search(header): - return [(header, None)] - decoded = [] - dec = '' - for line in header.splitlines(): - # This line might not have an encoding in it - if not ecre.search(line): - decoded.append((line, None)) - continue - parts = ecre.split(line) - while parts: - unenc = parts.pop(0).strip() - if unenc: - # Should we continue a long line? - if decoded and decoded[-1][1] is None: - decoded[-1] = (decoded[-1][0] + SPACE + unenc, None) - else: - decoded.append((unenc, None)) - if parts: - charset, encoding = [s.lower() for s in parts[0:2]] - encoded = parts[2] - dec = None - if encoding == 'q': - dec = bongo.external.email.quoprimime.header_decode(encoded) - elif encoding == 'b': - try: - dec = bongo.external.email.base64mime.decode(encoded) - except binascii.Error: - # Turn this into a higher level exception. BAW: Right - # now we throw the lower level exception away but - # when/if we get exception chaining, we'll preserve it. - raise HeaderParseError - if dec is None: - dec = encoded - - if decoded and decoded[-1][1] == charset: - decoded[-1] = (decoded[-1][0] + dec, decoded[-1][1]) - else: - decoded.append((dec, charset)) - del parts[0:3] - return decoded - - - -def make_header(decoded_seq, maxlinelen=None, header_name=None, - continuation_ws=' '): - """Create a Header from a sequence of pairs as returned by decode_header() - - decode_header() takes a header value string and returns a sequence of - pairs of the format (decoded_string, charset) where charset is the string - name of the character set. - - This function takes one of those sequence of pairs and returns a Header - instance. Optional maxlinelen, header_name, and continuation_ws are as in - the Header constructor. - """ - h = Header(maxlinelen=maxlinelen, header_name=header_name, - continuation_ws=continuation_ws) - for s, charset in decoded_seq: - # None means us-ascii but we can simply pass it on to h.append() - if charset is not None and not isinstance(charset, Charset): - charset = Charset(charset) - h.append(s, charset) - return h - - - -class Header: - def __init__(self, s=None, charset=None, - maxlinelen=None, header_name=None, - continuation_ws=' ', errors='strict'): - """Create a MIME-compliant header that can contain many character sets. - - Optional s is the initial header value. If None, the initial header - value is not set. You can later append to the header with .append() - method calls. s may be a byte string or a Unicode string, but see the - .append() documentation for semantics. - - Optional charset serves two purposes: it has the same meaning as the - charset argument to the .append() method. It also sets the default - character set for all subsequent .append() calls that omit the charset - argument. If charset is not provided in the constructor, the us-ascii - charset is used both as s's initial charset and as the default for - subsequent .append() calls. - - The maximum line length can be specified explicit via maxlinelen. For - splitting the first line to a shorter value (to account for the field - header which isn't included in s, e.g. `Subject') pass in the name of - the field in header_name. The default maxlinelen is 76. - - continuation_ws must be RFC 2822 compliant folding whitespace (usually - either a space or a hard tab) which will be prepended to continuation - lines. - - errors is passed through to the .append() call. - """ - if charset is None: - charset = USASCII - if not isinstance(charset, Charset): - charset = Charset(charset) - self._charset = charset - self._continuation_ws = continuation_ws - cws_expanded_len = len(continuation_ws.replace('\t', SPACE8)) - # BAW: I believe `chunks' and `maxlinelen' should be non-public. - self._chunks = [] - if s is not None: - self.append(s, charset, errors) - if maxlinelen is None: - maxlinelen = MAXLINELEN - if header_name is None: - # We don't know anything about the field header so the first line - # is the same length as subsequent lines. - self._firstlinelen = maxlinelen - else: - # The first line should be shorter to take into account the field - # header. Also subtract off 2 extra for the colon and space. - self._firstlinelen = maxlinelen - len(header_name) - 2 - # Second and subsequent lines should subtract off the length in - # columns of the continuation whitespace prefix. - self._maxlinelen = maxlinelen - cws_expanded_len - - def __str__(self): - """A synonym for self.encode().""" - return self.encode() - - def __unicode__(self): - """Helper for the built-in unicode function.""" - uchunks = [] - lastcs = None - for s, charset in self._chunks: - # We must preserve spaces between encoded and non-encoded word - # boundaries, which means for us we need to add a space when we go - # from a charset to None/us-ascii, or from None/us-ascii to a - # charset. Only do this for the second and subsequent chunks. - nextcs = charset - if uchunks: - if lastcs not in (None, 'us-ascii'): - if nextcs in (None, 'us-ascii'): - uchunks.append(USPACE) - nextcs = None - elif nextcs not in (None, 'us-ascii'): - uchunks.append(USPACE) - lastcs = nextcs - uchunks.append(str(s, str(charset))) - return UEMPTYSTRING.join(uchunks) - - # Rich comparison operators for equality only. BAW: does it make sense to - # have or explicitly disable <, <=, >, >= operators? - def __eq__(self, other): - # other may be a Header or a string. Both are fine so coerce - # ourselves to a string, swap the args and do another comparison. - return other == self.encode() - - def __ne__(self, other): - return not self == other - - def append(self, s, charset=None, errors='strict'): - """Append a string to the MIME header. - - Optional charset, if given, should be a Charset instance or the name - of a character set (which will be converted to a Charset instance). A - value of None (the default) means that the charset given in the - constructor is used. - - s may be a byte string or a Unicode string. If it is a byte string - (i.e. isinstance(s, str) is true), then charset is the encoding of - that byte string, and a UnicodeError will be raised if the string - cannot be decoded with that charset. If s is a Unicode string, then - charset is a hint specifying the character set of the characters in - the string. In this case, when producing an RFC 2822 compliant header - using RFC 2047 rules, the Unicode string will be encoded using the - following charsets in order: us-ascii, the charset hint, utf-8. The - first character set not to provoke a UnicodeError is used. - - Optional `errors' is passed as the third argument to any unicode() or - ustr.encode() call. - """ - if charset is None: - charset = self._charset - elif not isinstance(charset, Charset): - charset = Charset(charset) - # If the charset is our faux 8bit charset, leave the string unchanged - if charset != '8bit': - # We need to test that the string can be converted to unicode and - # back to a byte string, given the input and output codecs of the - # charset. - if isinstance(s, str): - # Possibly raise UnicodeError if the byte string can't be - # converted to a unicode with the input codec of the charset. - incodec = charset.input_codec or 'us-ascii' - ustr = str(s, incodec, errors) - # Now make sure that the unicode could be converted back to a - # byte string with the output codec, which may be different - # than the iput coded. Still, use the original byte string. - outcodec = charset.output_codec or 'us-ascii' - ustr.encode(outcodec, errors) - elif isinstance(s, str): - # Now we have to be sure the unicode string can be converted - # to a byte string with a reasonable output codec. We want to - # use the byte string in the chunk. - for charset in USASCII, charset, UTF8: - try: - outcodec = charset.output_codec or 'us-ascii' - s = s.encode(outcodec, errors) - break - except UnicodeError: - pass - else: - assert False, 'utf-8 conversion failed' - self._chunks.append((s, charset)) - - def _split(self, s, charset, maxlinelen, splitchars): - # Split up a header safely for use with encode_chunks. - splittable = charset.to_splittable(s) - encoded = charset.from_splittable(splittable, True) - elen = charset.encoded_header_len(encoded) - # If the line's encoded length first, just return it - if elen <= maxlinelen: - return [(encoded, charset)] - # If we have undetermined raw 8bit characters sitting in a byte - # string, we really don't know what the right thing to do is. We - # can't really split it because it might be multibyte data which we - # could break if we split it between pairs. The least harm seems to - # be to not split the header at all, but that means they could go out - # longer than maxlinelen. - if charset == '8bit': - return [(s, charset)] - # BAW: I'm not sure what the right test here is. What we're trying to - # do is be faithful to RFC 2822's recommendation that ($2.2.3): - # - # "Note: Though structured field bodies are defined in such a way that - # folding can take place between many of the lexical tokens (and even - # within some of the lexical tokens), folding SHOULD be limited to - # placing the CRLF at higher-level syntactic breaks." - # - # For now, I can only imagine doing this when the charset is us-ascii, - # although it's possible that other charsets may also benefit from the - # higher-level syntactic breaks. - elif charset == 'us-ascii': - return self._split_ascii(s, charset, maxlinelen, splitchars) - # BAW: should we use encoded? - elif elen == len(s): - # We can split on _maxlinelen boundaries because we know that the - # encoding won't change the size of the string - splitpnt = maxlinelen - first = charset.from_splittable(splittable[:splitpnt], False) - last = charset.from_splittable(splittable[splitpnt:], False) - else: - # Binary search for split point - first, last = _binsplit(splittable, charset, maxlinelen) - # first is of the proper length so just wrap it in the appropriate - # chrome. last must be recursively split. - fsplittable = charset.to_splittable(first) - fencoded = charset.from_splittable(fsplittable, True) - chunk = [(fencoded, charset)] - return chunk + self._split(last, charset, self._maxlinelen, splitchars) - - def _split_ascii(self, s, charset, firstlen, splitchars): - chunks = _split_ascii(s, firstlen, self._maxlinelen, - self._continuation_ws, splitchars) - return list(zip(chunks, [charset]*len(chunks))) - - def _encode_chunks(self, newchunks, maxlinelen): - # MIME-encode a header with many different charsets and/or encodings. - # - # Given a list of pairs (string, charset), return a MIME-encoded - # string suitable for use in a header field. Each pair may have - # different charsets and/or encodings, and the resulting header will - # accurately reflect each setting. - # - # Each encoding can be email.Utils.QP (quoted-printable, for - # ASCII-like character sets like iso-8859-1), email.Utils.BASE64 - # (Base64, for non-ASCII like character sets like KOI8-R and - # iso-2022-jp), or None (no encoding). - # - # Each pair will be represented on a separate line; the resulting - # string will be in the format: - # - # =?charset1?q?Mar=EDa_Gonz=E1lez_Alonso?=\n - # =?charset2?b?SvxyZ2VuIEL2aW5n?=" - chunks = [] - for header, charset in newchunks: - if not header: - continue - if charset is None or charset.header_encoding is None: - s = header - else: - s = charset.header_encode(header) - # Don't add more folding whitespace than necessary - if chunks and chunks[-1].endswith(' '): - extra = '' - else: - extra = ' ' - _max_append(chunks, s, maxlinelen, extra) - joiner = NL + self._continuation_ws - return joiner.join(chunks) - - def encode(self, splitchars=';, '): - """Encode a message header into an RFC-compliant format. - - There are many issues involved in converting a given string for use in - an email header. Only certain character sets are readable in most - email clients, and as header strings can only contain a subset of - 7-bit ASCII, care must be taken to properly convert and encode (with - Base64 or quoted-printable) header strings. In addition, there is a - 75-character length limit on any given encoded header field, so - line-wrapping must be performed, even with double-byte character sets. - - This method will do its best to convert the string to the correct - character set used in email, and encode and line wrap it safely with - the appropriate scheme for that character set. - - If the given charset is not known or an error occurs during - conversion, this function will return the header untouched. - - Optional splitchars is a string containing characters to split long - ASCII lines on, in rough support of RFC 2822's `highest level - syntactic breaks'. This doesn't affect RFC 2047 encoded lines. - """ - newchunks = [] - maxlinelen = self._firstlinelen - lastlen = 0 - for s, charset in self._chunks: - # The first bit of the next chunk should be just long enough to - # fill the next line. Don't forget the space separating the - # encoded words. - targetlen = maxlinelen - lastlen - 1 - if targetlen < charset.encoded_header_len(''): - # Stick it on the next line - targetlen = maxlinelen - newchunks += self._split(s, charset, targetlen, splitchars) - lastchunk, lastcharset = newchunks[-1] - lastlen = lastcharset.encoded_header_len(lastchunk) - return self._encode_chunks(newchunks, maxlinelen) - - - -def _split_ascii(s, firstlen, restlen, continuation_ws, splitchars): - lines = [] - maxlen = firstlen - for line in s.splitlines(): - # Ignore any leading whitespace (i.e. continuation whitespace) already - # on the line, since we'll be adding our own. - line = line.lstrip() - if len(line) < maxlen: - lines.append(line) - maxlen = restlen - continue - # Attempt to split the line at the highest-level syntactic break - # possible. Note that we don't have a lot of smarts about field - # syntax; we just try to break on semi-colons, then commas, then - # whitespace. - for ch in splitchars: - if ch in line: - break - else: - # There's nothing useful to split the line on, not even spaces, so - # just append this line unchanged - lines.append(line) - maxlen = restlen - continue - # Now split the line on the character plus trailing whitespace - cre = re.compile(r'%s\s*' % ch) - if ch in ';,': - eol = ch - else: - eol = '' - joiner = eol + ' ' - joinlen = len(joiner) - wslen = len(continuation_ws.replace('\t', SPACE8)) - this = [] - linelen = 0 - for part in cre.split(line): - curlen = linelen + max(0, len(this)-1) * joinlen - partlen = len(part) - onfirstline = not lines - # We don't want to split after the field name, if we're on the - # first line and the field name is present in the header string. - if ch == ' ' and onfirstline and \ - len(this) == 1 and fcre.match(this[0]): - this.append(part) - linelen += partlen - elif curlen + partlen > maxlen: - if this: - lines.append(joiner.join(this) + eol) - # If this part is longer than maxlen and we aren't already - # splitting on whitespace, try to recursively split this line - # on whitespace. - if partlen > maxlen and ch != ' ': - subl = _split_ascii(part, maxlen, restlen, - continuation_ws, ' ') - lines.extend(subl[:-1]) - this = [subl[-1]] - else: - this = [part] - linelen = wslen + len(this[-1]) - maxlen = restlen - else: - this.append(part) - linelen += partlen - # Put any left over parts on a line by themselves - if this: - lines.append(joiner.join(this)) - return lines - - - -def _binsplit(splittable, charset, maxlinelen): - i = 0 - j = len(splittable) - while i < j: - # Invariants: - # 1. splittable[:k] fits for all k <= i (note that we *assume*, - # at the start, that splittable[:0] fits). - # 2. splittable[:k] does not fit for any k > j (at the start, - # this means we shouldn't look at any k > len(splittable)). - # 3. We don't know about splittable[:k] for k in i+1..j. - # 4. We want to set i to the largest k that fits, with i <= k <= j. - # - m = (i+j+1) >> 1 # ceiling((i+j)/2); i < m <= j - chunk = charset.from_splittable(splittable[:m], True) - chunklen = charset.encoded_header_len(chunk) - if chunklen <= maxlinelen: - # m is acceptable, so is a new lower bound. - i = m - else: - # m is not acceptable, so final i must be < m. - j = m - 1 - # i == j. Invariant #1 implies that splittable[:i] fits, and - # invariant #2 implies that splittable[:i+1] does not fit, so i - # is what we're looking for. - first = charset.from_splittable(splittable[:i], False) - last = charset.from_splittable(splittable[i:], False) - return first, last diff --git a/src/libs/python/bongo/external/email/iterators.py b/src/libs/python/bongo/external/email/iterators.py deleted file mode 100644 index eb4cd67..0000000 --- a/src/libs/python/bongo/external/email/iterators.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Various types of useful iterators and generators.""" - -__all__ = [ - 'body_line_iterator', - 'typed_subpart_iterator', - 'walk', - # Do not include _structure() since it's part of the debugging API. - ] - -import sys -from io import StringIO - - - -# This function will become a method of the Message class -def walk(self): - """Walk over the message tree, yielding each subpart. - - The walk is performed in depth-first order. This method is a - generator. - """ - yield self - if self.is_multipart(): - for subpart in self.get_payload(): - for subsubpart in subpart.walk(): - yield subsubpart - - - -# These two functions are imported into the Iterators.py interface module. -def body_line_iterator(msg, decode=False): - """Iterate over the parts, returning string payloads line-by-line. - - Optional decode (default False) is passed through to .get_payload(). - """ - for subpart in msg.walk(): - payload = subpart.get_payload(decode=decode) - if isinstance(payload, str): - for line in StringIO(payload): - yield line - - -def typed_subpart_iterator(msg, maintype='text', subtype=None): - """Iterate over the subparts with a given MIME type. - - Use `maintype' as the main MIME type to match against; this defaults to - "text". Optional `subtype' is the MIME subtype to match against; if - omitted, only the main type is matched. - """ - for subpart in msg.walk(): - if subpart.get_content_maintype() == maintype: - if subtype is None or subpart.get_content_subtype() == subtype: - yield subpart - - - -def _structure(msg, fp=None, level=0, include_default=False): - """A handy debugging aid""" - if fp is None: - fp = sys.stdout - tab = ' ' * (level * 4) - print(tab + msg.get_content_type(), end=' ', file=fp) - if include_default: - print('[%s]' % msg.get_default_type(), file=fp) - else: - print(file=fp) - if msg.is_multipart(): - for subpart in msg.get_payload(): - _structure(subpart, fp, level+1, include_default) diff --git a/src/libs/python/bongo/external/email/message.py b/src/libs/python/bongo/external/email/message.py deleted file mode 100644 index 6bc4a5a..0000000 --- a/src/libs/python/bongo/external/email/message.py +++ /dev/null @@ -1,856 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Basic message object for the email package object model.""" - -__all__ = ['Message'] - -import re -from io import StringIO - -# Intrapackage imports -import bongo.external.email.charset -from bongo.external.email import utils -from bongo.external.email import errors -from bongo.external.email import payloads - -SEMISPACE = '; ' - -# Regular expression used to split header parameters. BAW: this may be too -# simple. It isn't strictly RFC 2045 (section 5.1) compliant, but it catches -# most headers found in the wild. We may eventually need a full fledged -# parser eventually. -paramre = re.compile(r'\s*;\s*') -# Regular expression that matches `special' characters in parameters, the -# existance of which force quoting of the parameter value. -tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]') - - - -# Helper functions -def _formatparam(param, value=None, quote=True): - """Convenience function to format and return a key=value pair. - - This will quote the value if needed or if quote is true. - """ - if value is not None and len(value) > 0: - # A tuple is used for RFC 2231 encoded parameter values where items - # are (charset, language, value). charset is a string, not a Charset - # instance. - if isinstance(value, tuple): - # Encode as per RFC 2231 - param += '*' - value = utils.encode_rfc2231(value[2], value[0], value[1]) - # BAW: Please check this. I think that if quote is set it should - # force quoting even if not necessary. - if quote or tspecials.search(value): - return '%s="%s"' % (param, utils.quote(value)) - else: - return '%s=%s' % (param, value) - else: - return param - -def _parseparam(s): - plist = [] - while s[:1] == ';': - s = s[1:] - end = s.find(';') - while end > 0 and s.count('"', 0, end) % 2: - end = s.find(';', end + 1) - if end < 0: - end = len(s) - f = s[:end] - if '=' in f: - i = f.index('=') - f = f[:i].strip().lower() + '=' + f[i+1:].strip() - plist.append(f.strip()) - s = s[end:] - return plist - - -def _unquotevalue(value): - # This is different than utils.collapse_rfc2231_value() because it doesn't - # try to convert the value to a unicode. Message.get_param() and - # Message.get_params() are both currently defined to return the tuple in - # the face of RFC 2231 parameters. - if isinstance(value, tuple): - return value[0], value[1], utils.unquote(value[2]) - else: - return utils.unquote(value) - - - -class Message: - """Basic message object. - - A message object is defined as something that has a bunch of RFC 2822 - headers and a payload. It may optionally have an envelope header - (a.k.a. Unix-From or From_ header). If the message is a container (i.e. a - multipart or a message/rfc822), then the payload is a list of Message - objects, otherwise it is a string. - - Message objects implement part of the `mapping' interface, which assumes - there is exactly one occurrance of the header per message. Some headers - do in fact appear multiple times (e.g. Received) and for those headers, - you must use the explicit API to set or get all the headers. Not all of - the mapping methods are implemented. - """ - def __init__(self): - self._headers = [] - self._unixfrom = None - self._payload = None - self._charset = None - # Defaults for multipart messages - self.preamble = self.epilogue = None - self.defects = [] - # Default content type - self._default_type = 'text/plain' - - def __str__(self): - """Return the entire formatted message as a string. - This includes the headers, body, and envelope header. - """ - return self.as_string(unixfrom=True) - - def as_string(self, unixfrom=False): - """Return the entire formatted message as a string. - Optional `unixfrom' when True, means include the Unix From_ envelope - header. - - This is a convenience method and may not generate the message exactly - as you intend because by default it mangles lines that begin with - "From ". For more flexibility, use the flatten() method of a - Generator instance. - """ - from bongo.external.email.Generator import Generator - fp = StringIO() - g = Generator(fp) - g.flatten(self, unixfrom=unixfrom) - return fp.getvalue() - - def is_multipart(self): - """Return True if the message consists of multiple parts.""" - return isinstance(self._payload, list) - - # - # Unix From_ line - # - def set_unixfrom(self, unixfrom): - self._unixfrom = unixfrom - - def get_unixfrom(self): - return self._unixfrom - - # - # Payload manipulation. - # - def attach(self, payload): - """Add the given payload to the current payload. - - The current payload will always be a list of objects after this method - is called. If you want to set the payload to a scalar object, use - set_payload() instead. - """ - if self._payload is None: - self._payload = [payload] - else: - self._payload.append(payload) - - def get_payload(self, i=None, decode=False): - """Return a reference to the payload. - - The payload will either be a list object or a string. If you mutate - the list object, you modify the message's payload in place. Optional - i returns that index into the payload. - - Optional decode is a flag indicating whether the payload should be - decoded or not, according to the Content-Transfer-Encoding header - (default is False). - - When True and the message is not a multipart, the payload will be - decoded if this header's value is `quoted-printable' or `base64'. If - some other encoding is used, or the header is missing, or if the - payload has bogus data (i.e. bogus base64 or uuencoded data), the - payload is returned as-is. - - If the message is a multipart and the decode flag is True, then None - is returned. - """ - if i is None : - if isinstance(self._payload, list) : - if decode : - return None - else : - return self._payload - elif isinstance(self._payload, str): - if self._charset and not decode: - return self._charset.body_encode(self._payload) - else : - return self._payload - elif isinstance(self._payload, payloads.Payload) : - ret = "" - for item in self.iter_payload(decode) : - ret += item - return ret - elif not isinstance(self._payload, list): - raise TypeError('Expected list, got %s' % type(self._payload)) - else: - # Multipart container - if decode: - return None - else: - return self._payload[i] - - def get_payload_obj(self) : - if self._payload == None : - return None - #XXX: document - if isinstance(self._payload, str): - if self._charset : - charset = self._charset.input_charset - else : - charset = "us-ascii" - - payload = payloads.MemoryPayload(charset, self._payload) - elif isinstance(self._payload, payloads.Payload) or isinstance(self._payload, list) : - payload = self._payload - else : - print("unsupported payload type: %s" % (str(self._payload))) - raise TypeError('unsupported payload type for iterating') - - return payload - - def set_payload(self, payload, charset=None): - """Set the payload to the given value. - - Optional charset sets the message's default character set. See - set_charset() for details. - """ - if isinstance(payload, payloads.Payload): - if payload.encoding: - if 'Content-Transfer-Encoding' in self : - self.replace_header('Content-Transfer-Encoding', payload.encoding) - else : - self['Content-Transfer-Encoding'] = payload.encoding - - self._payload = payload - if charset is not None: - self.set_charset(charset) - - def iter_payload(self, decode=False): - payload = self.get_payload_obj() - if payload is None : - return - if not isinstance(payload, payloads.Payload) : - raise TypeError('unsupported payload type for iterating') - - if self._charset : - charset = self._charset - else : - charset = self._read_charset() - - encoding = self['Content-Transfer-Encoding'] - - encode = not decode and charset and encoding != payload.encoding - subdecode = decode or not charset or (encoding != payload.encoding) - - leftover = None - for buf in payload.iter(subdecode): - if encode : - if isinstance(buf, str) : - buf = buf.encode(charset.get_output_charset()) - - if charset.body_encoding == bongo.external.email.charset.BASE64 : - # Encode 57 bytes at a time (lines 76 long) - if leftover : - buf = leftover + buf - leftover = None - - amountLeftover = len(buf) % 57 - leftover = buf[-amountLeftover:] - buf = buf[:-amountLeftover] - - if buf != '' : - buf = charset.body_encode(buf) - else : - buf = None - - if buf : - yield buf - - if leftover: - leftover = charset.body_encode(leftover) - yield leftover - - def set_charset(self, charset): - """Set the charset of the payload to a given character set. - - charset can be a Charset instance, a string naming a character set, or - None. If it is a string it will be converted to a Charset instance. - If charset is None, the charset parameter will be removed from the - Content-Type field. Anything else will generate a TypeError. - - The message will be assumed to be of type text/* encoded with - charset.input_charset. It will be converted to charset.output_charset - and encoded properly, if needed, when generating the plain text - representation of the message. MIME headers (MIME-Version, - Content-Type, Content-Transfer-Encoding) will be added as needed. - - """ - if charset is None: - self.del_param('charset') - self._charset = None - return - if isinstance(charset, str): - charset = bongo.external.email.charset.Charset(charset) - if not isinstance(charset, bongo.external.email.charset.Charset): - raise TypeError(charset) - # BAW: should we accept strings that can serve as arguments to the - # Charset constructor? - - self._charset = charset - if 'MIME-Version' not in self: - self.add_header('MIME-Version', '1.0') - if 'Content-Type' not in self: - self.add_header('Content-Type', 'text/plain', - charset=charset.get_output_charset()) - else: - self.set_param('charset', charset.get_output_charset()) - - if not self['Content-Transfer-Encoding'] : - cte = charset.get_body_encoding() - try : - cte(self) - except TypeError: - self.add_header('Content-Transfer-Encoding', cte) - - def _read_charset(self) : - charset = self.get_param('charset') or 'us-ascii' - encoding = self['Content-Transfer-Encoding'] - return bongo.external.email.charset.Charset(charset, encoding) - - def get_charset(self): - """Return the Charset instance associated with the message's payload. - """ - return self._charset - - # - # MAPPING INTERFACE (partial) - # - def __len__(self): - """Return the total number of headers, including duplicates.""" - return len(self._headers) - - def __getitem__(self, name): - """Get a header value. - - Return None if the header is missing instead of raising an exception. - - Note that if the header appeared multiple times, exactly which - occurrance gets returned is undefined. Use get_all() to get all - the values matching a header field name. - """ - return self.get(name) - - def __setitem__(self, name, val): - """Set the value of a header. - - Note: this does not overwrite an existing header with the same field - name. Use __delitem__() first to delete any existing headers. - """ - self._headers.append((name, val)) - - def __delitem__(self, name): - """Delete all occurrences of a header, if present. - - Does not raise an exception if the header is missing. - """ - name = name.lower() - newheaders = [] - for k, v in self._headers: - if k.lower() != name: - newheaders.append((k, v)) - self._headers = newheaders - - def __contains__(self, name): - return name.lower() in [k.lower() for k, v in self._headers] - - def has_key(self, name): - """Return true if the message contains the header.""" - missing = object() - return self.get(name, missing) is not missing - - def keys(self): - """Return a list of all the message's header field names. - - These will be sorted in the order they appeared in the original - message, or were added to the message, and may contain duplicates. - Any fields deleted and re-inserted are always appended to the header - list. - """ - return [k for k, v in self._headers] - - def values(self): - """Return a list of all the message's header values. - - These will be sorted in the order they appeared in the original - message, or were added to the message, and may contain duplicates. - Any fields deleted and re-inserted are always appended to the header - list. - """ - return [v for k, v in self._headers] - - def items(self): - """Get all the message's header fields and values. - - These will be sorted in the order they appeared in the original - message, or were added to the message, and may contain duplicates. - Any fields deleted and re-inserted are always appended to the header - list. - """ - return self._headers[:] - - def get(self, name, failobj=None): - """Get a header value. - - Like __getitem__() but return failobj instead of None when the field - is missing. - """ - name = name.lower() - for k, v in self._headers: - if k.lower() == name: - return v - return failobj - - # - # Additional useful stuff - # - - def get_all(self, name, failobj=None): - """Return a list of all the values for the named field. - - These will be sorted in the order they appeared in the original - message, and may contain duplicates. Any fields deleted and - re-inserted are always appended to the header list. - - If no such fields exist, failobj is returned (defaults to None). - """ - values = [] - name = name.lower() - for k, v in self._headers: - if k.lower() == name: - values.append(v) - if not values: - return failobj - return values - - def add_header(self, _name, _value, **_params): - """Extended header setting. - - name is the header field to add. keyword arguments can be used to set - additional parameters for the header field, with underscores converted - to dashes. Normally the parameter will be added as key="value" unless - value is None, in which case only the key will be added. - - Example: - - msg.add_header('content-disposition', 'attachment', filename='bud.gif') - """ - parts = [] - for k, v in list(_params.items()): - if v is None: - parts.append(k.replace('_', '-')) - else: - parts.append(_formatparam(k.replace('_', '-'), v)) - if _value is not None: - parts.insert(0, _value) - self._headers.append((_name, SEMISPACE.join(parts))) - - def replace_header(self, _name, _value): - """Replace a header. - - Replace the first matching header found in the message, retaining - header order and case. If no matching header was found, a KeyError is - raised. - """ - _name = _name.lower() - for i, (k, v) in zip(list(range(len(self._headers))), self._headers): - if k.lower() == _name: - self._headers[i] = (k, _value) - break - else: - raise KeyError(_name) - - # - # Use these three methods instead of the three above. - # - - def get_content_type(self): - """Return the message's content type. - - The returned string is coerced to lower case of the form - `maintype/subtype'. If there was no Content-Type header in the - message, the default type as given by get_default_type() will be - returned. Since according to RFC 2045, messages always have a default - type this will always return a value. - - RFC 2045 defines a message's default type to be text/plain unless it - appears inside a multipart/digest container, in which case it would be - message/rfc822. - """ - missing = object() - value = self.get('content-type', missing) - if value is missing: - # This should have no parameters - return self.get_default_type() - ctype = paramre.split(value)[0].lower().strip() - # RFC 2045, section 5.2 says if its invalid, use text/plain - if ctype.count('/') != 1: - return 'text/plain' - return ctype - - def get_content_maintype(self): - """Return the message's main content type. - - This is the `maintype' part of the string returned by - get_content_type(). - """ - ctype = self.get_content_type() - return ctype.split('/')[0] - - def get_content_subtype(self): - """Returns the message's sub-content type. - - This is the `subtype' part of the string returned by - get_content_type(). - """ - ctype = self.get_content_type() - return ctype.split('/')[1] - - def get_default_type(self): - """Return the `default' content type. - - Most messages have a default content type of text/plain, except for - messages that are subparts of multipart/digest containers. Such - subparts have a default content type of message/rfc822. - """ - return self._default_type - - def set_default_type(self, ctype): - """Set the `default' content type. - - ctype should be either "text/plain" or "message/rfc822", although this - is not enforced. The default content type is not stored in the - Content-Type header. - """ - self._default_type = ctype - - def _get_params_preserve(self, failobj, header): - # Like get_params() but preserves the quoting of values. BAW: - # should this be part of the public interface? - missing = object() - value = self.get(header, missing) - if value is missing: - return failobj - params = [] - for p in _parseparam(';' + value): - try: - name, val = p.split('=', 1) - name = name.strip() - val = val.strip() - except ValueError: - # Must have been a bare attribute - name = p.strip() - val = '' - params.append((name, val)) - params = utils.decode_params(params) - return params - - def get_params(self, failobj=None, header='content-type', unquote=True): - """Return the message's Content-Type parameters, as a list. - - The elements of the returned list are 2-tuples of key/value pairs, as - split on the `=' sign. The left hand side of the `=' is the key, - while the right hand side is the value. If there is no `=' sign in - the parameter the value is the empty string. The value is as - described in the get_param() method. - - Optional failobj is the object to return if there is no Content-Type - header. Optional header is the header to search instead of - Content-Type. If unquote is True, the value is unquoted. - """ - missing = object() - params = self._get_params_preserve(missing, header) - if params is missing: - return failobj - if unquote: - return [(k, _unquotevalue(v)) for k, v in params] - else: - return params - - def get_param(self, param, failobj=None, header='content-type', - unquote=True): - """Return the parameter value if found in the Content-Type header. - - Optional failobj is the object to return if there is no Content-Type - header, or the Content-Type header has no such parameter. Optional - header is the header to search instead of Content-Type. - - Parameter keys are always compared case insensitively. The return - value can either be a string, or a 3-tuple if the parameter was RFC - 2231 encoded. When it's a 3-tuple, the elements of the value are of - the form (CHARSET, LANGUAGE, VALUE). Note that both CHARSET and - LANGUAGE can be None, in which case you should consider VALUE to be - encoded in the us-ascii charset. You can usually ignore LANGUAGE. - - Your application should be prepared to deal with 3-tuple return - values, and can convert the parameter to a Unicode string like so: - - param = msg.get_param('foo') - if isinstance(param, tuple): - param = unicode(param[2], param[0] or 'us-ascii') - - In any case, the parameter value (either the returned string, or the - VALUE item in the 3-tuple) is always unquoted, unless unquote is set - to False. - """ - if header not in self: - return failobj - for k, v in self._get_params_preserve(failobj, header): - if k.lower() == param.lower(): - if unquote: - return _unquotevalue(v) - else: - return v - return failobj - - def set_param(self, param, value, header='Content-Type', requote=True, - charset=None, language=''): - """Set a parameter in the Content-Type header. - - If the parameter already exists in the header, its value will be - replaced with the new value. - - If header is Content-Type and has not yet been defined for this - message, it will be set to "text/plain" and the new parameter and - value will be appended as per RFC 2045. - - An alternate header can specified in the header argument, and all - parameters will be quoted as necessary unless requote is False. - - If charset is specified, the parameter will be encoded according to RFC - 2231. Optional language specifies the RFC 2231 language, defaulting - to the empty string. Both charset and language should be strings. - """ - if not isinstance(value, tuple) and charset: - value = (charset, language, value) - - if header not in self and header.lower() == 'content-type': - ctype = 'text/plain' - else: - ctype = self.get(header) - if not self.get_param(param, header=header): - if not ctype: - ctype = _formatparam(param, value, requote) - else: - ctype = SEMISPACE.join( - [ctype, _formatparam(param, value, requote)]) - else: - ctype = '' - for old_param, old_value in self.get_params(header=header, - unquote=requote): - append_param = '' - if old_param.lower() == param.lower(): - append_param = _formatparam(param, value, requote) - else: - append_param = _formatparam(old_param, old_value, requote) - if not ctype: - ctype = append_param - else: - ctype = SEMISPACE.join([ctype, append_param]) - if ctype != self.get(header): - del self[header] - self[header] = ctype - - def del_param(self, param, header='content-type', requote=True): - """Remove the given parameter completely from the Content-Type header. - - The header will be re-written in place without the parameter or its - value. All values will be quoted as necessary unless requote is - False. Optional header specifies an alternative to the Content-Type - header. - """ - if header not in self: - return - new_ctype = '' - for p, v in self.get_params(header=header, unquote=requote): - if p.lower() != param.lower(): - if not new_ctype: - new_ctype = _formatparam(p, v, requote) - else: - new_ctype = SEMISPACE.join([new_ctype, - _formatparam(p, v, requote)]) - if new_ctype != self.get(header): - del self[header] - self[header] = new_ctype - - def set_type(self, type, header='Content-Type', requote=True): - """Set the main type and subtype for the Content-Type header. - - type must be a string in the form "maintype/subtype", otherwise a - ValueError is raised. - - This method replaces the Content-Type header, keeping all the - parameters in place. If requote is False, this leaves the existing - header's quoting as is. Otherwise, the parameters will be quoted (the - default). - - An alternative header can be specified in the header argument. When - the Content-Type header is set, we'll always also add a MIME-Version - header. - """ - # BAW: should we be strict? - if not type.count('/') == 1: - raise ValueError - # Set the Content-Type, you get a MIME-Version - if header.lower() == 'content-type': - del self['mime-version'] - self['MIME-Version'] = '1.0' - if header not in self: - self[header] = type - return - params = self.get_params(header=header, unquote=requote) - del self[header] - self[header] = type - # Skip the first param; it's the old type. - for p, v in params[1:]: - self.set_param(p, v, header, requote) - - def get_filename(self, failobj=None): - """Return the filename associated with the payload if present. - - The filename is extracted from the Content-Disposition header's - `filename' parameter, and it is unquoted. If that header is missing - the `filename' parameter, this method falls back to looking for the - `name' parameter. - """ - - missing = object() - filename = self.get_param('filename', missing, 'content-disposition') - if filename is missing: - filename = self.get_param('name', missing, 'content-disposition') - if filename is missing: - return failobj - ret = utils.collapse_rfc2231_value(filename).strip() - return ret - - def get_boundary(self, failobj=None): - """Return the boundary associated with the payload if present. - - The boundary is extracted from the Content-Type header's `boundary' - parameter, and it is unquoted. - """ - missing = object() - boundary = self.get_param('boundary', missing) - if boundary is missing: - return failobj - # RFC 2046 says that boundaries may begin but not end in w/s - return utils.collapse_rfc2231_value(boundary).rstrip() - - def set_boundary(self, boundary): - """Set the boundary parameter in Content-Type to 'boundary'. - - This is subtly different than deleting the Content-Type header and - adding a new one with a new boundary parameter via add_header(). The - main difference is that using the set_boundary() method preserves the - order of the Content-Type header in the original message. - - HeaderParseError is raised if the message has no Content-Type header. - """ - missing = object() - params = self._get_params_preserve(missing, 'content-type') - if params is missing: - # There was no Content-Type header, and we don't know what type - # to set it to, so raise an exception. - raise errors.HeaderParseError('No Content-Type header found') - newparams = [] - foundp = False - for pk, pv in params: - if pk.lower() == 'boundary': - newparams.append(('boundary', '"%s"' % boundary)) - foundp = True - else: - newparams.append((pk, pv)) - if not foundp: - # The original Content-Type header had no boundary attribute. - # Tack one on the end. BAW: should we raise an exception - # instead??? - newparams.append(('boundary', '"%s"' % boundary)) - # Replace the existing Content-Type header with the new value - newheaders = [] - for h, v in self._headers: - if h.lower() == 'content-type': - parts = [] - for k, v in newparams: - if v == '': - parts.append(k) - else: - parts.append('%s=%s' % (k, v)) - newheaders.append((h, SEMISPACE.join(parts))) - - else: - newheaders.append((h, v)) - self._headers = newheaders - - def get_content_charset(self, failobj=None): - """Return the charset parameter of the Content-Type header. - - The returned string is always coerced to lower case. If there is no - Content-Type header, or if that header has no charset parameter, - failobj is returned. - """ - missing = object() - charset = self.get_param('charset', missing) - if charset is missing: - return failobj - if isinstance(charset, tuple): - # RFC 2231 encoded, so decode it, and it better end up as ascii. - pcharset = charset[0] or 'us-ascii' - - try: - # LookupError will be raised if the charset isn't known to - # Python. UnicodeError will be raised if the encoded text - # contains a character not in the charset. - charset = str(charset[2], pcharset).encode('us-ascii') - except (LookupError, UnicodeError): - charset = charset[2] - # charset character must be in us-ascii range - try: - charset = str(charset, 'us-ascii').encode('us-ascii') - except UnicodeError: - return failobj - - # RFC 2046, $4.1.2 says charsets are not case sensitive - return charset.lower() - - def get_charsets(self, failobj=None): - """Return a list containing the charset(s) used in this message. - - The returned list of items describes the Content-Type headers' - charset parameter for this message and all the subparts in its - payload. - - Each item will either be a string (the value of the charset parameter - in the Content-Type header of that part) or the value of the - 'failobj' parameter (defaults to None), if the part does not have a - main MIME type of "text", or the charset is not defined. - - The list will contain one string for each part of the message, plus - one for the container message (i.e. self), so that a non-multipart - message will still return a list of length 1. - """ - return [part.get_content_charset(failobj) for part in self.walk()] - - # I.e. def walk(self): ... - from bongo.external.email.iterators import walk diff --git a/src/libs/python/bongo/external/email/mime/__init__.py b/src/libs/python/bongo/external/email/mime/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/libs/python/bongo/external/email/mime/application.py b/src/libs/python/bongo/external/email/mime/application.py deleted file mode 100644 index 51bb75c..0000000 --- a/src/libs/python/bongo/external/email/mime/application.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Keith Dart -# Contact: email-sig@python.org - -"""Class representing application/* type MIME documents.""" - -__all__ = ["MIMEApplication"] - -from bongo.external.email import encoders -from bongo.external.email.mime.nonmultipart import MIMENonMultipart - - -class MIMEApplication(MIMENonMultipart): - """Class for generating application/* MIME documents.""" - - def __init__(self, _data, _subtype='octet-stream', - _encoder=encoders.encode_base64, **_params): - """Create an application/* type MIME document. - - _data is a string containing the raw applicatoin data. - - _subtype is the MIME content type subtype, defaulting to - 'octet-stream'. - - _encoder is a function which will perform the actual encoding for - transport of the application data, defaulting to base64 encoding. - - Any additional keyword arguments are passed to the base class - constructor, which turns them into parameters on the Content-Type - header. - """ - if _subtype is None: - raise TypeError('Invalid application MIME subtype') - MIMENonMultipart.__init__(self, 'application', _subtype, **_params) - self.set_payload(_data) - _encoder(self) diff --git a/src/libs/python/bongo/external/email/mime/audio.py b/src/libs/python/bongo/external/email/mime/audio.py deleted file mode 100644 index d07a9d4..0000000 --- a/src/libs/python/bongo/external/email/mime/audio.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Anthony Baxter -# Contact: email-sig@python.org - -"""Class representing audio/* type MIME documents.""" - -__all__ = ['MIMEAudio'] - -import sndhdr - -from io import StringIO -from bongo.external.email import encoders -from bongo.external.email.mime.nonmultipart import MIMENonMultipart - - - -_sndhdr_MIMEmap = {'au' : 'basic', - 'wav' :'x-wav', - 'aiff':'x-aiff', - 'aifc':'x-aiff', - } - -# There are others in sndhdr that don't have MIME types. :( -# Additional ones to be added to sndhdr? midi, mp3, realaudio, wma?? -def _whatsnd(data): - """Try to identify a sound file type. - - sndhdr.what() has a pretty cruddy interface, unfortunately. This is why - we re-do it here. It would be easier to reverse engineer the Unix 'file' - command and use the standard 'magic' file, as shipped with a modern Unix. - """ - hdr = data[:512] - fakefile = StringIO(hdr) - for testfn in sndhdr.tests: - res = testfn(hdr, fakefile) - if res is not None: - return _sndhdr_MIMEmap.get(res[0]) - return None - - - -class MIMEAudio(MIMENonMultipart): - """Class for generating audio/* MIME documents.""" - - def __init__(self, _audiodata, _subtype=None, - _encoder=encoders.encode_base64, **_params): - """Create an audio/* type MIME document. - - _audiodata is a string containing the raw audio data. If this data - can be decoded by the standard Python `sndhdr' module, then the - subtype will be automatically included in the Content-Type header. - Otherwise, you can specify the specific audio subtype via the - _subtype parameter. If _subtype is not given, and no subtype can be - guessed, a TypeError is raised. - - _encoder is a function which will perform the actual encoding for - transport of the image data. It takes one argument, which is this - Image instance. It should use get_payload() and set_payload() to - change the payload to the encoded form. It should also add any - Content-Transfer-Encoding or other headers to the message as - necessary. The default encoding is Base64. - - Any additional keyword arguments are passed to the base class - constructor, which turns them into parameters on the Content-Type - header. - """ - if _subtype is None: - _subtype = _whatsnd(_audiodata) - if _subtype is None: - raise TypeError('Could not find audio MIME subtype') - MIMENonMultipart.__init__(self, 'audio', _subtype, **_params) - self.set_payload(_audiodata) - _encoder(self) diff --git a/src/libs/python/bongo/external/email/mime/base.py b/src/libs/python/bongo/external/email/mime/base.py deleted file mode 100644 index 390e25a..0000000 --- a/src/libs/python/bongo/external/email/mime/base.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Base class for MIME specializations.""" - -__all__ = ['MIMEBase'] - -from bongo.external.email import message - - - -class MIMEBase(message.Message): - """Base class for MIME specializations.""" - - def __init__(self, _maintype, _subtype, **_params): - """This constructor adds a Content-Type: and a MIME-Version: header. - - The Content-Type: header is taken from the _maintype and _subtype - arguments. Additional parameters for this header are taken from the - keyword arguments. - """ - message.Message.__init__(self) - ctype = '%s/%s' % (_maintype, _subtype) - self.add_header('Content-Type', ctype, **_params) - self['MIME-Version'] = '1.0' diff --git a/src/libs/python/bongo/external/email/mime/image.py b/src/libs/python/bongo/external/email/mime/image.py deleted file mode 100644 index 9e11e3a..0000000 --- a/src/libs/python/bongo/external/email/mime/image.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Class representing image/* type MIME documents.""" - -__all__ = ['MIMEImage'] - -import imghdr - -from bongo.external.email import encoders -from bongo.external.email.mime.nonmultipart import MIMENonMultipart - - - -class MIMEImage(MIMENonMultipart): - """Class for generating image/* type MIME documents.""" - - def __init__(self, _imagedata, _subtype=None, - _encoder=encoders.encode_base64, **_params): - """Create an image/* type MIME document. - - _imagedata is a string containing the raw image data. If this data - can be decoded by the standard Python `imghdr' module, then the - subtype will be automatically included in the Content-Type header. - Otherwise, you can specify the specific image subtype via the _subtype - parameter. - - _encoder is a function which will perform the actual encoding for - transport of the image data. It takes one argument, which is this - Image instance. It should use get_payload() and set_payload() to - change the payload to the encoded form. It should also add any - Content-Transfer-Encoding or other headers to the message as - necessary. The default encoding is Base64. - - Any additional keyword arguments are passed to the base class - constructor, which turns them into parameters on the Content-Type - header. - """ - if _subtype is None: - _subtype = imghdr.what(None, _imagedata) - if _subtype is None: - raise TypeError('Could not guess image MIME subtype') - MIMENonMultipart.__init__(self, 'image', _subtype, **_params) - self.set_payload(_imagedata) - _encoder(self) diff --git a/src/libs/python/bongo/external/email/mime/message.py b/src/libs/python/bongo/external/email/mime/message.py deleted file mode 100644 index 22af733..0000000 --- a/src/libs/python/bongo/external/email/mime/message.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Class representing message/* MIME documents.""" - -__all__ = ['MIMEMessage'] - -from bongo.external.email import message -from bongo.external.email.mime.nonmultipart import MIMENonMultipart - - - -class MIMEMessage(MIMENonMultipart): - """Class representing message/* MIME documents.""" - - def __init__(self, _msg, _subtype='rfc822'): - """Create a message/* type MIME document. - - _msg is a message object and must be an instance of Message, or a - derived class of Message, otherwise a TypeError is raised. - - Optional _subtype defines the subtype of the contained message. The - default is "rfc822" (this is defined by the MIME standard, even though - the term "rfc822" is technically outdated by RFC 2822). - """ - MIMENonMultipart.__init__(self, 'message', _subtype) - if not isinstance(_msg, message.Message): - raise TypeError('Argument is not an instance of Message') - # It's convenient to use this base class method. We need to do it - # this way or we'll get an exception - message.Message.attach(self, _msg) - # And be sure our default type is set correctly - self.set_default_type('message/rfc822') diff --git a/src/libs/python/bongo/external/email/mime/multipart.py b/src/libs/python/bongo/external/email/mime/multipart.py deleted file mode 100644 index c679a57..0000000 --- a/src/libs/python/bongo/external/email/mime/multipart.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Base class for MIME multipart/* type messages.""" - -__all__ = ['MIMEMultipart'] - -from bongo.external.email.mime.base import MIMEBase - - - -class MIMEMultipart(MIMEBase): - """Base class for MIME multipart/* type messages.""" - - def __init__(self, _subtype='mixed', boundary=None, _subparts=None, - **_params): - """Creates a multipart/* type message. - - By default, creates a multipart/mixed message, with proper - Content-Type and MIME-Version headers. - - _subtype is the subtype of the multipart content type, defaulting to - `mixed'. - - boundary is the multipart boundary string. By default it is - calculated as needed. - - _subparts is a sequence of initial subparts for the payload. It - must be an iterable object, such as a list. You can always - attach new subparts to the message by using the attach() method. - - Additional parameters for the Content-Type header are taken from the - keyword arguments (or passed into the _params argument). - """ - MIMEBase.__init__(self, 'multipart', _subtype, **_params) - if _subparts: - for p in _subparts: - self.attach(p) - if boundary: - self.set_boundary(boundary) diff --git a/src/libs/python/bongo/external/email/mime/nonmultipart.py b/src/libs/python/bongo/external/email/mime/nonmultipart.py deleted file mode 100644 index 025c6b2..0000000 --- a/src/libs/python/bongo/external/email/mime/nonmultipart.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Base class for MIME type messages that are not multipart.""" - -__all__ = ['MIMENonMultipart'] - -from bongo.external.email import errors -from bongo.external.email.mime.base import MIMEBase - - - -class MIMENonMultipart(MIMEBase): - """Base class for MIME multipart/* type messages.""" - - __pychecker__ = 'unusednames=payload' - - def attach(self, payload): - # The public API prohibits attaching multiple subparts to MIMEBase - # derived subtypes since none of them are, by definition, of content - # type multipart/* - raise errors.MultipartConversionError( - 'Cannot attach additional subparts to non-multipart/*') - - del __pychecker__ diff --git a/src/libs/python/bongo/external/email/mime/text.py b/src/libs/python/bongo/external/email/mime/text.py deleted file mode 100644 index 716ea8c..0000000 --- a/src/libs/python/bongo/external/email/mime/text.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Class representing text/* type MIME documents.""" - -__all__ = ['MIMEText'] - -from bongo.external.email.encoders import encode_7or8bit -from bongo.external.email.mime.nonmultipart import MIMENonMultipart - - - -class MIMEText(MIMENonMultipart): - """Class for generating text/* type MIME documents.""" - - def __init__(self, _text, _subtype='plain', _charset='us-ascii'): - """Create a text/* type MIME document. - - _text is the string for this message object. - - _subtype is the MIME sub content type, defaulting to "plain". - - _charset is the character set parameter added to the Content-Type - header. This defaults to "us-ascii". Note that as a side-effect, the - Content-Transfer-Encoding header will also be set. - """ - MIMENonMultipart.__init__(self, 'text', _subtype, - **{'charset': _charset}) - self.set_payload(_text, _charset) diff --git a/src/libs/python/bongo/external/email/parser.py b/src/libs/python/bongo/external/email/parser.py deleted file mode 100644 index dd0a987..0000000 --- a/src/libs/python/bongo/external/email/parser.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw, Thomas Wouters, Anthony Baxter -# Contact: email-sig@python.org - -"""A parser of RFC 2822 and MIME email messages.""" - -__all__ = ['Parser', 'HeaderParser'] - -import warnings -from io import StringIO - -from bongo.external.email.feedparser import FeedParser -from bongo.external.email.message import Message - - - -class Parser: - def __init__(self, *args, **kws): - """Parser of RFC 2822 and MIME email messages. - - Creates an in-memory object tree representing the email message, which - can then be manipulated and turned over to a Generator to return the - textual representation of the message. - - The string must be formatted as a block of RFC 2822 headers and header - continuation lines, optionally preceeded by a `Unix-from' header. The - header block is terminated either by the end of the string or by a - blank line. - - _class is the class to instantiate for new message objects when they - must be created. This class must have a constructor that can take - zero arguments. Default is Message.Message. - """ - if len(args) >= 1: - if '_class' in kws: - raise TypeError("Multiple values for keyword arg '_class'") - kws['_class'] = args[0] - if len(args) == 2: - if 'strict' in kws: - raise TypeError("Multiple values for keyword arg 'strict'") - kws['strict'] = args[1] - if len(args) > 2: - raise TypeError('Too many arguments') - if '_class' in kws: - self._class = kws['_class'] - del kws['_class'] - else: - self._class = Message - if 'strict' in kws: - warnings.warn("'strict' argument is deprecated (and ignored)", - DeprecationWarning, 2) - del kws['strict'] - if kws: - raise TypeError('Unexpected keyword arguments') - - def parse(self, fp, headersonly=False): - """Create a message structure from the data in a file. - - Reads all the data from the file and returns the root of the message - structure. Optional headersonly is a flag specifying whether to stop - parsing after reading the headers or not. The default is False, - meaning it parses the entire contents of the file. - """ - feedparser = FeedParser(self._class) - if headersonly: - feedparser._set_headersonly() - while True: - data = fp.read(8192) - if not data: - break - feedparser.feed(data) - return feedparser.close() - - def parsestr(self, text, headersonly=False): - """Create a message structure from a string. - - Returns the root of the message structure. Optional headersonly is a - flag specifying whether to stop parsing after reading the headers or - not. The default is False, meaning it parses the entire contents of - the file. - """ - return self.parse(StringIO(text), headersonly=headersonly) - - - -class HeaderParser(Parser): - def parse(self, fp, headersonly=True): - return Parser.parse(self, fp, True) - - def parsestr(self, text, headersonly=True): - return Parser.parsestr(self, text, True) diff --git a/src/libs/python/bongo/external/email/payloads.py b/src/libs/python/bongo/external/email/payloads.py deleted file mode 100644 index 81dcd7f..0000000 --- a/src/libs/python/bongo/external/email/payloads.py +++ /dev/null @@ -1,154 +0,0 @@ -import tempfile -import binascii -import quopri -import uu -import base64 -from bongo.external.email import utils - -class Payload: - def __init__(self, encoding=None, value=''): - self.encoding = encoding - self.set(value) - - def set(self, buffer): - raise NotImplementedError - - def get(self, decode): - return ''.join(self.iter(decode)) - - def add(self, chunk): - raise NotImplementedError - - def strip_eol(self) : - raise NotImplementedError - - def iter(self, decode): - raise NotImplementedError - -class MemoryPayload(Payload): - def __init__(self, encoding=None, value='') : - Payload.__init__(self, encoding, value) - - def set(self, value): - self._buffer = [value] - - def add(self, chunk): - self._buffer.append(chunk) - - def strip_eol(self) : - value = ''.join(self._buffer) - if value.endswith('\r\n') : - value = value[:-2] - elif value.endswith('\r') or value.endswith('\n') : - value = value[:-1] - self._buffer = [value] - - def iter(self, decode): - if decode: - decoder = get_string_decoder(self.encoding) - else: - decoder = None - - # Just yield the payload all at once since it's all in memory already - if decoder: - yield decoder(''.join(self._buffer)) - else: - yield ''.join(self._buffer) - -class FilePayload(Payload): - - chunk_size = 8192 - - def __init__(self, encoding=None, value=''): - self._f = None - Payload.__init__(self, encoding, value) - - def __del__(self): - self._close() - - def _close(self): - if self._f: - self._f.close() - - def set(self, buf): - self._close() - self._f = tempfile.TemporaryFile() - self._f.write(buf) - - def add(self, chunk): - self._f.seek(0, 2) - self._f.write(chunk) - - def strip_eol(self) : - self._f.seek(0, 2) - toRead = self._f.tell() - if toRead > 2 : - toRead = 2 - - self._f.seek(0 - toRead, 2) - end = self._f.read(toRead) - if end == '\r\n' : - self._f.seek(-2, 2) - self._f.truncate() - elif end.endswith('\r') or end.endswith('\n') : - self._f.seek(-1, 2) - self._f.truncate() - - def iter(self, decode): - if decode: - decoder = get_file_decoder(self.encoding) - else: - decoder = None - - if decoder: - # Decode into a separate file first to ensure there are no decoding - # errors. - fout = tempfile.TemporaryFile() - self._f.seek(0, 0) - decoder(self._f, fout) - else: - fout = self._f - - # Feed out the payload in chunks - fout.seek(0, 0) - while 1: - buf = fout.read(self.chunk_size) - if buf: - yield buf - else: - break - - # If decoding occurred, close the temporary file for the decoded version - if fout != self._f: - fout.close() - -def get_string_decoder(cte): - if cte == None : - return None - - cte = cte.lower() - - if cte == 'quoted-printable': - return utils._qdecode - elif cte == 'base64': - return utils._bdecode - elif cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'): - return binascii.a2b_uu - else: - return None - -def get_file_decoder(cte): - if cte == None : - return None - - cte = cte.lower() - - if cte == 'quoted-printable': - return quopri.decode - elif cte == 'base64': - return base64.decode - elif cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'): - return uu.decode - else: - return None - diff --git a/src/libs/python/bongo/external/email/quoprimime.py b/src/libs/python/bongo/external/email/quoprimime.py deleted file mode 100644 index b3846d2..0000000 --- a/src/libs/python/bongo/external/email/quoprimime.py +++ /dev/null @@ -1,336 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Ben Gertzfield -# Contact: email-sig@python.org - -"""Quoted-printable content transfer encoding per RFCs 2045-2047. - -This module handles the content transfer encoding method defined in RFC 2045 -to encode US ASCII-like 8-bit data called `quoted-printable'. It is used to -safely encode text that is in a character set similar to the 7-bit US ASCII -character set, but that includes some 8-bit characters that are normally not -allowed in email bodies or headers. - -Quoted-printable is very space-inefficient for encoding binary files; use the -email.base64MIME module for that instead. - -This module provides an interface to encode and decode both headers and bodies -with quoted-printable encoding. - -RFC 2045 defines a method for including character set information in an -`encoded-word' in a header. This method is commonly used for 8-bit real names -in To:/From:/Cc: etc. fields, as well as Subject: lines. - -This module does not do the line wrapping or end-of-line character -conversion necessary for proper internationalized headers; it only -does dumb encoding and decoding. To deal with the various line -wrapping issues, use the email.Header module. -""" - -__all__ = [ - 'body_decode', - 'body_encode', - 'body_quopri_check', - 'body_quopri_len', - 'decode', - 'decodestring', - 'encode', - 'encodestring', - 'header_decode', - 'header_encode', - 'header_quopri_check', - 'header_quopri_len', - 'quote', - 'unquote', - ] - -import re - -from string import hexdigits -from bongo.external.email.utils import fix_eols - -CRLF = '\r\n' -NL = '\n' - -# See also Charset.py -MISC_LEN = 7 - -hqre = re.compile(r'[^-a-zA-Z0-9!*+/ ]') -bqre = re.compile(r'[^ !-<>-~\t]') - - - -# Helpers -def header_quopri_check(c): - """Return True if the character should be escaped with header quopri.""" - return bool(hqre.match(c)) - - -def body_quopri_check(c): - """Return True if the character should be escaped with body quopri.""" - return bool(bqre.match(c)) - - -def header_quopri_len(s): - """Return the length of str when it is encoded with header quopri.""" - count = 0 - for c in s: - if hqre.match(c): - count += 3 - else: - count += 1 - return count - - -def body_quopri_len(str): - """Return the length of str when it is encoded with body quopri.""" - count = 0 - for c in str: - if bqre.match(c): - count += 3 - else: - count += 1 - return count - - -def _max_append(L, s, maxlen, extra=''): - if not L: - L.append(s.lstrip()) - elif len(L[-1]) + len(s) <= maxlen: - L[-1] += extra + s - else: - L.append(s.lstrip()) - - -def unquote(s): - """Turn a string in the form =AB to the ASCII character with value 0xab""" - return chr(int(s[1:3], 16)) - - -def quote(c): - return "=%02X" % ord(c) - - - -def header_encode(header, charset="iso-8859-1", keep_eols=False, - maxlinelen=76, eol=NL): - """Encode a single header line with quoted-printable (like) encoding. - - Defined in RFC 2045, this `Q' encoding is similar to quoted-printable, but - used specifically for email header fields to allow charsets with mostly 7 - bit characters (and some 8 bit) to remain more or less readable in non-RFC - 2045 aware mail clients. - - charset names the character set to use to encode the header. It defaults - to iso-8859-1. - - The resulting string will be in the form: - - "=?charset?q?I_f=E2rt_in_your_g=E8n=E8ral_dire=E7tion?\\n - =?charset?q?Silly_=C8nglish_Kn=EEghts?=" - - with each line wrapped safely at, at most, maxlinelen characters (defaults - to 76 characters). If maxlinelen is None, the entire string is encoded in - one chunk with no splitting. - - End-of-line characters (\\r, \\n, \\r\\n) will be automatically converted - to the canonical email line separator \\r\\n unless the keep_eols - parameter is True (the default is False). - - Each line of the header will be terminated in the value of eol, which - defaults to "\\n". Set this to "\\r\\n" if you are using the result of - this function directly in email. - """ - # Return empty headers unchanged - if not header: - return header - - if not keep_eols: - header = fix_eols(header) - - # Quopri encode each line, in encoded chunks no greater than maxlinelen in - # length, after the RFC chrome is added in. - quoted = [] - if maxlinelen is None: - # An obnoxiously large number that's good enough - max_encoded = 100000 - else: - max_encoded = maxlinelen - len(charset) - MISC_LEN - 1 - - for c in header: - # Space may be represented as _ instead of =20 for readability - if c == ' ': - _max_append(quoted, '_', max_encoded) - # These characters can be included verbatim - elif not hqre.match(c): - _max_append(quoted, c, max_encoded) - # Otherwise, replace with hex value like =E2 - else: - _max_append(quoted, "=%02X" % ord(c), max_encoded) - - # Now add the RFC chrome to each encoded chunk and glue the chunks - # together. BAW: should we be able to specify the leading whitespace in - # the joiner? - joiner = eol + ' ' - return joiner.join(['=?%s?q?%s?=' % (charset, line) for line in quoted]) - - - -def encode(body, binary=False, maxlinelen=76, eol=NL): - """Encode with quoted-printable, wrapping at maxlinelen characters. - - If binary is False (the default), end-of-line characters will be converted - to the canonical email end-of-line sequence \\r\\n. Otherwise they will - be left verbatim. - - Each line of encoded text will end with eol, which defaults to "\\n". Set - this to "\\r\\n" if you will be using the result of this function directly - in an email. - - Each line will be wrapped at, at most, maxlinelen characters (defaults to - 76 characters). Long lines will have the `soft linefeed' quoted-printable - character "=" appended to them, so the decoded text will be identical to - the original text. - """ - if not body: - return body - - if not binary: - body = fix_eols(body) - - # BAW: We're accumulating the body text by string concatenation. That - # can't be very efficient, but I don't have time now to rewrite it. It - # just feels like this algorithm could be more efficient. - encoded_body = '' - lineno = -1 - # Preserve line endings here so we can check later to see an eol needs to - # be added to the output later. - lines = body.splitlines(1) - for line in lines: - # But strip off line-endings for processing this line. - if line.endswith(CRLF): - line = line[:-2] - elif line[-1] in CRLF: - line = line[:-1] - - lineno += 1 - encoded_line = '' - prev = None - linelen = len(line) - # Now we need to examine every character to see if it needs to be - # quopri encoded. BAW: again, string concatenation is inefficient. - for j in range(linelen): - c = line[j] - prev = c - if bqre.match(c): - c = quote(c) - elif j+1 == linelen: - # Check for whitespace at end of line; special case - if c not in ' \t': - encoded_line += c - prev = c - continue - # Check to see to see if the line has reached its maximum length - if len(encoded_line) + len(c) >= maxlinelen: - encoded_body += encoded_line + '=' + eol - encoded_line = '' - encoded_line += c - # Now at end of line.. - if prev and prev in ' \t': - # Special case for whitespace at end of file - if lineno + 1 == len(lines): - prev = quote(prev) - if len(encoded_line) + len(prev) > maxlinelen: - encoded_body += encoded_line + '=' + eol + prev - else: - encoded_body += encoded_line + prev - # Just normal whitespace at end of line - else: - encoded_body += encoded_line + prev + '=' + eol - encoded_line = '' - # Now look at the line we just finished and it has a line ending, we - # need to add eol to the end of the line. - if lines[lineno].endswith(CRLF) or lines[lineno][-1] in CRLF: - encoded_body += encoded_line + eol - else: - encoded_body += encoded_line - encoded_line = '' - return encoded_body - - -# For convenience and backwards compatibility w/ standard base64 module -body_encode = encode -encodestring = encode - - - -# BAW: I'm not sure if the intent was for the signature of this function to be -# the same as base64MIME.decode() or not... -def decode(encoded, eol=NL): - """Decode a quoted-printable string. - - Lines are separated with eol, which defaults to \\n. - """ - if not encoded: - return encoded - # BAW: see comment in encode() above. Again, we're building up the - # decoded string with string concatenation, which could be done much more - # efficiently. - decoded = '' - - for line in encoded.splitlines(): - line = line.rstrip() - if not line: - decoded += eol - continue - - i = 0 - n = len(line) - while i < n: - c = line[i] - if c != '=': - decoded += c - i += 1 - # Otherwise, c == "=". Are we at the end of the line? If so, add - # a soft line break. - elif i+1 == n: - i += 1 - continue - # Decode if in form =AB - elif i+2 < n and line[i+1] in hexdigits and line[i+2] in hexdigits: - decoded += unquote(line[i:i+3]) - i += 3 - # Otherwise, not in form =AB, pass literally - else: - decoded += c - i += 1 - - if i == n: - decoded += eol - # Special case if original string did not end with eol - if not encoded.endswith(eol) and decoded.endswith(eol): - decoded = decoded[:-1] - return decoded - - -# For convenience and backwards compatibility w/ standard base64 module -body_decode = decode -decodestring = decode - - - -def _unquote_match(match): - """Turn a match in the form =AB to the ASCII character with value 0xab""" - s = match.group(0) - return unquote(s) - - -# Header decoding is done a bit differently -def header_decode(s): - """Decode a string encoded with RFC 2045 MIME header `Q' encoding. - - This function does not parse a full MIME header value encoded with - quoted-printable (like =?iso-8895-1?q?Hello_World?=) -- please use - the high level email.Header class for that functionality. - """ - s = s.replace('_', ' ') - return re.sub(r'=\w{2}', _unquote_match, s) diff --git a/src/libs/python/bongo/external/email/test/__init__.py b/src/libs/python/bongo/external/email/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/libs/python/bongo/external/email/test/data/PyBanner048.gif b/src/libs/python/bongo/external/email/test/data/PyBanner048.gif deleted file mode 100644 index 1a5c87f..0000000 Binary files a/src/libs/python/bongo/external/email/test/data/PyBanner048.gif and /dev/null differ diff --git a/src/libs/python/bongo/external/email/test/data/audiotest.au b/src/libs/python/bongo/external/email/test/data/audiotest.au deleted file mode 100644 index 1ad28ce..0000000 Binary files a/src/libs/python/bongo/external/email/test/data/audiotest.au and /dev/null differ diff --git a/src/libs/python/bongo/external/email/test/data/msg_01.txt b/src/libs/python/bongo/external/email/test/data/msg_01.txt deleted file mode 100644 index 7e33bcf..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_01.txt +++ /dev/null @@ -1,19 +0,0 @@ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) - id 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 - - -Hi, - -Do you like this message? - --Me diff --git a/src/libs/python/bongo/external/email/test/data/msg_02.txt b/src/libs/python/bongo/external/email/test/data/msg_02.txt deleted file mode 100644 index 43f2480..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_02.txt +++ /dev/null @@ -1,135 +0,0 @@ -MIME-version: 1.0 -From: ppp-request@zzz.org -Sender: ppp-admin@zzz.org -To: ppp@zzz.org -Subject: Ppp digest, Vol 1 #2 - 5 msgs -Date: Fri, 20 Apr 2001 20:18:00 -0400 (EDT) -X-Mailer: Mailman v2.0.4 -X-Mailman-Version: 2.0.4 -Content-Type: multipart/mixed; boundary="192.168.1.2.889.32614.987812255.500.21814" - ---192.168.1.2.889.32614.987812255.500.21814 -Content-type: text/plain; charset=us-ascii -Content-description: Masthead (Ppp digest, Vol 1 #2) - -Send Ppp mailing list submissions to - ppp@zzz.org - -To subscribe or unsubscribe via the World Wide Web, visit - http://www.zzz.org/mailman/listinfo/ppp -or, via email, send a message with subject or body 'help' to - ppp-request@zzz.org - -You can reach the person managing the list at - ppp-admin@zzz.org - -When replying, please edit your Subject line so it is more specific -than "Re: Contents of Ppp digest..." - - ---192.168.1.2.889.32614.987812255.500.21814 -Content-type: text/plain; charset=us-ascii -Content-description: Today's Topics (5 msgs) - -Today's Topics: - - 1. testing #1 (Barry A. Warsaw) - 2. testing #2 (Barry A. Warsaw) - 3. testing #3 (Barry A. Warsaw) - 4. testing #4 (Barry A. Warsaw) - 5. testing #5 (Barry A. Warsaw) - ---192.168.1.2.889.32614.987812255.500.21814 -Content-Type: multipart/digest; boundary="__--__--" - ---__--__-- - -Message: 1 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -Date: Fri, 20 Apr 2001 20:16:13 -0400 -To: ppp@zzz.org -From: barry@digicool.com (Barry A. Warsaw) -Subject: [Ppp] testing #1 -Precedence: bulk - - -hello - - ---__--__-- - -Message: 2 -Date: Fri, 20 Apr 2001 20:16:21 -0400 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -To: ppp@zzz.org -From: barry@digicool.com (Barry A. Warsaw) -Precedence: bulk - - -hello - - ---__--__-- - -Message: 3 -Date: Fri, 20 Apr 2001 20:16:25 -0400 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -To: ppp@zzz.org -From: barry@digicool.com (Barry A. Warsaw) -Subject: [Ppp] testing #3 -Precedence: bulk - - -hello - - ---__--__-- - -Message: 4 -Date: Fri, 20 Apr 2001 20:16:28 -0400 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -To: ppp@zzz.org -From: barry@digicool.com (Barry A. Warsaw) -Subject: [Ppp] testing #4 -Precedence: bulk - - -hello - - ---__--__-- - -Message: 5 -Date: Fri, 20 Apr 2001 20:16:32 -0400 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -To: ppp@zzz.org -From: barry@digicool.com (Barry A. Warsaw) -Subject: [Ppp] testing #5 -Precedence: bulk - - -hello - - - - ---__--__---- ---192.168.1.2.889.32614.987812255.500.21814 -Content-type: text/plain; charset=us-ascii -Content-description: Digest Footer - -_______________________________________________ -Ppp mailing list -Ppp@zzz.org -http://www.zzz.org/mailman/listinfo/ppp - - ---192.168.1.2.889.32614.987812255.500.21814-- - -End of Ppp Digest - diff --git a/src/libs/python/bongo/external/email/test/data/msg_03.txt b/src/libs/python/bongo/external/email/test/data/msg_03.txt deleted file mode 100644 index c748ebf..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_03.txt +++ /dev/null @@ -1,16 +0,0 @@ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) - id 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 - - -Hi, - -Do you like this message? - --Me diff --git a/src/libs/python/bongo/external/email/test/data/msg_04.txt b/src/libs/python/bongo/external/email/test/data/msg_04.txt deleted file mode 100644 index 1f633c4..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_04.txt +++ /dev/null @@ -1,37 +0,0 @@ -Return-Path: -Delivered-To: barry@python.org -Received: by mail.python.org (Postfix, from userid 889) - id C2BF0D37C6; Tue, 11 Sep 2001 00:05:05 -0400 (EDT) -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="h90VIIIKmx" -Content-Transfer-Encoding: 7bit -Message-ID: <15261.36209.358846.118674@anthem.python.org> -From: barry@python.org (Barry A. Warsaw) -To: barry@python.org -Subject: a simple multipart -Date: Tue, 11 Sep 2001 00:05:05 -0400 -X-Mailer: VM 6.95 under 21.4 (patch 4) "Artificial Intelligence" XEmacs Lucid -X-Attribution: BAW -X-Oblique-Strategy: Make a door into a window - - ---h90VIIIKmx -Content-Type: text/plain -Content-Disposition: inline; - filename="msg.txt" -Content-Transfer-Encoding: 7bit - -a simple kind of mirror -to reflect upon our own - ---h90VIIIKmx -Content-Type: text/plain -Content-Disposition: inline; - filename="msg.txt" -Content-Transfer-Encoding: 7bit - -a simple kind of mirror -to reflect upon our own - ---h90VIIIKmx-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_05.txt b/src/libs/python/bongo/external/email/test/data/msg_05.txt deleted file mode 100644 index 87d5e9c..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_05.txt +++ /dev/null @@ -1,28 +0,0 @@ -From: foo -Subject: bar -To: baz -MIME-Version: 1.0 -Content-Type: multipart/report; report-type=delivery-status; - boundary="D1690A7AC1.996856090/mail.example.com" -Message-Id: <20010803162810.0CA8AA7ACC@mail.example.com> - -This is a MIME-encapsulated message. - ---D1690A7AC1.996856090/mail.example.com -Content-Type: text/plain - -Yadda yadda yadda - ---D1690A7AC1.996856090/mail.example.com - -Yadda yadda yadda - ---D1690A7AC1.996856090/mail.example.com -Content-Type: message/rfc822 - -From: nobody@python.org - -Yadda yadda yadda - ---D1690A7AC1.996856090/mail.example.com-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_06.txt b/src/libs/python/bongo/external/email/test/data/msg_06.txt deleted file mode 100644 index 69f3a47..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_06.txt +++ /dev/null @@ -1,33 +0,0 @@ -Return-Path: -Delivered-To: barry@python.org -MIME-Version: 1.0 -Content-Type: message/rfc822 -Content-Description: forwarded message -Content-Transfer-Encoding: 7bit -Message-ID: <15265.9482.641338.555352@python.org> -From: barry@zope.com (Barry A. Warsaw) -Sender: barry@python.org -To: barry@python.org -Subject: forwarded message from Barry A. Warsaw -Date: Thu, 13 Sep 2001 17:28:42 -0400 -X-Mailer: VM 6.95 under 21.4 (patch 4) "Artificial Intelligence" XEmacs Lucid -X-Attribution: BAW -X-Oblique-Strategy: Be dirty -X-Url: http://barry.wooz.org - -MIME-Version: 1.0 -Content-Type: text/plain; charset=us-ascii -Return-Path: -Delivered-To: barry@python.org -Message-ID: <15265.9468.713530.98441@python.org> -From: barry@zope.com (Barry A. Warsaw) -Sender: barry@python.org -To: barry@python.org -Subject: testing -Date: Thu, 13 Sep 2001 17:28:28 -0400 -X-Mailer: VM 6.95 under 21.4 (patch 4) "Artificial Intelligence" XEmacs Lucid -X-Attribution: BAW -X-Oblique-Strategy: Spectrum analysis -X-Url: http://barry.wooz.org - - diff --git a/src/libs/python/bongo/external/email/test/data/msg_07.txt b/src/libs/python/bongo/external/email/test/data/msg_07.txt deleted file mode 100644 index 721f3a0..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_07.txt +++ /dev/null @@ -1,83 +0,0 @@ -MIME-Version: 1.0 -From: Barry -To: Dingus Lovers -Subject: Here is your dingus fish -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - -Hi there, - -This is the dingus fish. - ---BOUNDARY -Content-Type: image/gif; name="dingusfish.gif" -Content-Transfer-Encoding: base64 -content-disposition: attachment; filename="dingusfish.gif" - -R0lGODdhAAEAAfAAAP///wAAACwAAAAAAAEAAQAC/oSPqcvtD6OctNqLs968+w+G4kiW5omm6sq2 -7gvH8kzX9o3n+s73/g8MCofEovGITGICTKbyCV0FDNOo9SqpQqpOrJfXzTQj2vD3TGtqL+NtGQ2f -qTXmxzuOd7WXdcc9DyjU53ewFni4s0fGhdiYaEhGBelICTNoV1j5NUnFcrmUqemjNifJVWpaOqaI -oFq3SspZsSraE7sHq3jr1MZqWvi662vxV4tD+pvKW6aLDOCLyur8PDwbanyDeq0N3DctbQYeLDvR -RY6t95m6UB0d3mwIrV7e2VGNvjjffukeJp4w7F65KecGFsTHQGAygOrgrWs1jt28Rc88KESYcGLA -/obvTkH6p+CinWJiJmIMqXGQwH/y4qk0SYjgQTczT3ajKZGfuI0uJ4kkVI/DT5s3/ejkxI0aT4Y+ -YTYgWbImUaXk9nlLmnSh1qJiJFl0OpUqRK4oOy7NyRQtHWofhoYVxkwWXKUSn0YsS+fUV6lhqfYb -6ayd3Z5qQdG1B7bvQzaJjwUV2lixMUZ7JVsOlfjWVr/3NB/uFvnySBN6Dcb6rGwaRM3wsormw5cC -M9NxWy/bWdufudCvy8bOAjXjVVwta/uO21sE5RHBCzNFXtgq9ORtH4eYjVP4Yryo026nvkFmCeyA -B29efV6ravCMK5JwWd5897Qrx7ll38o6iHDZ/rXPR//feevhF4l7wjUGX3xq1eeRfM4RSJGBIV1D -z1gKPkfWag3mVBVvva1RlX5bAJTPR/2YqNtw/FkIYYEi/pIZiAdpcxpoHtmnYYoZtvhUftzdx5ZX -JSKDW405zkGcZzzGZ6KEv4FI224oDmijlEf+xp6MJK5ojY/ASeVUR+wsKRuJ+XFZ5o7ZeEime8t1 -ouUsU6YjF5ZtUihhkGfCdFQLWQFJ3UXxmElfhQnR+eCdcDbkFZp6vTRmj56ApCihn5QGpaToNZmR -n3NVSpZcQpZ2KEONusaiCsKAug0wkQbJSFO+PTSjneGxOuFjPlUk3ovWvdIerjUg9ZGIOtGq/qeX -eCYrrCX+1UPsgTKGGRSbzd5q156d/gpfbJxe66eD5iQKrXj7RGgruGxs62qebBHUKS32CKluCiqZ -qh+pmehmEb71noAUoe5e9Zm17S7773V10pjrtG4CmuurCV/n6zLK5turWNhqOvFXbjhZrMD0YhKe -wR0zOyuvsh6MWrGoIuzvyWu5y1WIFAqmJselypxXh6dKLNOKEB98L88bS2rkNqqlKzCNJp9c0G0j -Gzh0iRrCbHSXmPR643QS+4rWhgFmnSbSuXCjS0xAOWkU2UdLqyuUNfHSFdUouy3bm5i5GnDM3tG8 -doJ4r5tqu3pPbRSVfvs8uJzeNXhp3n4j/tZ42SwH7eaWUUOjc3qFV9453UHTXZfcLH+OeNs5g36x -lBnHvTm7EbMbLeuaLncao8vWCXimfo1o+843Ak6y4ChNeGntvAYvfLK4ezmoyNIbNCLTCXO9ZV3A -E8/s88RczPzDwI4Ob7XZyl7+9Miban29h+tJZPrE21wgvBphDfrrfPdCTPKJD/y98L1rZwHcV6Jq -Zab0metpuNIX/qAFPoz171WUaUb4HAhBSzHuHfjzHb3kha/2Cctis/ORArVHNYfFyYRH2pYIRzic -isVOfPWD1b6mRTqpCRBozzof6UZVvFXRxWIr3GGrEviGYgyPMfahheiSaLs/9QeFu7oZ/ndSY8DD -ya9x+uPed+7mxN2IzIISBOMLFYWVqC3Pew1T2nFuuCiwZS5/v6II10i4t1OJcUH2U9zxKodHsGGv -Oa+zkvNUYUOa/TCCRutF9MzDwdlUMJADTCGSbDQ5OV4PTamDoPEi6Ecc/RF5RWwkcdSXvSOaDWSn -I9LlvubFTQpuc6JKXLcKeb+xdbKRBnwREemXyjg6ME65aJiOuBgrktzykfPLJBKR9ClMavJ62/Ff -BlNIyod9yX9wcSXexnXFpvkrbXk64xsx5Db7wXKP5fSgsvwIMM/9631VLBfkmtbHRXpqmtei52hG -pUwSlo+BASQoeILDOBgREECxBBh5/iYmNsQ9dIv5+OI++QkqdsJPc3uykz5fkM+OraeekcQF7X4n -B5S67za5U967PmooGQhUXfF7afXyCD7ONdRe17QogYjVx38uLwtrS6nhTnm15LQUnu9E2uK6CNI/ -1HOABj0ESwOjut4FEpFQpdNAm4K2LHnDWHNcmKB2ioKBogysVZtMO2nSxUdZ8Yk2kJc7URioLVI0 -YgmtIwZj4LoeKemgnOnbUdGnzZ4Oa6scqiolBGqS6RgWNLu0RMhcaE6rhhU4hiuqFXPAG8fGwTPW -FKeLMtdVmXLSs5YJGF/YeVm7rREMlY3UYE+yCxbaMXX8y15m5zVHq6GOKDMynzII/jdUHdyVqIy0 -ifX2+r/EgtZcvRzSb72gU9ui87M2VecjKildW/aFqaYhKoryUjfB/g4qtyVuc60xFDGmCxwjW+qu -zjuwl2GkOWn66+3QiiEctvd04OVvcCVzjgT7lrkvjVGKKHmmlDUKowSeikb5kK/mJReuWOxONx+s -ULsl+Lqb0CVn0SrVyJ6wt4t6yTeSCafhPhAf0OXn6L60UMxiLolFAtmN35S2Ob1lZpQ1r/n0Qb5D -oQ1zJiRVDgF8N3Q8TYfbi3DyWCy3lT1nxyBs6FT3S2GOzWRlxwKvlRP0RPJA9SjxEy0UoEnkA+M4 -cnzLMJrBGWLFEaaUb5lvpqbq/loOaU5+DFuHPxo82/OZuM8FXG3oVNZhtWpMpb/0Xu5m/LfLhHZQ -7yuVI0MqZ7NE43imC8jH3IwGZlbPm0xkJYs7+2U48hXTsFSMqgGDvai0kLxyynKNT/waj+q1c1tz -GjOpPBgdCSq3UKZxCSsqFIY+O6JbAWGWcV1pwqLyj5sGqCF1xb1F3varUWqrJv6cN3PrUXzijtfZ -FshpBL3Xwr4GIPvU2N8EjrJgS1zl21rbXQMXeXc5jjFyrhpCzijSv/RQtyPSzHCFMhlME95fHglt -pRsX+dfSQjUeHAlpWzJ5iOo79Ldnaxai6bXTcGO3fp07ri7HLEmXXPlYi8bv/qVxvNcdra6m7Rlb -6JBTb5fd66VhFRjGArh2n7R1rDW4P5NOT9K0I183T2scYkeZ3q/VFyLb09U9ajzXBS8Kgkhc4mBS -kYY9cy3Vy9lUnuNJH8HGIclUilwnBtjUOH0gteGOZ4c/XNrhXLSYDyxfnD8z1pDy7rYRvDolhnbe -UMzxCZUs40s6s7UIvBnLgc0+vKuOkIXeOrDymlp+Zxra4MZLBbVrqD/jTJ597pDmnw5c4+DbyB88 -9Cg9DodYcSuMZT/114pptqc/EuTjRPvH/z5slzI3tluOEBBLqOXLOX+0I5929tO97wkvl/atCz+y -xJrdwteW2FNW/NSmBP+f/maYtVs/bYyBC7Ox3jsYZHL05CIrBa/nS+b3bHfiYm4Ueil1YZZSgAUI -fFZ1dxUmeA2oQRQ3RuGXNGLFV9/XbGFGPV6kfzk1TBBCd+izc7q1H+OHMJwmaBX2IQNYVAKHYepV -SSGCe6CnbYHHETKGNe43EDvFgZr0gB/nVHPHZ80VV1ojOiI3XDvYIkl4ayo4bxQIgrFXWTvBI0nH -VElWMuw2aLUWCRHHf8ymVCHjFlJnOSojfevCYyyyZDH0IcvHhrsnQ5O1OsWzONuVVKIxSxiFZ/tR -fKDAf6xFTnw4O9Qig2VCfW2hJQrmMOuHW0W3dLQmCMO2ccdUd/xyfflH/olTiHZVdGwb8nIwRzSE -J15jFlOJuBZBZ4CiyHyd2IFylFlB+HgHhYabhWOGwYO1ZH/Og1dtQlFMk352CGRSIFTapnWQEUtN -l4zv8S0aaCFDyGCBqDUxZYpxGHX01y/JuH1xhn7TOCnNCI4eKDs5WGX4R425F4vF1o3BJ4vO0otq -I3rimI7jJY1jISqnBxknCIvruF83mF5wN4X7qGLIhR8A2Vg0yFERSIXn9Vv3GHy3Vj/WIkKddlYi -yIMv2I/VMjTLpW7pt05SWIZR0RPyxpB4SIUM9lBPGBl0GC7oSEEwRYLe4pJpZY2P0zbI1n+Oc44w -qY3PUnmF0ixjVpDD/mJ9wpOBGTVgXlaCaZiPcIWK5NiKBIiPdGaQ0TWGvAiG7nMchdZb7Vgf8zNi -MuMyzRdy/lePe9iC4TRx7WhhOQI/QiSVNAmAa2lT/piFbuh7ofJoYSZzrSZ1bvmWw3eN2nKUPVky -uPN5/VRfohRd0VYZoqhKIlU6TXYhJxmPUIloAwc1bPmHEpaZYZORHNlXUJM07hATwHR8MJYqkwWR -WaIezFhxSFlc8/Fq82hEnpeRozg3ULhhr9lAGtVEkCg5ZNRuuVleBPaZadhG0ZgkyPmDOTOKzViM -YgOcpukKqQcbjAWS0IleQ2ROjdh6A+md1qWdBRSX7iSYgFRTtRmBpJioieXJiHfJiMGIR9fJOn8I -MSfXYhspn4ooSa2mSAj4n+8Bmg03fBJZoPOJgsVZRxu1oOMRPXYYjdqjihFaEoZpXBREanuJoRI6 -cibFinq4ngUKh/wQd/H5ofYCZ0HJXR62opZFaAT0iFIZo4DIiUojkjeqKiuoZirKo5Y1a7AWckGa -BkuYoD5lpDK6eUs6CkDqpETwl1EqpfhJpVeKpVl6EgUAADs= - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_08.txt b/src/libs/python/bongo/external/email/test/data/msg_08.txt deleted file mode 100644 index b563083..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_08.txt +++ /dev/null @@ -1,24 +0,0 @@ -MIME-Version: 1.0 -From: Barry Warsaw -To: Dingus Lovers -Subject: Lyrics -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - - ---BOUNDARY -Content-Type: text/html; charset="iso-8859-1" - - ---BOUNDARY -Content-Type: text/plain; charset="iso-8859-2" - - ---BOUNDARY -Content-Type: text/plain; charset="koi8-r" - - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_09.txt b/src/libs/python/bongo/external/email/test/data/msg_09.txt deleted file mode 100644 index 575c4c2..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_09.txt +++ /dev/null @@ -1,24 +0,0 @@ -MIME-Version: 1.0 -From: Barry Warsaw -To: Dingus Lovers -Subject: Lyrics -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - - ---BOUNDARY -Content-Type: text/html; charset="iso-8859-1" - - ---BOUNDARY -Content-Type: text/plain - - ---BOUNDARY -Content-Type: text/plain; charset="koi8-r" - - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_10.txt b/src/libs/python/bongo/external/email/test/data/msg_10.txt deleted file mode 100644 index bd30d13..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_10.txt +++ /dev/null @@ -1,32 +0,0 @@ -MIME-Version: 1.0 -From: Barry Warsaw -To: Dingus Lovers -Subject: Lyrics -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -Content-Transfer-Encoding: 7bit - -This is a 7bit encoded message. - ---BOUNDARY -Content-Type: text/html; charset="iso-8859-1" -Content-Transfer-Encoding: Quoted-Printable - -=A1This is a Quoted Printable encoded message! - ---BOUNDARY -Content-Type: text/plain; charset="iso-8859-1" -Content-Transfer-Encoding: Base64 - -VGhpcyBpcyBhIEJhc2U2NCBlbmNvZGVkIG1lc3NhZ2Uu - - ---BOUNDARY -Content-Type: text/plain; charset="iso-8859-1" - -This has no Content-Transfer-Encoding: header. - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_11.txt b/src/libs/python/bongo/external/email/test/data/msg_11.txt deleted file mode 100644 index 8f7f199..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_11.txt +++ /dev/null @@ -1,7 +0,0 @@ -Content-Type: message/rfc822 -MIME-Version: 1.0 -Subject: The enclosing message - -Subject: An enclosed message - -Here is the body of the message. diff --git a/src/libs/python/bongo/external/email/test/data/msg_12.txt b/src/libs/python/bongo/external/email/test/data/msg_12.txt deleted file mode 100644 index 4bec8d9..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_12.txt +++ /dev/null @@ -1,36 +0,0 @@ -MIME-Version: 1.0 -From: Barry Warsaw -To: Dingus Lovers -Subject: Lyrics -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - - ---BOUNDARY -Content-Type: text/html; charset="iso-8859-1" - - ---BOUNDARY -Content-Type: multipart/mixed; boundary="ANOTHER" - ---ANOTHER -Content-Type: text/plain; charset="iso-8859-2" - - ---ANOTHER -Content-Type: text/plain; charset="iso-8859-3" - ---ANOTHER-- - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - - ---BOUNDARY -Content-Type: text/plain; charset="koi8-r" - - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_12a.txt b/src/libs/python/bongo/external/email/test/data/msg_12a.txt deleted file mode 100644 index e94224e..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_12a.txt +++ /dev/null @@ -1,38 +0,0 @@ -MIME-Version: 1.0 -From: Barry Warsaw -To: Dingus Lovers -Subject: Lyrics -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - - ---BOUNDARY -Content-Type: text/html; charset="iso-8859-1" - - ---BOUNDARY -Content-Type: multipart/mixed; boundary="ANOTHER" - ---ANOTHER -Content-Type: text/plain; charset="iso-8859-2" - - ---ANOTHER -Content-Type: text/plain; charset="iso-8859-3" - - ---ANOTHER-- - - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - - ---BOUNDARY -Content-Type: text/plain; charset="koi8-r" - - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_13.txt b/src/libs/python/bongo/external/email/test/data/msg_13.txt deleted file mode 100644 index 8e6d52d..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_13.txt +++ /dev/null @@ -1,94 +0,0 @@ -MIME-Version: 1.0 -From: Barry -To: Dingus Lovers -Subject: Here is your dingus fish -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="OUTER" - ---OUTER -Content-Type: text/plain; charset="us-ascii" - -A text/plain part - ---OUTER -Content-Type: multipart/mixed; boundary=BOUNDARY - - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - -Hi there, - -This is the dingus fish. - ---BOUNDARY -Content-Type: image/gif; name="dingusfish.gif" -Content-Transfer-Encoding: base64 -content-disposition: attachment; filename="dingusfish.gif" - -R0lGODdhAAEAAfAAAP///wAAACwAAAAAAAEAAQAC/oSPqcvtD6OctNqLs968+w+G4kiW5omm6sq2 -7gvH8kzX9o3n+s73/g8MCofEovGITGICTKbyCV0FDNOo9SqpQqpOrJfXzTQj2vD3TGtqL+NtGQ2f -qTXmxzuOd7WXdcc9DyjU53ewFni4s0fGhdiYaEhGBelICTNoV1j5NUnFcrmUqemjNifJVWpaOqaI -oFq3SspZsSraE7sHq3jr1MZqWvi662vxV4tD+pvKW6aLDOCLyur8PDwbanyDeq0N3DctbQYeLDvR -RY6t95m6UB0d3mwIrV7e2VGNvjjffukeJp4w7F65KecGFsTHQGAygOrgrWs1jt28Rc88KESYcGLA -/obvTkH6p+CinWJiJmIMqXGQwH/y4qk0SYjgQTczT3ajKZGfuI0uJ4kkVI/DT5s3/ejkxI0aT4Y+ -YTYgWbImUaXk9nlLmnSh1qJiJFl0OpUqRK4oOy7NyRQtHWofhoYVxkwWXKUSn0YsS+fUV6lhqfYb -6ayd3Z5qQdG1B7bvQzaJjwUV2lixMUZ7JVsOlfjWVr/3NB/uFvnySBN6Dcb6rGwaRM3wsormw5cC -M9NxWy/bWdufudCvy8bOAjXjVVwta/uO21sE5RHBCzNFXtgq9ORtH4eYjVP4Yryo026nvkFmCeyA -B29efV6ravCMK5JwWd5897Qrx7ll38o6iHDZ/rXPR//feevhF4l7wjUGX3xq1eeRfM4RSJGBIV1D -z1gKPkfWag3mVBVvva1RlX5bAJTPR/2YqNtw/FkIYYEi/pIZiAdpcxpoHtmnYYoZtvhUftzdx5ZX -JSKDW405zkGcZzzGZ6KEv4FI224oDmijlEf+xp6MJK5ojY/ASeVUR+wsKRuJ+XFZ5o7ZeEime8t1 -ouUsU6YjF5ZtUihhkGfCdFQLWQFJ3UXxmElfhQnR+eCdcDbkFZp6vTRmj56ApCihn5QGpaToNZmR -n3NVSpZcQpZ2KEONusaiCsKAug0wkQbJSFO+PTSjneGxOuFjPlUk3ovWvdIerjUg9ZGIOtGq/qeX -eCYrrCX+1UPsgTKGGRSbzd5q156d/gpfbJxe66eD5iQKrXj7RGgruGxs62qebBHUKS32CKluCiqZ -qh+pmehmEb71noAUoe5e9Zm17S7773V10pjrtG4CmuurCV/n6zLK5turWNhqOvFXbjhZrMD0YhKe -wR0zOyuvsh6MWrGoIuzvyWu5y1WIFAqmJselypxXh6dKLNOKEB98L88bS2rkNqqlKzCNJp9c0G0j -Gzh0iRrCbHSXmPR643QS+4rWhgFmnSbSuXCjS0xAOWkU2UdLqyuUNfHSFdUouy3bm5i5GnDM3tG8 -doJ4r5tqu3pPbRSVfvs8uJzeNXhp3n4j/tZ42SwH7eaWUUOjc3qFV9453UHTXZfcLH+OeNs5g36x -lBnHvTm7EbMbLeuaLncao8vWCXimfo1o+843Ak6y4ChNeGntvAYvfLK4ezmoyNIbNCLTCXO9ZV3A -E8/s88RczPzDwI4Ob7XZyl7+9Miban29h+tJZPrE21wgvBphDfrrfPdCTPKJD/y98L1rZwHcV6Jq -Zab0metpuNIX/qAFPoz171WUaUb4HAhBSzHuHfjzHb3kha/2Cctis/ORArVHNYfFyYRH2pYIRzic -isVOfPWD1b6mRTqpCRBozzof6UZVvFXRxWIr3GGrEviGYgyPMfahheiSaLs/9QeFu7oZ/ndSY8DD -ya9x+uPed+7mxN2IzIISBOMLFYWVqC3Pew1T2nFuuCiwZS5/v6II10i4t1OJcUH2U9zxKodHsGGv -Oa+zkvNUYUOa/TCCRutF9MzDwdlUMJADTCGSbDQ5OV4PTamDoPEi6Ecc/RF5RWwkcdSXvSOaDWSn -I9LlvubFTQpuc6JKXLcKeb+xdbKRBnwREemXyjg6ME65aJiOuBgrktzykfPLJBKR9ClMavJ62/Ff -BlNIyod9yX9wcSXexnXFpvkrbXk64xsx5Db7wXKP5fSgsvwIMM/9631VLBfkmtbHRXpqmtei52hG -pUwSlo+BASQoeILDOBgREECxBBh5/iYmNsQ9dIv5+OI++QkqdsJPc3uykz5fkM+OraeekcQF7X4n -B5S67za5U967PmooGQhUXfF7afXyCD7ONdRe17QogYjVx38uLwtrS6nhTnm15LQUnu9E2uK6CNI/ -1HOABj0ESwOjut4FEpFQpdNAm4K2LHnDWHNcmKB2ioKBogysVZtMO2nSxUdZ8Yk2kJc7URioLVI0 -YgmtIwZj4LoeKemgnOnbUdGnzZ4Oa6scqiolBGqS6RgWNLu0RMhcaE6rhhU4hiuqFXPAG8fGwTPW -FKeLMtdVmXLSs5YJGF/YeVm7rREMlY3UYE+yCxbaMXX8y15m5zVHq6GOKDMynzII/jdUHdyVqIy0 -ifX2+r/EgtZcvRzSb72gU9ui87M2VecjKildW/aFqaYhKoryUjfB/g4qtyVuc60xFDGmCxwjW+qu -zjuwl2GkOWn66+3QiiEctvd04OVvcCVzjgT7lrkvjVGKKHmmlDUKowSeikb5kK/mJReuWOxONx+s -ULsl+Lqb0CVn0SrVyJ6wt4t6yTeSCafhPhAf0OXn6L60UMxiLolFAtmN35S2Ob1lZpQ1r/n0Qb5D -oQ1zJiRVDgF8N3Q8TYfbi3DyWCy3lT1nxyBs6FT3S2GOzWRlxwKvlRP0RPJA9SjxEy0UoEnkA+M4 -cnzLMJrBGWLFEaaUb5lvpqbq/loOaU5+DFuHPxo82/OZuM8FXG3oVNZhtWpMpb/0Xu5m/LfLhHZQ -7yuVI0MqZ7NE43imC8jH3IwGZlbPm0xkJYs7+2U48hXTsFSMqgGDvai0kLxyynKNT/waj+q1c1tz -GjOpPBgdCSq3UKZxCSsqFIY+O6JbAWGWcV1pwqLyj5sGqCF1xb1F3varUWqrJv6cN3PrUXzijtfZ -FshpBL3Xwr4GIPvU2N8EjrJgS1zl21rbXQMXeXc5jjFyrhpCzijSv/RQtyPSzHCFMhlME95fHglt -pRsX+dfSQjUeHAlpWzJ5iOo79Ldnaxai6bXTcGO3fp07ri7HLEmXXPlYi8bv/qVxvNcdra6m7Rlb -6JBTb5fd66VhFRjGArh2n7R1rDW4P5NOT9K0I183T2scYkeZ3q/VFyLb09U9ajzXBS8Kgkhc4mBS -kYY9cy3Vy9lUnuNJH8HGIclUilwnBtjUOH0gteGOZ4c/XNrhXLSYDyxfnD8z1pDy7rYRvDolhnbe -UMzxCZUs40s6s7UIvBnLgc0+vKuOkIXeOrDymlp+Zxra4MZLBbVrqD/jTJ597pDmnw5c4+DbyB88 -9Cg9DodYcSuMZT/114pptqc/EuTjRPvH/z5slzI3tluOEBBLqOXLOX+0I5929tO97wkvl/atCz+y -xJrdwteW2FNW/NSmBP+f/maYtVs/bYyBC7Ox3jsYZHL05CIrBa/nS+b3bHfiYm4Ueil1YZZSgAUI -fFZ1dxUmeA2oQRQ3RuGXNGLFV9/XbGFGPV6kfzk1TBBCd+izc7q1H+OHMJwmaBX2IQNYVAKHYepV -SSGCe6CnbYHHETKGNe43EDvFgZr0gB/nVHPHZ80VV1ojOiI3XDvYIkl4ayo4bxQIgrFXWTvBI0nH -VElWMuw2aLUWCRHHf8ymVCHjFlJnOSojfevCYyyyZDH0IcvHhrsnQ5O1OsWzONuVVKIxSxiFZ/tR -fKDAf6xFTnw4O9Qig2VCfW2hJQrmMOuHW0W3dLQmCMO2ccdUd/xyfflH/olTiHZVdGwb8nIwRzSE -J15jFlOJuBZBZ4CiyHyd2IFylFlB+HgHhYabhWOGwYO1ZH/Og1dtQlFMk352CGRSIFTapnWQEUtN -l4zv8S0aaCFDyGCBqDUxZYpxGHX01y/JuH1xhn7TOCnNCI4eKDs5WGX4R425F4vF1o3BJ4vO0otq -I3rimI7jJY1jISqnBxknCIvruF83mF5wN4X7qGLIhR8A2Vg0yFERSIXn9Vv3GHy3Vj/WIkKddlYi -yIMv2I/VMjTLpW7pt05SWIZR0RPyxpB4SIUM9lBPGBl0GC7oSEEwRYLe4pJpZY2P0zbI1n+Oc44w -qY3PUnmF0ixjVpDD/mJ9wpOBGTVgXlaCaZiPcIWK5NiKBIiPdGaQ0TWGvAiG7nMchdZb7Vgf8zNi -MuMyzRdy/lePe9iC4TRx7WhhOQI/QiSVNAmAa2lT/piFbuh7ofJoYSZzrSZ1bvmWw3eN2nKUPVky -uPN5/VRfohRd0VYZoqhKIlU6TXYhJxmPUIloAwc1bPmHEpaZYZORHNlXUJM07hATwHR8MJYqkwWR -WaIezFhxSFlc8/Fq82hEnpeRozg3ULhhr9lAGtVEkCg5ZNRuuVleBPaZadhG0ZgkyPmDOTOKzViM -YgOcpukKqQcbjAWS0IleQ2ROjdh6A+md1qWdBRSX7iSYgFRTtRmBpJioieXJiHfJiMGIR9fJOn8I -MSfXYhspn4ooSa2mSAj4n+8Bmg03fBJZoPOJgsVZRxu1oOMRPXYYjdqjihFaEoZpXBREanuJoRI6 -cibFinq4ngUKh/wQd/H5ofYCZ0HJXR62opZFaAT0iFIZo4DIiUojkjeqKiuoZirKo5Y1a7AWckGa -BkuYoD5lpDK6eUs6CkDqpETwl1EqpfhJpVeKpVl6EgUAADs= - ---BOUNDARY-- - ---OUTER-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_14.txt b/src/libs/python/bongo/external/email/test/data/msg_14.txt deleted file mode 100644 index 5d98d2f..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_14.txt +++ /dev/null @@ -1,23 +0,0 @@ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) - id 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Type: text; charset=us-ascii -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 - - -Hi, - -I'm sorry but I'm using a drainbread ISP, which although big and -wealthy can't seem to generate standard compliant email. :( - -This message has a Content-Type: header with no subtype. I hope you -can still read it. - --Me diff --git a/src/libs/python/bongo/external/email/test/data/msg_15.txt b/src/libs/python/bongo/external/email/test/data/msg_15.txt deleted file mode 100644 index 33b8487..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_15.txt +++ /dev/null @@ -1,52 +0,0 @@ -Return-Path: -Received: from fepD.post.tele.dk (195.41.46.149) by mail.groupcare.dk (LSMTP for Windows NT v1.1b) with SMTP id <0.0014F8A2@mail.groupcare.dk>; Mon, 30 Apr 2001 12:17:50 +0200 -User-Agent: Microsoft-Outlook-Express-Macintosh-Edition/5.02.2106 -Subject: XX -From: xx@xx.dk -To: XX -Message-ID: -Mime-version: 1.0 -Content-type: multipart/mixed; - boundary="MS_Mac_OE_3071477847_720252_MIME_Part" - -> Denne meddelelse er i MIME-format. Da dit postl¾sningsprogram ikke forstŒr dette format, kan del af eller hele meddelelsen v¾re ul¾selig. - ---MS_Mac_OE_3071477847_720252_MIME_Part -Content-type: multipart/alternative; - boundary="MS_Mac_OE_3071477847_720252_MIME_Part" - - ---MS_Mac_OE_3071477847_720252_MIME_Part -Content-type: text/plain; charset="ISO-8859-1" -Content-transfer-encoding: quoted-printable - -Some removed test. - ---MS_Mac_OE_3071477847_720252_MIME_Part -Content-type: text/html; charset="ISO-8859-1" -Content-transfer-encoding: quoted-printable - - - -Some removed HTML - - -Some removed text. - - - - ---MS_Mac_OE_3071477847_720252_MIME_Part-- - - ---MS_Mac_OE_3071477847_720252_MIME_Part -Content-type: image/gif; name="xx.gif"; - x-mac-creator="6F676C65"; - x-mac-type="47494666" -Content-disposition: attachment -Content-transfer-encoding: base64 - -Some removed base64 encoded chars. - ---MS_Mac_OE_3071477847_720252_MIME_Part-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_16.txt b/src/libs/python/bongo/external/email/test/data/msg_16.txt deleted file mode 100644 index 56167e9..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_16.txt +++ /dev/null @@ -1,123 +0,0 @@ -Return-Path: <> -Delivered-To: scr-admin@socal-raves.org -Received: from cougar.noc.ucla.edu (cougar.noc.ucla.edu [169.232.10.18]) - by babylon.socal-raves.org (Postfix) with ESMTP id CCC2C51B84 - for ; Sun, 23 Sep 2001 20:13:54 -0700 (PDT) -Received: from sims-ms-daemon by cougar.noc.ucla.edu - (Sun Internet Mail Server sims.3.5.2000.03.23.18.03.p10) - id <0GK500B01D0B8Y@cougar.noc.ucla.edu> for scr-admin@socal-raves.org; Sun, - 23 Sep 2001 20:14:35 -0700 (PDT) -Received: from cougar.noc.ucla.edu - (Sun Internet Mail Server sims.3.5.2000.03.23.18.03.p10) - id <0GK500B01D0B8X@cougar.noc.ucla.edu>; Sun, 23 Sep 2001 20:14:35 -0700 (PDT) -Date: Sun, 23 Sep 2001 20:14:35 -0700 (PDT) -From: Internet Mail Delivery -Subject: Delivery Notification: Delivery has failed -To: scr-admin@socal-raves.org -Message-id: <0GK500B04D0B8X@cougar.noc.ucla.edu> -MIME-version: 1.0 -Sender: scr-owner@socal-raves.org -Errors-To: scr-owner@socal-raves.org -X-BeenThere: scr@socal-raves.org -X-Mailman-Version: 2.1a3 -Precedence: bulk -List-Help: -List-Post: -List-Subscribe: , - -List-Id: SoCal-Raves -List-Unsubscribe: , - -List-Archive: -Content-Type: multipart/report; boundary="Boundary_(ID_PGS2F2a+z+/jL7hupKgRhA)" - - ---Boundary_(ID_PGS2F2a+z+/jL7hupKgRhA) -Content-type: text/plain; charset=ISO-8859-1 - -This report relates to a message you sent with the following header fields: - - Message-id: <002001c144a6$8752e060$56104586@oxy.edu> - Date: Sun, 23 Sep 2001 20:10:55 -0700 - From: "Ian T. Henry" - To: SoCal Raves - Subject: [scr] yeah for Ians!! - -Your message cannot be delivered to the following recipients: - - Recipient address: jangel1@cougar.noc.ucla.edu - Reason: recipient reached disk quota - - ---Boundary_(ID_PGS2F2a+z+/jL7hupKgRhA) -Content-type: message/DELIVERY-STATUS - -Original-envelope-id: 0GK500B4HD0888@cougar.noc.ucla.edu -Reporting-MTA: dns; cougar.noc.ucla.edu - -Action: failed -Status: 5.0.0 (recipient reached disk quota) -Original-recipient: rfc822;jangel1@cougar.noc.ucla.edu -Final-recipient: rfc822;jangel1@cougar.noc.ucla.edu - ---Boundary_(ID_PGS2F2a+z+/jL7hupKgRhA) -Content-type: MESSAGE/RFC822 - -Return-path: scr-admin@socal-raves.org -Received: from sims-ms-daemon by cougar.noc.ucla.edu - (Sun Internet Mail Server sims.3.5.2000.03.23.18.03.p10) - id <0GK500B01D0B8X@cougar.noc.ucla.edu>; Sun, 23 Sep 2001 20:14:35 -0700 (PDT) -Received: from panther.noc.ucla.edu by cougar.noc.ucla.edu - (Sun Internet Mail Server sims.3.5.2000.03.23.18.03.p10) - with ESMTP id <0GK500B4GD0888@cougar.noc.ucla.edu> for jangel1@sims-ms-daemon; - Sun, 23 Sep 2001 20:14:33 -0700 (PDT) -Received: from babylon.socal-raves.org - (ip-209-85-222-117.dreamhost.com [209.85.222.117]) - by panther.noc.ucla.edu (8.9.1a/8.9.1) with ESMTP id UAA09793 for - ; Sun, 23 Sep 2001 20:14:32 -0700 (PDT) -Received: from babylon (localhost [127.0.0.1]) by babylon.socal-raves.org - (Postfix) with ESMTP id D3B2951B70; Sun, 23 Sep 2001 20:13:47 -0700 (PDT) -Received: by babylon.socal-raves.org (Postfix, from userid 60001) - id A611F51B82; Sun, 23 Sep 2001 20:13:46 -0700 (PDT) -Received: from tiger.cc.oxy.edu (tiger.cc.oxy.edu [134.69.3.112]) - by babylon.socal-raves.org (Postfix) with ESMTP id ADA7351B70 for - ; Sun, 23 Sep 2001 20:13:44 -0700 (PDT) -Received: from ent (n16h86.dhcp.oxy.edu [134.69.16.86]) - by tiger.cc.oxy.edu (8.8.8/8.8.8) with SMTP id UAA08100 for - ; Sun, 23 Sep 2001 20:14:24 -0700 (PDT) -Date: Sun, 23 Sep 2001 20:10:55 -0700 -From: "Ian T. Henry" -Subject: [scr] yeah for Ians!! -Sender: scr-admin@socal-raves.org -To: SoCal Raves -Errors-to: scr-admin@socal-raves.org -Message-id: <002001c144a6$8752e060$56104586@oxy.edu> -MIME-version: 1.0 -X-Mailer: Microsoft Outlook Express 5.50.4522.1200 -Content-type: text/plain; charset=us-ascii -Precedence: bulk -Delivered-to: scr-post@babylon.socal-raves.org -Delivered-to: scr@socal-raves.org -X-Converted-To-Plain-Text: from multipart/alternative by demime 0.98e -X-Converted-To-Plain-Text: Alternative section used was text/plain -X-BeenThere: scr@socal-raves.org -X-Mailman-Version: 2.1a3 -List-Help: -List-Post: -List-Subscribe: , - -List-Id: SoCal-Raves -List-Unsubscribe: , - -List-Archive: - -I always love to find more Ian's that are over 3 years old!! - -Ian -_______________________________________________ -For event info, list questions, or to unsubscribe, see http://www.socal-raves.org/ - - - ---Boundary_(ID_PGS2F2a+z+/jL7hupKgRhA)-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_17.txt b/src/libs/python/bongo/external/email/test/data/msg_17.txt deleted file mode 100644 index 8d86e41..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_17.txt +++ /dev/null @@ -1,12 +0,0 @@ -MIME-Version: 1.0 -From: Barry -To: Dingus Lovers -Subject: Here is your dingus fish -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - -Hi there, - -This is the dingus fish. - -[Non-text (image/gif) part of message omitted, filename dingusfish.gif] diff --git a/src/libs/python/bongo/external/email/test/data/msg_19.txt b/src/libs/python/bongo/external/email/test/data/msg_19.txt deleted file mode 100644 index 49bf7fc..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_19.txt +++ /dev/null @@ -1,43 +0,0 @@ -Send Ppp mailing list submissions to - ppp@zzz.org - -To subscribe or unsubscribe via the World Wide Web, visit - http://www.zzz.org/mailman/listinfo/ppp -or, via email, send a message with subject or body 'help' to - ppp-request@zzz.org - -You can reach the person managing the list at - ppp-admin@zzz.org - -When replying, please edit your Subject line so it is more specific -than "Re: Contents of Ppp digest..." - -Today's Topics: - - 1. testing #1 (Barry A. Warsaw) - 2. testing #2 (Barry A. Warsaw) - 3. testing #3 (Barry A. Warsaw) - 4. testing #4 (Barry A. Warsaw) - 5. testing #5 (Barry A. Warsaw) - -hello - - -hello - - -hello - - -hello - - -hello - - - -_______________________________________________ -Ppp mailing list -Ppp@zzz.org -http://www.zzz.org/mailman/listinfo/ppp - diff --git a/src/libs/python/bongo/external/email/test/data/msg_20.txt b/src/libs/python/bongo/external/email/test/data/msg_20.txt deleted file mode 100644 index 1a6a887..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_20.txt +++ /dev/null @@ -1,22 +0,0 @@ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) - id 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Cc: ccc@zzz.org -CC: ddd@zzz.org -cc: eee@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 - - -Hi, - -Do you like this message? - --Me diff --git a/src/libs/python/bongo/external/email/test/data/msg_21.txt b/src/libs/python/bongo/external/email/test/data/msg_21.txt deleted file mode 100644 index 23590b2..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_21.txt +++ /dev/null @@ -1,20 +0,0 @@ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: Test -Content-Type: multipart/mixed; boundary="BOUNDARY" - -MIME message ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -One ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -Two ---BOUNDARY-- -End of MIME message diff --git a/src/libs/python/bongo/external/email/test/data/msg_22.txt b/src/libs/python/bongo/external/email/test/data/msg_22.txt deleted file mode 100644 index af9de5f..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_22.txt +++ /dev/null @@ -1,46 +0,0 @@ -Mime-Version: 1.0 -Message-Id: -Date: Tue, 16 Oct 2001 13:59:25 +0300 -To: a@example.com -From: b@example.com -Content-Type: multipart/mixed; boundary="============_-1208892523==_============" - ---============_-1208892523==_============ -Content-Type: text/plain; charset="us-ascii" ; format="flowed" - -Text text text. ---============_-1208892523==_============ -Content-Id: -Content-Type: image/jpeg; name="wibble.JPG" - ; x-mac-type="4A504547" - ; x-mac-creator="474B4F4E" -Content-Disposition: attachment; filename="wibble.JPG" -Content-Transfer-Encoding: base64 - -/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB -AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAALCAXABIEBAREA -g6bCjjw/pIZSjO6FWFpldjySOmCNrO7DBZibUXhTwtCixw+GtAijVdqxxaPp0aKvmGXa -qrbBQvms0mAMeYS/3iTV1dG0hHaRNK01XblnWxtVdjkHLMIgTyqnk9VB7CrP2KzIINpa -4O7I+zxYO9WV8jZg71Zlb+8rMDkEirAVQFAUAKAFAAAUAYAUDgADgY6DjpRtXj5RxjHA -4wQRj0wQCMdCAewpaKKK/9k= ---============_-1208892523==_============ -Content-Id: -Content-Type: image/jpeg; name="wibble2.JPG" - ; x-mac-type="4A504547" - ; x-mac-creator="474B4F4E" -Content-Disposition: attachment; filename="wibble2.JPG" -Content-Transfer-Encoding: base64 - -/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB -AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAALCAXABJ0BAREA -/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA -W6NFJJBEkU10kKGTcWMDwxuU+0JHvk8qAtOpNwqSR0n8c3BlDyXHlqsUltHEiTvdXLxR -7vMiGDNJAJWkAMk8ZkCFp5G2oo5W++INrbQtNfTQxJAuXlupz9oS4d5Y1W+E2XlWZJJE -Y7LWYQxTLE1zuMbfBPxw8X2fibVdIbSbI6nLZxX635t9TjtYreWR7WGKJTLJFFKSlozO -0ShxIXM43uC3/9k= ---============_-1208892523==_============ -Content-Type: text/plain; charset="us-ascii" ; format="flowed" - -Text text text. ---============_-1208892523==_============-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_23.txt b/src/libs/python/bongo/external/email/test/data/msg_23.txt deleted file mode 100644 index bb2e8ec..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_23.txt +++ /dev/null @@ -1,8 +0,0 @@ -From: aperson@dom.ain -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain - -A message part ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_24.txt b/src/libs/python/bongo/external/email/test/data/msg_24.txt deleted file mode 100644 index 4e52339..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_24.txt +++ /dev/null @@ -1,10 +0,0 @@ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY - - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_25.txt b/src/libs/python/bongo/external/email/test/data/msg_25.txt deleted file mode 100644 index 9e35275..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_25.txt +++ /dev/null @@ -1,117 +0,0 @@ -From MAILER-DAEMON Fri Apr 06 16:46:09 2001 -Received: from [204.245.199.98] (helo=zinfandel.lacita.com) - by www.linux.org.uk with esmtp (Exim 3.13 #1) - id 14lYR6-0008Iv-00 - for linuxuser-admin@www.linux.org.uk; Fri, 06 Apr 2001 16:46:09 +0100 -Received: from localhost (localhost) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with internal id JAB03225; Fri, 6 Apr 2001 09:23:06 -0800 (GMT-0800) -Date: Fri, 6 Apr 2001 09:23:06 -0800 (GMT-0800) -From: Mail Delivery Subsystem -Subject: Returned mail: Too many hops 19 (17 max): from via [199.164.235.226], to -Message-Id: <200104061723.JAB03225@zinfandel.lacita.com> -To: -To: postmaster@zinfandel.lacita.com -MIME-Version: 1.0 -Content-Type: multipart/report; report-type=delivery-status; - bo -Auto-Submitted: auto-generated (failure) - -This is a MIME-encapsulated message - ---JAB03225.986577786/zinfandel.lacita.com - -The original message was received at Fri, 6 Apr 2001 09:23:03 -0800 (GMT-0800) -from [199.164.235.226] - - ----- The following addresses have delivery notifications ----- - (unrecoverable error) - - ----- Transcript of session follows ----- -554 Too many hops 19 (17 max): from via [199.164.235.226], to - ---JAB03225.986577786/zinfandel.lacita.com -Content-Type: message/delivery-status - -Reporting-MTA: dns; zinfandel.lacita.com -Received-From-MTA: dns; [199.164.235.226] -Arrival-Date: Fri, 6 Apr 2001 09:23:03 -0800 (GMT-0800) - -Final-Recipient: rfc822; scoffman@wellpartner.com -Action: failed -Status: 5.4.6 -Last-Attempt-Date: Fri, 6 Apr 2001 09:23:06 -0800 (GMT-0800) - ---JAB03225.986577786/zinfandel.lacita.com -Content-Type: text/rfc822-headers - -Return-Path: linuxuser-admin@www.linux.org.uk -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03225 for ; Fri, 6 Apr 2001 09:23:03 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03221 for ; Fri, 6 Apr 2001 09:22:18 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03217 for ; Fri, 6 Apr 2001 09:21:37 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03213 for ; Fri, 6 Apr 2001 09:20:56 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03209 for ; Fri, 6 Apr 2001 09:20:15 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03205 for ; Fri, 6 Apr 2001 09:19:33 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03201 for ; Fri, 6 Apr 2001 09:18:52 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03197 for ; Fri, 6 Apr 2001 09:17:54 -0800 (GMT-0800) -Received: from www.linux.org.uk (parcelfarce.linux.theplanet.co.uk [195.92.249.252]) - by - fo -Received: from localhost.localdomain - ([ - by - id -Received: from [212.1.130.11] (helo=s1.uklinux.net ident=root) - by - id - fo -Received: from server (ppp-2-22.cvx4.telinco.net [212.1.149.22]) - by - fo -From: Daniel James -Organization: LinuxUser -To: linuxuser@www.linux.org.uk -X-Mailer: KMail [version 1.1.99] -Content-Type: text/plain; - c -MIME-Version: 1.0 -Message-Id: <01040616033903.00962@server> -Content-Transfer-Encoding: 8bit -Subject: [LinuxUser] bulletin no. 45 -Sender: linuxuser-admin@www.linux.org.uk -Errors-To: linuxuser-admin@www.linux.org.uk -X-BeenThere: linuxuser@www.linux.org.uk -X-Mailman-Version: 2.0.3 -Precedence: bulk -List-Help: -List-Post: -List-Subscribe: , - -List-Unsubscribe: , - -Date: Fri, 6 Apr 2001 16:03:39 +0100 - ---JAB03225.986577786/zinfandel.lacita.com-- - - diff --git a/src/libs/python/bongo/external/email/test/data/msg_26.txt b/src/libs/python/bongo/external/email/test/data/msg_26.txt deleted file mode 100644 index 6c71bce..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_26.txt +++ /dev/null @@ -1,45 +0,0 @@ -Received: from xcar [192.168.0.2] by jeeves.wooster.local - (SMTPD32-7.07 EVAL) id AFF92F0214; Sun, 12 May 2002 08:55:37 +0100 -Date: Sun, 12 May 2002 08:56:15 +0100 -From: Father Time -To: timbo@jeeves.wooster.local -Subject: IMAP file test -Message-ID: <6df65d354b.father.time@rpc.wooster.local> -X-Organization: Home -User-Agent: Messenger-Pro/2.50a (MsgServe/1.50) (RISC-OS/4.02) POPstar/2.03 -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="1618492860--2051301190--113853680" -Status: R -X-UIDL: 319998302 - -This message is in MIME format which your mailer apparently does not support. -You either require a newer version of your software which supports MIME, or -a separate MIME decoding utility. Alternatively, ask the sender of this -message to resend it in a different format. - ---1618492860--2051301190--113853680 -Content-Type: text/plain; charset=us-ascii - -Simple email with attachment. - - ---1618492860--2051301190--113853680 -Content-Type: application/riscos; name="clock.bmp,69c"; type=BMP; load=&fff69c4b; exec=&355dd4d1; access=&03 -Content-Disposition: attachment; filename="clock.bmp" -Content-Transfer-Encoding: base64 - -Qk12AgAAAAAAAHYAAAAoAAAAIAAAACAAAAABAAQAAAAAAAAAAADXDQAA1w0AAAAAAAAA -AAAAAAAAAAAAiAAAiAAAAIiIAIgAAACIAIgAiIgAALu7uwCIiIgAERHdACLuIgAz//8A -zAAAAN0R3QDu7iIA////AAAAAAAAAAAAAAAAAAAAAAAAAAi3AAAAAAAAADeAAAAAAAAA -C3ADMzMzMANwAAAAAAAAAAAHMAAAAANwAAAAAAAAAACAMAd3zPfwAwgAAAAAAAAIAwd/ -f8x/f3AwgAAAAAAAgDB0x/f3//zPAwgAAAAAAAcHfM9////8z/AwAAAAAAiwd/f3//// -////A4AAAAAAcEx/f///////zAMAAAAAiwfM9////3///8zwOAAAAAcHf3////B///// -8DAAAAALB/f3///wd3d3//AwAAAABwTPf//wCQAAD/zAMAAAAAsEx/f///B////8wDAA -AAAHB39////wf/////AwAAAACwf39///8H/////wMAAAAIcHfM9///B////M8DgAAAAA -sHTH///wf///xAMAAAAACHB3f3//8H////cDgAAAAAALB3zH//D//M9wMAAAAAAAgLB0 -z39///xHAwgAAAAAAAgLB3d3RHd3cDCAAAAAAAAAgLAHd0R3cAMIAAAAAAAAgAgLcAAA -AAMwgAgAAAAACDAAAAu7t7cwAAgDgAAAAABzcIAAAAAAAAgDMwAAAAAAN7uwgAAAAAgH -MzMAAAAACH97tzAAAAALu3c3gAAAAAAL+7tzDABAu7f7cAAAAAAACA+3MA7EQAv/sIAA -AAAAAAAIAAAAAAAAAIAAAAAA - ---1618492860--2051301190--113853680-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_27.txt b/src/libs/python/bongo/external/email/test/data/msg_27.txt deleted file mode 100644 index d019176..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_27.txt +++ /dev/null @@ -1,15 +0,0 @@ -Return-Path: -Received: by mail.dom.ain (Postfix, from userid 889) - id B9D0AD35DB; Tue, 4 Jun 2002 21:46:59 -0400 (EDT) -Message-ID: <15613.28051.707126.569693@dom.ain> -Date: Tue, 4 Jun 2002 21:46:59 -0400 -MIME-Version: 1.0 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -Subject: bug demonstration - 12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 - more text -From: aperson@dom.ain (Anne P. Erson) -To: bperson@dom.ain (Barney P. Erson) - -test diff --git a/src/libs/python/bongo/external/email/test/data/msg_28.txt b/src/libs/python/bongo/external/email/test/data/msg_28.txt deleted file mode 100644 index 1e4824c..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_28.txt +++ /dev/null @@ -1,25 +0,0 @@ -From: aperson@dom.ain -MIME-Version: 1.0 -Content-Type: multipart/digest; boundary=BOUNDARY - ---BOUNDARY -Content-Type: message/rfc822 - -Content-Type: text/plain; charset=us-ascii -To: aa@bb.org -From: cc@dd.org -Subject: ee - -message 1 - ---BOUNDARY -Content-Type: message/rfc822 - -Content-Type: text/plain; charset=us-ascii -To: aa@bb.org -From: cc@dd.org -Subject: ee - -message 2 - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_29.txt b/src/libs/python/bongo/external/email/test/data/msg_29.txt deleted file mode 100644 index 1fab561..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_29.txt +++ /dev/null @@ -1,22 +0,0 @@ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) - id 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Type: text/plain; charset=us-ascii; - title*0*="us-ascii'en'This%20is%20even%20more%20"; - title*1*="%2A%2A%2Afun%2A%2A%2A%20"; - title*2="isn't it!" -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 - - -Hi, - -Do you like this message? - --Me diff --git a/src/libs/python/bongo/external/email/test/data/msg_30.txt b/src/libs/python/bongo/external/email/test/data/msg_30.txt deleted file mode 100644 index 4334bb6..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_30.txt +++ /dev/null @@ -1,23 +0,0 @@ -From: aperson@dom.ain -MIME-Version: 1.0 -Content-Type: multipart/digest; boundary=BOUNDARY - ---BOUNDARY - -Content-Type: text/plain; charset=us-ascii -To: aa@bb.org -From: cc@dd.org -Subject: ee - -message 1 - ---BOUNDARY - -Content-Type: text/plain; charset=us-ascii -To: aa@bb.org -From: cc@dd.org -Subject: ee - -message 2 - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_31.txt b/src/libs/python/bongo/external/email/test/data/msg_31.txt deleted file mode 100644 index 1e58e56..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_31.txt +++ /dev/null @@ -1,15 +0,0 @@ -From: aperson@dom.ain -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary=BOUNDARY_ - ---BOUNDARY -Content-Type: text/plain - -message 1 - ---BOUNDARY -Content-Type: text/plain - -message 2 - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_32.txt b/src/libs/python/bongo/external/email/test/data/msg_32.txt deleted file mode 100644 index 07ec5af..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_32.txt +++ /dev/null @@ -1,14 +0,0 @@ -Delivered-To: freebsd-isp@freebsd.org -Date: Tue, 26 Sep 2000 12:23:03 -0500 -From: Anne Person -To: Barney Dude -Subject: Re: Limiting Perl CPU Utilization... -Mime-Version: 1.0 -Content-Type: text/plain; charset*=ansi-x3.4-1968''us-ascii -Content-Disposition: inline -User-Agent: Mutt/1.3.8i -Sender: owner-freebsd-isp@FreeBSD.ORG -Precedence: bulk -X-Loop: FreeBSD.org - -Some message. diff --git a/src/libs/python/bongo/external/email/test/data/msg_33.txt b/src/libs/python/bongo/external/email/test/data/msg_33.txt deleted file mode 100644 index 042787a..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_33.txt +++ /dev/null @@ -1,29 +0,0 @@ -Delivered-To: freebsd-isp@freebsd.org -Date: Wed, 27 Sep 2000 11:11:09 -0500 -From: Anne Person -To: Barney Dude -Subject: Re: Limiting Perl CPU Utilization... -Mime-Version: 1.0 -Content-Type: multipart/signed; micalg*=ansi-x3.4-1968''pgp-md5; - protocol*=ansi-x3.4-1968''application%2Fpgp-signature; - boundary*="ansi-x3.4-1968''EeQfGwPcQSOJBaQU" -Content-Disposition: inline -Sender: owner-freebsd-isp@FreeBSD.ORG -Precedence: bulk -X-Loop: FreeBSD.org - - ---EeQfGwPcQSOJBaQU -Content-Type: text/plain; charset*=ansi-x3.4-1968''us-ascii -Content-Disposition: inline -Content-Transfer-Encoding: quoted-printable - -part 1 - ---EeQfGwPcQSOJBaQU -Content-Type: text/plain -Content-Disposition: inline - -part 2 - ---EeQfGwPcQSOJBaQU-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_34.txt b/src/libs/python/bongo/external/email/test/data/msg_34.txt deleted file mode 100644 index 055dfea..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_34.txt +++ /dev/null @@ -1,19 +0,0 @@ -From: aperson@dom.ain -To: bperson@dom.ain -Content-Type: multipart/digest; boundary=XYZ - ---XYZ -Content-Type: text/plain - - -This is a text plain part that is counter to recommended practice in -RFC 2046, $5.1.5, but is not illegal - ---XYZ - -From: cperson@dom.ain -To: dperson@dom.ain - -A submessage - ---XYZ-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_35.txt b/src/libs/python/bongo/external/email/test/data/msg_35.txt deleted file mode 100644 index be7d5a2..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_35.txt +++ /dev/null @@ -1,4 +0,0 @@ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: here's something interesting -counter to RFC 2822, there's no separating newline here diff --git a/src/libs/python/bongo/external/email/test/data/msg_36.txt b/src/libs/python/bongo/external/email/test/data/msg_36.txt deleted file mode 100644 index 5632c30..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_36.txt +++ /dev/null @@ -1,40 +0,0 @@ -Mime-Version: 1.0 -Content-Type: Multipart/Mixed; Boundary="NextPart" -To: IETF-Announce:; -From: Internet-Drafts@ietf.org -Subject: I-D ACTION:draft-ietf-mboned-mix-00.txt -Date: Tue, 22 Dec 1998 16:55:06 -0500 - ---NextPart - -Blah blah blah - ---NextPart -Content-Type: Multipart/Alternative; Boundary="OtherAccess" - ---OtherAccess -Content-Type: Message/External-body; - access-type="mail-server"; - server="mailserv@ietf.org" - -Content-Type: text/plain -Content-ID: <19981222151406.I-D@ietf.org> - -ENCODING mime -FILE /internet-drafts/draft-ietf-mboned-mix-00.txt - ---OtherAccess -Content-Type: Message/External-body; - name="draft-ietf-mboned-mix-00.txt"; - site="ftp.ietf.org"; - access-type="anon-ftp"; - directory="internet-drafts" - -Content-Type: text/plain -Content-ID: <19981222151406.I-D@ietf.org> - - ---OtherAccess-- - ---NextPart-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_37.txt b/src/libs/python/bongo/external/email/test/data/msg_37.txt deleted file mode 100644 index 038d34a..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_37.txt +++ /dev/null @@ -1,22 +0,0 @@ -Content-Type: multipart/mixed; boundary=ABCDE - ---ABCDE -Content-Type: text/x-one - -Blah - ---ABCDE ---ABCDE -Content-Type: text/x-two - -Blah - ---ABCDE ---ABCDE ---ABCDE ---ABCDE -Content-Type: text/x-two - -Blah - ---ABCDE-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_38.txt b/src/libs/python/bongo/external/email/test/data/msg_38.txt deleted file mode 100644 index 006df81..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_38.txt +++ /dev/null @@ -1,101 +0,0 @@ -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0" - -------- =_aaaaaaaaaa0 -Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa1" -Content-ID: <20592.1022586929.1@example.com> - -------- =_aaaaaaaaaa1 -Content-Type: multipart/alternative; boundary="----- =_aaaaaaaaaa2" -Content-ID: <20592.1022586929.2@example.com> - -------- =_aaaaaaaaaa2 -Content-Type: text/plain -Content-ID: <20592.1022586929.3@example.com> -Content-Description: very tricky -Content-Transfer-Encoding: 7bit - - -Unlike the test test_nested-multiples-with-internal-boundary, this -piece of text not only contains the outer boundary tags -------- =_aaaaaaaaaa1 -and -------- =_aaaaaaaaaa0 -but puts them at the start of a line! And, to be even nastier, it -even includes a couple of end tags, such as this one: - -------- =_aaaaaaaaaa1-- - -and this one, which is from a multipart we haven't even seen yet! - -------- =_aaaaaaaaaa4-- - -This will, I'm sure, cause much breakage of MIME parsers. But, as -far as I can tell, it's perfectly legal. I have not yet ever seen -a case of this in the wild, but I've seen *similar* things. - - -------- =_aaaaaaaaaa2 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.4@example.com> -Content-Description: patch2 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa2-- - -------- =_aaaaaaaaaa1 -Content-Type: multipart/alternative; boundary="----- =_aaaaaaaaaa3" -Content-ID: <20592.1022586929.6@example.com> - -------- =_aaaaaaaaaa3 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.7@example.com> -Content-Description: patch3 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa3 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.8@example.com> -Content-Description: patch4 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa3-- - -------- =_aaaaaaaaaa1 -Content-Type: multipart/alternative; boundary="----- =_aaaaaaaaaa4" -Content-ID: <20592.1022586929.10@example.com> - -------- =_aaaaaaaaaa4 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.11@example.com> -Content-Description: patch5 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa4 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.12@example.com> -Content-Description: patch6 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa4-- - -------- =_aaaaaaaaaa1-- - -------- =_aaaaaaaaaa0 -Content-Type: text/plain; charset="us-ascii" -Content-ID: <20592.1022586929.15@example.com> - --- -It's never too late to have a happy childhood. - -------- =_aaaaaaaaaa0-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_39.txt b/src/libs/python/bongo/external/email/test/data/msg_39.txt deleted file mode 100644 index 124b269..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_39.txt +++ /dev/null @@ -1,83 +0,0 @@ -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0" - -------- =_aaaaaaaaaa0 -Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa1" -Content-ID: <20592.1022586929.1@example.com> - -------- =_aaaaaaaaaa1 -Content-Type: multipart/alternative; boundary="----- =_aaaaaaaaaa1" -Content-ID: <20592.1022586929.2@example.com> - -------- =_aaaaaaaaaa1 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.3@example.com> -Content-Description: patch1 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa1 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.4@example.com> -Content-Description: patch2 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa1-- - -------- =_aaaaaaaaaa1 -Content-Type: multipart/alternative; boundary="----- =_aaaaaaaaaa1" -Content-ID: <20592.1022586929.6@example.com> - -------- =_aaaaaaaaaa1 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.7@example.com> -Content-Description: patch3 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa1 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.8@example.com> -Content-Description: patch4 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa1-- - -------- =_aaaaaaaaaa1 -Content-Type: multipart/alternative; boundary="----- =_aaaaaaaaaa1" -Content-ID: <20592.1022586929.10@example.com> - -------- =_aaaaaaaaaa1 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.11@example.com> -Content-Description: patch5 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa1 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.12@example.com> -Content-Description: patch6 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa1-- - -------- =_aaaaaaaaaa1-- - -------- =_aaaaaaaaaa0 -Content-Type: text/plain; charset="us-ascii" -Content-ID: <20592.1022586929.15@example.com> - --- -It's never too late to have a happy childhood. - -------- =_aaaaaaaaaa0-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_40.txt b/src/libs/python/bongo/external/email/test/data/msg_40.txt deleted file mode 100644 index 1435fa1..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_40.txt +++ /dev/null @@ -1,10 +0,0 @@ -MIME-Version: 1.0 -Content-Type: text/html; boundary="--961284236552522269" - -----961284236552522269 -Content-Type: text/html; -Content-Transfer-Encoding: 7Bit - - - -----961284236552522269-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_41.txt b/src/libs/python/bongo/external/email/test/data/msg_41.txt deleted file mode 100644 index 76cdd1c..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_41.txt +++ /dev/null @@ -1,8 +0,0 @@ -From: "Allison Dunlap" -To: yyy@example.com -Subject: 64423 -Date: Sun, 11 Jul 2004 16:09:27 -0300 -MIME-Version: 1.0 -Content-Type: multipart/alternative; - -Blah blah blah diff --git a/src/libs/python/bongo/external/email/test/data/msg_42.txt b/src/libs/python/bongo/external/email/test/data/msg_42.txt deleted file mode 100644 index a75f8f4..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_42.txt +++ /dev/null @@ -1,20 +0,0 @@ -Content-Type: multipart/mixed; boundary="AAA" -From: Mail Delivery Subsystem -To: yyy@example.com - -This is a MIME-encapsulated message - ---AAA - -Stuff - ---AAA -Content-Type: message/rfc822 - -From: webmaster@python.org -To: zzz@example.com -Content-Type: multipart/mixed; boundary="BBB" - ---BBB-- - ---AAA-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_43.txt b/src/libs/python/bongo/external/email/test/data/msg_43.txt deleted file mode 100644 index 797d12c..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_43.txt +++ /dev/null @@ -1,217 +0,0 @@ -From SRS0=aO/p=ON=bag.python.org=None@bounce2.pobox.com Fri Nov 26 21:40:36 2004 -X-VM-v5-Data: ([nil nil nil nil nil nil nil nil nil] - [nil nil nil nil nil nil nil "MAILER DAEMON <>" "MAILER DAEMON <>" nil nil "Banned file: auto__mail.python.bat in mail from you" "^From:" nil nil nil nil "Banned file: auto__mail.python.bat in mail from you" nil nil nil nil nil nil nil] - nil) -MIME-Version: 1.0 -Message-Id: -Content-Type: multipart/report; report-type=delivery-status; - charset=utf-8; - boundary="----------=_1101526904-1956-5" -X-Virus-Scanned: by XS4ALL Virus Scanner -X-UIDL: 4\G!!! -To: -Subject: Banned file: auto__mail.python.bat in mail from you -Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -This is a multi-part message in MIME format... - -------------=_1101526904-1956-5 -Content-Type: text/plain; charset="utf-8" -Content-Disposition: inline -Content-Transfer-Encoding: 7bit - -BANNED FILENAME ALERT - -Your message to: xxxxxxx@dot.ca.gov, xxxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxx@dot.ca.gov, xxxxxx@dot.ca.gov, xxxxxxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxxxx@dot.ca.gov, xxxxxxx@dot.ca.gov, xxxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxx@dot.ca.gov, xxx@dot.ca.gov, xxxxxxx@dot.ca.gov, xxxxxxx@dot.ca.gov, xxxxxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxxx@dot.ca.gov, xxx@dot.ca.gov, xxxxxxxx@dot.ca.gov, xxxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxxx@dot.ca.gov, xxxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxxxxxxxx@dot.ca.gov, xxxxxxx@dot.ca.gov, xxxxxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxxxxx@dot.ca.gov, xxxx@dot.ca.gov, xxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxxxxxxxxxxxxxx@dot.ca.gov -was blocked by our Spam Firewall. The email you sent with the following subject has NOT BEEN DELIVERED: - -Subject: Delivery_failure_notice - -An attachment in that mail was of a file type that the Spam Firewall is set to block. - - - -------------=_1101526904-1956-5 -Content-Type: message/delivery-status -Content-Disposition: inline -Content-Transfer-Encoding: 7bit -Content-Description: Delivery error report - -Reporting-MTA: dns; sacspam01.dot.ca.gov -Received-From-MTA: smtp; sacspam01.dot.ca.gov ([127.0.0.1]) -Arrival-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -------------=_1101526904-1956-5 -Content-Type: text/rfc822-headers -Content-Disposition: inline -Content-Transfer-Encoding: 7bit -Content-Description: Undelivered-message headers - -Received: from kgsav.org (ppp-70-242-162-63.dsl.spfdmo.swbell.net [70.242.162.63]) - by sacspam01.dot.ca.gov (Spam Firewall) with SMTP - id A232AD03DE3A; Fri, 26 Nov 2004 19:41:35 -0800 (PST) -From: webmaster@python.org -To: xxxxx@dot.ca.gov -Date: Sat, 27 Nov 2004 03:35:30 UTC -Subject: Delivery_failure_notice -Importance: Normal -X-Priority: 3 (Normal) -X-MSMail-Priority: Normal -Message-ID: -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="====67bd2b7a5.f99f7" -Content-Transfer-Encoding: 7bit - -------------=_1101526904-1956-5-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_44.txt b/src/libs/python/bongo/external/email/test/data/msg_44.txt deleted file mode 100644 index ae462a6..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_44.txt +++ /dev/null @@ -1,35 +0,0 @@ -Return-Path: -Delivered-To: barry@python.org -Received: by mail.python.org (Postfix, from userid 889) - id C2BF0D37C6; Tue, 11 Sep 2001 00:05:05 -0400 (EDT) -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="h90VIIIKmx" -Content-Transfer-Encoding: 7bit -Message-ID: <15261.36209.358846.118674@anthem.python.org> -From: barry@python.org (Barry A. Warsaw) -To: barry@python.org -Subject: a simple multipart -Date: Tue, 11 Sep 2001 00:05:05 -0400 -X-Mailer: VM 6.95 under 21.4 (patch 4) "Artificial Intelligence" XEmacs Lucid -X-Attribution: BAW -X-Oblique-Strategy: Make a door into a window - - ---h90VIIIKmx -Content-Type: text/plain -Content-Disposition: inline; name="msg.txt" -Content-Transfer-Encoding: 7bit - -a simple kind of mirror -to reflect upon our own - ---h90VIIIKmx -Content-Type: text/plain -Content-Disposition: inline; name="msg.txt" -Content-Transfer-Encoding: 7bit - -a simple kind of mirror -to reflect upon our own - ---h90VIIIKmx-- - diff --git a/src/libs/python/bongo/external/email/test/test_email.py b/src/libs/python/bongo/external/email/test/test_email.py deleted file mode 100644 index 3a873e9..0000000 --- a/src/libs/python/bongo/external/email/test/test_email.py +++ /dev/null @@ -1,3252 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Contact: email-sig@python.org -# email package unit tests - -import os -import sys -import time -import base64 -import difflib -import unittest -import warnings -from io import StringIO - -import bongo.external.email - -from bongo.external.email.charset import Charset -from bongo.external.email.header import Header, decode_header, make_header -from bongo.external.email.parser import Parser, HeaderParser -from bongo.external.email.generator import Generator, DecodedGenerator -from bongo.external.email.message import Message -from bongo.external.email.mime.audio import MIMEAudio -from bongo.external.email.mime.text import MIMEText -from bongo.external.email.mime.image import MIMEImage -from bongo.external.email.mime.base import MIMEBase -from bongo.external.email.mime.message import MIMEMessage -from bongo.external.email.mime.multipart import MIMEMultipart -from bongo.external.email import Utils -from bongo.external.email import Errors -from bongo.external.email import Encoders -from bongo.external.email import Iterators -from bongo.external.email import base64MIME -from bongo.external.email import quopriMIME - -from test.test_support import findfile, run_unittest -from bongo.external.email.test import __file__ as landmark - - -NL = '\n' -EMPTYSTRING = '' -SPACE = ' ' - - - -def openfile(filename, mode='r'): - path = os.path.join(os.path.dirname(landmark), 'data', filename) - return open(path, mode) - - - -# Base test class -class TestEmailBase(unittest.TestCase): - def ndiffAssertEqual(self, first, second): - """Like failUnlessEqual except use ndiff for readable output.""" - if first != second: - sfirst = str(first) - ssecond = str(second) - diff = difflib.ndiff(sfirst.splitlines(), ssecond.splitlines()) - fp = StringIO() - print(NL, NL.join(diff), file=fp) - raise self.failureException(fp.getvalue()) - - def _msgobj(self, filename): - fp = openfile(findfile(filename)) - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - return msg - - - -# Test various aspects of the Message class's API -class TestMessageAPI(TestEmailBase): - def test_get_all(self): - eq = self.assertEqual - msg = self._msgobj('msg_20.txt') - eq(msg.get_all('cc'), ['ccc@zzz.org', 'ddd@zzz.org', 'eee@zzz.org']) - eq(msg.get_all('xx', 'n/a'), 'n/a') - - def test_getset_charset(self): - eq = self.assertEqual - msg = Message() - eq(msg.get_charset(), None) - charset = Charset('iso-8859-1') - msg.set_charset(charset) - eq(msg['mime-version'], '1.0') - eq(msg.get_content_type(), 'text/plain') - eq(msg['content-type'], 'text/plain; charset="iso-8859-1"') - eq(msg.get_param('charset'), 'iso-8859-1') - eq(msg['content-transfer-encoding'], 'quoted-printable') - eq(msg.get_charset().input_charset, 'iso-8859-1') - # Remove the charset - msg.set_charset(None) - eq(msg.get_charset(), None) - eq(msg['content-type'], 'text/plain') - # Try adding a charset when there's already MIME headers present - msg = Message() - msg['MIME-Version'] = '2.0' - msg['Content-Type'] = 'text/x-weird' - msg['Content-Transfer-Encoding'] = 'quinted-puntable' - msg.set_charset(charset) - eq(msg['mime-version'], '2.0') - eq(msg['content-type'], 'text/x-weird; charset="iso-8859-1"') - eq(msg['content-transfer-encoding'], 'quinted-puntable') - - def test_set_charset_from_string(self): - eq = self.assertEqual - msg = Message() - msg.set_charset('us-ascii') - eq(msg.get_charset().input_charset, 'us-ascii') - eq(msg['content-type'], 'text/plain; charset="us-ascii"') - - def test_set_payload_with_charset(self): - msg = Message() - charset = Charset('iso-8859-1') - msg.set_payload('This is a string payload', charset) - self.assertEqual(msg.get_charset().input_charset, 'iso-8859-1') - - def test_get_charsets(self): - eq = self.assertEqual - - msg = self._msgobj('msg_08.txt') - charsets = msg.get_charsets() - eq(charsets, [None, 'us-ascii', 'iso-8859-1', 'iso-8859-2', 'koi8-r']) - - msg = self._msgobj('msg_09.txt') - charsets = msg.get_charsets('dingbat') - eq(charsets, ['dingbat', 'us-ascii', 'iso-8859-1', 'dingbat', - 'koi8-r']) - - msg = self._msgobj('msg_12.txt') - charsets = msg.get_charsets() - eq(charsets, [None, 'us-ascii', 'iso-8859-1', None, 'iso-8859-2', - 'iso-8859-3', 'us-ascii', 'koi8-r']) - - def test_get_filename(self): - eq = self.assertEqual - - msg = self._msgobj('msg_04.txt') - filenames = [p.get_filename() for p in msg.get_payload()] - eq(filenames, ['msg.txt', 'msg.txt']) - - msg = self._msgobj('msg_07.txt') - subpart = msg.get_payload(1) - eq(subpart.get_filename(), 'dingusfish.gif') - - def test_get_filename_with_name_parameter(self): - eq = self.assertEqual - - msg = self._msgobj('msg_44.txt') - filenames = [p.get_filename() for p in msg.get_payload()] - eq(filenames, ['msg.txt', 'msg.txt']) - - def test_get_boundary(self): - eq = self.assertEqual - msg = self._msgobj('msg_07.txt') - # No quotes! - eq(msg.get_boundary(), 'BOUNDARY') - - def test_set_boundary(self): - eq = self.assertEqual - # This one has no existing boundary parameter, but the Content-Type: - # header appears fifth. - msg = self._msgobj('msg_01.txt') - msg.set_boundary('BOUNDARY') - header, value = list(msg.items())[4] - eq(header.lower(), 'content-type') - eq(value, 'text/plain; charset="us-ascii"; boundary="BOUNDARY"') - # This one has a Content-Type: header, with a boundary, stuck in the - # middle of its headers. Make sure the order is preserved; it should - # be fifth. - msg = self._msgobj('msg_04.txt') - msg.set_boundary('BOUNDARY') - header, value = list(msg.items())[4] - eq(header.lower(), 'content-type') - eq(value, 'multipart/mixed; boundary="BOUNDARY"') - # And this one has no Content-Type: header at all. - msg = self._msgobj('msg_03.txt') - self.assertRaises(Errors.HeaderParseError, - msg.set_boundary, 'BOUNDARY') - - def test_get_decoded_payload(self): - eq = self.assertEqual - msg = self._msgobj('msg_10.txt') - # The outer message is a multipart - eq(msg.get_payload(decode=True), None) - # Subpart 1 is 7bit encoded - eq(msg.get_payload(0).get_payload(decode=True), - 'This is a 7bit encoded message.\n') - # Subpart 2 is quopri - eq(msg.get_payload(1).get_payload(decode=True), - '\xa1This is a Quoted Printable encoded message!\n') - # Subpart 3 is base64 - eq(msg.get_payload(2).get_payload(decode=True), - 'This is a Base64 encoded message.') - # Subpart 4 has no Content-Transfer-Encoding: header. - eq(msg.get_payload(3).get_payload(decode=True), - 'This has no Content-Transfer-Encoding: header.\n') - - def test_get_decoded_uu_payload(self): - eq = self.assertEqual - msg = Message() - msg.set_payload('begin 666 -\n+:&5L;&\\@=V]R;&0 \n \nend\n') - for cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'): - msg['content-transfer-encoding'] = cte - eq(msg.get_payload(decode=True), 'hello world') - # Now try some bogus data - msg.set_payload('foo') - eq(msg.get_payload(decode=True), 'foo') - - def test_decode_bogus_uu_payload_quietly(self): - msg = Message() - msg.set_payload('begin 664 foo.txt\n%' % i for i in range(10)]) - msg.set_payload('Test') - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), """\ -From: test@dom.ain -References: <0@dom.ain> <1@dom.ain> <2@dom.ain> <3@dom.ain> <4@dom.ain> -\t<5@dom.ain> <6@dom.ain> <7@dom.ain> <8@dom.ain> <9@dom.ain> - -Test""") - - def test_no_split_long_header(self): - eq = self.ndiffAssertEqual - hstr = 'References: ' + 'x' * 80 - h = Header(hstr, continuation_ws='\t') - eq(h.encode(), """\ -References: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx""") - - def test_splitting_multiple_long_lines(self): - eq = self.ndiffAssertEqual - hstr = """\ -from babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for ; Sat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for ; Sat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for ; Sat, 2 Feb 2002 17:00:06 -0800 (PST) -""" - h = Header(hstr, continuation_ws='\t') - eq(h.encode(), """\ -from babylon.socal-raves.org (localhost [127.0.0.1]); -\tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; -\tfor ; -\tSat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); -\tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; -\tfor ; -\tSat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); -\tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; -\tfor ; -\tSat, 2 Feb 2002 17:00:06 -0800 (PST)""") - - def test_splitting_first_line_only_is_long(self): - eq = self.ndiffAssertEqual - hstr = """\ -from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] helo=cthulhu.gerg.ca) -\tby kronos.mems-exchange.org with esmtp (Exim 4.05) -\tid 17k4h5-00034i-00 -\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""" - h = Header(hstr, maxlinelen=78, header_name='Received', - continuation_ws='\t') - eq(h.encode(), """\ -from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] -\thelo=cthulhu.gerg.ca) -\tby kronos.mems-exchange.org with esmtp (Exim 4.05) -\tid 17k4h5-00034i-00 -\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""") - - def test_long_8bit_header(self): - eq = self.ndiffAssertEqual - msg = Message() - h = Header('Britische Regierung gibt', 'iso-8859-1', - header_name='Subject') - h.append('gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte') - msg['Subject'] = h - eq(msg.as_string(), """\ -Subject: =?iso-8859-1?q?Britische_Regierung_gibt?= =?iso-8859-1?q?gr=FCnes?= - =?iso-8859-1?q?_Licht_f=FCr_Offshore-Windkraftprojekte?= - -""") - - def test_long_8bit_header_no_charset(self): - eq = self.ndiffAssertEqual - msg = Message() - msg['Reply-To'] = 'Britische Regierung gibt gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte ' - eq(msg.as_string(), """\ -Reply-To: Britische Regierung gibt gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte - -""") - - def test_long_to_header(self): - eq = self.ndiffAssertEqual - to = '"Someone Test #A" ,,"Someone Test #B" , "Someone Test #C" , "Someone Test #D" ' - msg = Message() - msg['To'] = to - eq(msg.as_string(0), '''\ -To: "Someone Test #A" , , -\t"Someone Test #B" , -\t"Someone Test #C" , -\t"Someone Test #D" - -''') - - def test_long_line_after_append(self): - eq = self.ndiffAssertEqual - s = 'This is an example of string which has almost the limit of header length.' - h = Header(s) - h.append('Add another line.') - eq(h.encode(), """\ -This is an example of string which has almost the limit of header length. - Add another line.""") - - def test_shorter_line_with_append(self): - eq = self.ndiffAssertEqual - s = 'This is a shorter line.' - h = Header(s) - h.append('Add another sentence. (Surprise?)') - eq(h.encode(), - 'This is a shorter line. Add another sentence. (Surprise?)') - - def test_long_field_name(self): - eq = self.ndiffAssertEqual - fn = 'X-Very-Very-Very-Long-Header-Name' - gs = "Die Mieter treten hier ein werden mit einem Foerderband komfortabel den Korridor entlang, an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, gegen die rotierenden Klingen bef\xf6rdert. " - h = Header(gs, 'iso-8859-1', header_name=fn) - # BAW: this seems broken because the first line is too long - eq(h.encode(), """\ -=?iso-8859-1?q?Die_Mieter_treten_hier_?= - =?iso-8859-1?q?ein_werden_mit_einem_Foerderband_komfortabel_den_Korridor_?= - =?iso-8859-1?q?entlang=2C_an_s=FCdl=FCndischen_Wandgem=E4lden_vorbei=2C_g?= - =?iso-8859-1?q?egen_die_rotierenden_Klingen_bef=F6rdert=2E_?=""") - - def test_long_received_header(self): - h = 'from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP; Wed, 05 Mar 2003 18:10:18 -0700' - msg = Message() - msg['Received-1'] = Header(h, continuation_ws='\t') - msg['Received-2'] = h - self.assertEqual(msg.as_string(), """\ -Received-1: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by -\throthgar.la.mastaler.com (tmda-ofmipd) with ESMTP; -\tWed, 05 Mar 2003 18:10:18 -0700 -Received-2: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by -\throthgar.la.mastaler.com (tmda-ofmipd) with ESMTP; -\tWed, 05 Mar 2003 18:10:18 -0700 - -""") - - def test_string_headerinst_eq(self): - h = '<15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner\'s message of "Thu, 6 Mar 2003 13:58:21 +0100")' - msg = Message() - msg['Received-1'] = Header(h, header_name='Received-1', - continuation_ws='\t') - msg['Received-2'] = h - self.assertEqual(msg.as_string(), """\ -Received-1: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> -\t(David Bremner's message of "Thu, 6 Mar 2003 13:58:21 +0100") -Received-2: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> -\t(David Bremner's message of "Thu, 6 Mar 2003 13:58:21 +0100") - -""") - - def test_long_unbreakable_lines_with_continuation(self): - eq = self.ndiffAssertEqual - msg = Message() - t = """\ - iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 - locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp""" - msg['Face-1'] = t - msg['Face-2'] = Header(t, header_name='Face-2') - eq(msg.as_string(), """\ -Face-1: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 -\tlocQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp -Face-2: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 - locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp - -""") - - def test_another_long_multiline_header(self): - eq = self.ndiffAssertEqual - m = '''\ -Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with Microsoft SMTPSVC(5.0.2195.4905); -\tWed, 16 Oct 2002 07:41:11 -0700''' - msg = bongo.external.email.message_from_string(m) - eq(msg.as_string(), '''\ -Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with -\tMicrosoft SMTPSVC(5.0.2195.4905); Wed, 16 Oct 2002 07:41:11 -0700 - -''') - - def test_long_lines_with_different_header(self): - eq = self.ndiffAssertEqual - h = """\ -List-Unsubscribe: , - """ - msg = Message() - msg['List'] = h - msg['List'] = Header(h, header_name='List') - eq(msg.as_string(), """\ -List: List-Unsubscribe: , -\t -List: List-Unsubscribe: , - - -""") - - - -# Test mangling of "From " lines in the body of a message -class TestFromMangling(unittest.TestCase): - def setUp(self): - self.msg = Message() - self.msg['From'] = 'aaa@bbb.org' - self.msg.set_payload("""\ -From the desk of A.A.A.: -Blah blah blah -""") - - def test_mangled_from(self): - s = StringIO() - g = Generator(s, mangle_from_=True) - g.flatten(self.msg) - self.assertEqual(s.getvalue(), """\ -From: aaa@bbb.org - ->From the desk of A.A.A.: -Blah blah blah -""") - - def test_dont_mangle_from(self): - s = StringIO() - g = Generator(s, mangle_from_=False) - g.flatten(self.msg) - self.assertEqual(s.getvalue(), """\ -From: aaa@bbb.org - -From the desk of A.A.A.: -Blah blah blah -""") - - - -# Test the basic MIMEAudio class -class TestMIMEAudio(unittest.TestCase): - def setUp(self): - # Make sure we pick up the audiotest.au that lives in email/test/data. - # In Python, there's an audiotest.au living in Lib/test but that isn't - # included in some binary distros that don't include the test - # package. The trailing empty string on the .join() is significant - # since findfile() will do a dirname(). - datadir = os.path.join(os.path.dirname(landmark), 'data', '') - fp = open(findfile('audiotest.au', datadir), 'rb') - try: - self._audiodata = fp.read() - finally: - fp.close() - self._au = MIMEAudio(self._audiodata) - - def test_guess_minor_type(self): - self.assertEqual(self._au.get_content_type(), 'audio/basic') - - def test_encoding(self): - payload = self._au.get_payload() - self.assertEqual(base64.decodestring(payload), self._audiodata) - - def test_checkSetMinor(self): - au = MIMEAudio(self._audiodata, 'fish') - self.assertEqual(au.get_content_type(), 'audio/fish') - - def test_add_header(self): - eq = self.assertEqual - unless = self.assertTrue - self._au.add_header('Content-Disposition', 'attachment', - filename='audiotest.au') - eq(self._au['content-disposition'], - 'attachment; filename="audiotest.au"') - eq(self._au.get_params(header='content-disposition'), - [('attachment', ''), ('filename', 'audiotest.au')]) - eq(self._au.get_param('filename', header='content-disposition'), - 'audiotest.au') - missing = [] - eq(self._au.get_param('attachment', header='content-disposition'), '') - unless(self._au.get_param('foo', failobj=missing, - header='content-disposition') is missing) - # Try some missing stuff - unless(self._au.get_param('foobar', missing) is missing) - unless(self._au.get_param('attachment', missing, - header='foobar') is missing) - - - -# Test the basic MIMEImage class -class TestMIMEImage(unittest.TestCase): - def setUp(self): - fp = openfile('PyBanner048.gif') - try: - self._imgdata = fp.read() - finally: - fp.close() - self._im = MIMEImage(self._imgdata) - - def test_guess_minor_type(self): - self.assertEqual(self._im.get_content_type(), 'image/gif') - - def test_encoding(self): - payload = self._im.get_payload() - self.assertEqual(base64.decodestring(payload), self._imgdata) - - def test_checkSetMinor(self): - im = MIMEImage(self._imgdata, 'fish') - self.assertEqual(im.get_content_type(), 'image/fish') - - def test_add_header(self): - eq = self.assertEqual - unless = self.assertTrue - self._im.add_header('Content-Disposition', 'attachment', - filename='dingusfish.gif') - eq(self._im['content-disposition'], - 'attachment; filename="dingusfish.gif"') - eq(self._im.get_params(header='content-disposition'), - [('attachment', ''), ('filename', 'dingusfish.gif')]) - eq(self._im.get_param('filename', header='content-disposition'), - 'dingusfish.gif') - missing = [] - eq(self._im.get_param('attachment', header='content-disposition'), '') - unless(self._im.get_param('foo', failobj=missing, - header='content-disposition') is missing) - # Try some missing stuff - unless(self._im.get_param('foobar', missing) is missing) - unless(self._im.get_param('attachment', missing, - header='foobar') is missing) - - - -# Test the basic MIMEText class -class TestMIMEText(unittest.TestCase): - def setUp(self): - self._msg = MIMEText('hello there') - - def test_types(self): - eq = self.assertEqual - unless = self.assertTrue - eq(self._msg.get_content_type(), 'text/plain') - eq(self._msg.get_param('charset'), 'us-ascii') - missing = [] - unless(self._msg.get_param('foobar', missing) is missing) - unless(self._msg.get_param('charset', missing, header='foobar') - is missing) - - def test_payload(self): - self.assertEqual(self._msg.get_payload(), 'hello there') - self.assertTrue(not self._msg.is_multipart()) - - def test_charset(self): - eq = self.assertEqual - msg = MIMEText('hello there', _charset='us-ascii') - eq(msg.get_charset().input_charset, 'us-ascii') - eq(msg['content-type'], 'text/plain; charset="us-ascii"') - - - -# Test complicated multipart/* messages -class TestMultipart(TestEmailBase): - def setUp(self): - fp = openfile('PyBanner048.gif') - try: - data = fp.read() - finally: - fp.close() - - container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY') - image = MIMEImage(data, name='dingusfish.gif') - image.add_header('content-disposition', 'attachment', - filename='dingusfish.gif') - intro = MIMEText('''\ -Hi there, - -This is the dingus fish. -''') - container.attach(intro) - container.attach(image) - container['From'] = 'Barry ' - container['To'] = 'Dingus Lovers ' - container['Subject'] = 'Here is your dingus fish' - - now = 987809702.54848599 - timetuple = time.localtime(now) - if timetuple[-1] == 0: - tzsecs = time.timezone - else: - tzsecs = time.altzone - if tzsecs > 0: - sign = '-' - else: - sign = '+' - tzoffset = ' %s%04d' % (sign, tzsecs / 36) - container['Date'] = time.strftime( - '%a, %d %b %Y %H:%M:%S', - time.localtime(now)) + tzoffset - self._msg = container - self._im = image - self._txt = intro - - def test_hierarchy(self): - # convenience - eq = self.assertEqual - unless = self.assertTrue - raises = self.assertRaises - # tests - m = self._msg - unless(m.is_multipart()) - eq(m.get_content_type(), 'multipart/mixed') - eq(len(m.get_payload()), 2) - raises(IndexError, m.get_payload, 2) - m0 = m.get_payload(0) - m1 = m.get_payload(1) - unless(m0 is self._txt) - unless(m1 is self._im) - eq(m.get_payload(), [m0, m1]) - unless(not m0.is_multipart()) - unless(not m1.is_multipart()) - - def test_empty_multipart_idempotent(self): - text = """\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - - ---BOUNDARY - - ---BOUNDARY-- -""" - msg = Parser().parsestr(text) - self.ndiffAssertEqual(text, msg.as_string()) - - def test_no_parts_in_a_multipart_with_none_epilogue(self): - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.set_boundary('BOUNDARY') - self.ndiffAssertEqual(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY - ---BOUNDARY--''') - - def test_no_parts_in_a_multipart_with_empty_epilogue(self): - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.preamble = '' - outer.epilogue = '' - outer.set_boundary('BOUNDARY') - self.ndiffAssertEqual(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - - ---BOUNDARY - ---BOUNDARY-- -''') - - def test_one_part_in_a_multipart(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.set_boundary('BOUNDARY') - msg = MIMEText('hello world') - outer.attach(msg) - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - def test_seq_parts_in_a_multipart_with_empty_preamble(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.preamble = '' - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - - def test_seq_parts_in_a_multipart_with_none_preamble(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.preamble = None - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - - def test_seq_parts_in_a_multipart_with_none_epilogue(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.epilogue = None - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - - def test_seq_parts_in_a_multipart_with_empty_epilogue(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.epilogue = '' - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY-- -''') - - - def test_seq_parts_in_a_multipart_with_nl_epilogue(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.epilogue = '\n' - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY-- - -''') - - def test_message_external_body(self): - eq = self.assertEqual - msg = self._msgobj('msg_36.txt') - eq(len(msg.get_payload()), 2) - msg1 = msg.get_payload(1) - eq(msg1.get_content_type(), 'multipart/alternative') - eq(len(msg1.get_payload()), 2) - for subpart in msg1.get_payload(): - eq(subpart.get_content_type(), 'message/external-body') - eq(len(subpart.get_payload()), 1) - subsubpart = subpart.get_payload(0) - eq(subsubpart.get_content_type(), 'text/plain') - - def test_double_boundary(self): - # msg_37.txt is a multipart that contains two dash-boundary's in a - # row. Our interpretation of RFC 2046 calls for ignoring the second - # and subsequent boundaries. - msg = self._msgobj('msg_37.txt') - self.assertEqual(len(msg.get_payload()), 3) - - def test_nested_inner_contains_outer_boundary(self): - eq = self.ndiffAssertEqual - # msg_38.txt has an inner part that contains outer boundaries. My - # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say - # these are illegal and should be interpreted as unterminated inner - # parts. - msg = self._msgobj('msg_38.txt') - sfp = StringIO() - Iterators._structure(msg, sfp) - eq(sfp.getvalue(), """\ -multipart/mixed - multipart/mixed - multipart/alternative - text/plain - text/plain - text/plain - text/plain -""") - - def test_nested_with_same_boundary(self): - eq = self.ndiffAssertEqual - # msg 39.txt is similarly evil in that it's got inner parts that use - # the same boundary as outer parts. Again, I believe the way this is - # parsed is closest to the spirit of RFC 2046 - msg = self._msgobj('msg_39.txt') - sfp = StringIO() - Iterators._structure(msg, sfp) - eq(sfp.getvalue(), """\ -multipart/mixed - multipart/mixed - multipart/alternative - application/octet-stream - application/octet-stream - text/plain -""") - - def test_boundary_in_non_multipart(self): - msg = self._msgobj('msg_40.txt') - self.assertEqual(msg.as_string(), '''\ -MIME-Version: 1.0 -Content-Type: text/html; boundary="--961284236552522269" - -----961284236552522269 -Content-Type: text/html; -Content-Transfer-Encoding: 7Bit - - - -----961284236552522269-- -''') - - def test_boundary_with_leading_space(self): - eq = self.assertEqual - msg = bongo.external.email.message_from_string('''\ -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary=" XXXX" - --- XXXX -Content-Type: text/plain - - --- XXXX -Content-Type: text/plain - --- XXXX-- -''') - self.assertTrue(msg.is_multipart()) - eq(msg.get_boundary(), ' XXXX') - eq(len(msg.get_payload()), 2) - - def test_boundary_without_trailing_newline(self): - m = Parser().parsestr("""\ -Content-Type: multipart/mixed; boundary="===============0012394164==" -MIME-Version: 1.0 - ---===============0012394164== -Content-Type: image/file1.jpg -MIME-Version: 1.0 -Content-Transfer-Encoding: base64 - -YXNkZg== ---===============0012394164==--""") - self.assertEqual(m.get_payload(0).get_payload(), 'YXNkZg==') - - - -# Test some badly formatted messages -class TestNonConformant(TestEmailBase): - def test_parse_missing_minor_type(self): - eq = self.assertEqual - msg = self._msgobj('msg_14.txt') - eq(msg.get_content_type(), 'text/plain') - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - - def test_same_boundary_inner_outer(self): - unless = self.assertTrue - msg = self._msgobj('msg_15.txt') - # XXX We can probably eventually do better - inner = msg.get_payload(0) - unless(hasattr(inner, 'defects')) - self.assertEqual(len(inner.defects), 1) - unless(isinstance(inner.defects[0], - Errors.StartBoundaryNotFoundDefect)) - - def test_multipart_no_boundary(self): - unless = self.assertTrue - msg = self._msgobj('msg_25.txt') - unless(isinstance(msg.get_payload(), str)) - self.assertEqual(len(msg.defects), 2) - unless(isinstance(msg.defects[0], Errors.NoBoundaryInMultipartDefect)) - unless(isinstance(msg.defects[1], - Errors.MultipartInvariantViolationDefect)) - - def test_invalid_content_type(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - msg = Message() - # RFC 2045, $5.2 says invalid yields text/plain - msg['Content-Type'] = 'text' - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - eq(msg.get_content_type(), 'text/plain') - # Clear the old value and try something /really/ invalid - del msg['content-type'] - msg['Content-Type'] = 'foo' - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - eq(msg.get_content_type(), 'text/plain') - # Still, make sure that the message is idempotently generated - s = StringIO() - g = Generator(s) - g.flatten(msg) - neq(s.getvalue(), 'Content-Type: foo\n\n') - - def test_no_start_boundary(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_31.txt') - eq(msg.get_payload(), """\ ---BOUNDARY -Content-Type: text/plain - -message 1 - ---BOUNDARY -Content-Type: text/plain - -message 2 - ---BOUNDARY-- -""") - - def test_no_separating_blank_line(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_35.txt') - eq(msg.as_string(), """\ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: here's something interesting - -counter to RFC 2822, there's no separating newline here -""") - - def test_lying_multipart(self): - unless = self.assertTrue - msg = self._msgobj('msg_41.txt') - unless(hasattr(msg, 'defects')) - self.assertEqual(len(msg.defects), 2) - unless(isinstance(msg.defects[0], Errors.NoBoundaryInMultipartDefect)) - unless(isinstance(msg.defects[1], - Errors.MultipartInvariantViolationDefect)) - - def test_missing_start_boundary(self): - outer = self._msgobj('msg_42.txt') - # The message structure is: - # - # multipart/mixed - # text/plain - # message/rfc822 - # multipart/mixed [*] - # - # [*] This message is missing its start boundary - bad = outer.get_payload(1).get_payload(0) - self.assertEqual(len(bad.defects), 1) - self.assertTrue(isinstance(bad.defects[0], - Errors.StartBoundaryNotFoundDefect)) - - - -# Test RFC 2047 header encoding and decoding -class TestRFC2047(unittest.TestCase): - def test_rfc2047_multiline(self): - eq = self.assertEqual - s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz - foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?=""" - dh = decode_header(s) - eq(dh, [ - ('Re:', None), - ('r\x8aksm\x9arg\x8cs', 'mac-iceland'), - ('baz foo bar', None), - ('r\x8aksm\x9arg\x8cs', 'mac-iceland')]) - eq(str(make_header(dh)), - """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar - =?mac-iceland?q?r=8Aksm=9Arg=8Cs?=""") - - def test_whitespace_eater_unicode(self): - eq = self.assertEqual - s = '=?ISO-8859-1?Q?Andr=E9?= Pirard ' - dh = decode_header(s) - eq(dh, [('Andr\xe9', 'iso-8859-1'), ('Pirard ', None)]) - hu = str(make_header(dh)).encode('latin-1') - eq(hu, 'Andr\xe9 Pirard ') - - def test_whitespace_eater_unicode_2(self): - eq = self.assertEqual - s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?=' - dh = decode_header(s) - eq(dh, [('The', None), ('quick brown fox', 'iso-8859-1'), - ('jumped over the', None), ('lazy dog', 'iso-8859-1')]) - hu = make_header(dh).__unicode__() - eq(hu, 'The quick brown fox jumped over the lazy dog') - - - -# Test the MIMEMessage class -class TestMIMEMessage(TestEmailBase): - def setUp(self): - fp = openfile('msg_11.txt') - try: - self._text = fp.read() - finally: - fp.close() - - def test_type_error(self): - self.assertRaises(TypeError, MIMEMessage, 'a plain string') - - def test_valid_argument(self): - eq = self.assertEqual - unless = self.assertTrue - subject = 'A sub-message' - m = Message() - m['Subject'] = subject - r = MIMEMessage(m) - eq(r.get_content_type(), 'message/rfc822') - payload = r.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - subpart = payload[0] - unless(subpart is m) - eq(subpart['subject'], subject) - - def test_bad_multipart(self): - eq = self.assertEqual - msg1 = Message() - msg1['Subject'] = 'subpart 1' - msg2 = Message() - msg2['Subject'] = 'subpart 2' - r = MIMEMessage(msg1) - self.assertRaises(Errors.MultipartConversionError, r.attach, msg2) - - def test_generate(self): - # First craft the message to be encapsulated - m = Message() - m['Subject'] = 'An enclosed message' - m.set_payload('Here is the body of the message.\n') - r = MIMEMessage(m) - r['Subject'] = 'The enclosing message' - s = StringIO() - g = Generator(s) - g.flatten(r) - self.assertEqual(s.getvalue(), """\ -Content-Type: message/rfc822 -MIME-Version: 1.0 -Subject: The enclosing message - -Subject: An enclosed message - -Here is the body of the message. -""") - - def test_parse_message_rfc822(self): - eq = self.assertEqual - unless = self.assertTrue - msg = self._msgobj('msg_11.txt') - eq(msg.get_content_type(), 'message/rfc822') - payload = msg.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - submsg = payload[0] - self.assertTrue(isinstance(submsg, Message)) - eq(submsg['subject'], 'An enclosed message') - eq(submsg.get_payload(), 'Here is the body of the message.\n') - - def test_dsn(self): - eq = self.assertEqual - unless = self.assertTrue - # msg 16 is a Delivery Status Notification, see RFC 1894 - msg = self._msgobj('msg_16.txt') - eq(msg.get_content_type(), 'multipart/report') - unless(msg.is_multipart()) - eq(len(msg.get_payload()), 3) - # Subpart 1 is a text/plain, human readable section - subpart = msg.get_payload(0) - eq(subpart.get_content_type(), 'text/plain') - eq(subpart.get_payload(), """\ -This report relates to a message you sent with the following header fields: - - Message-id: <002001c144a6$8752e060$56104586@oxy.edu> - Date: Sun, 23 Sep 2001 20:10:55 -0700 - From: "Ian T. Henry" - To: SoCal Raves - Subject: [scr] yeah for Ians!! - -Your message cannot be delivered to the following recipients: - - Recipient address: jangel1@cougar.noc.ucla.edu - Reason: recipient reached disk quota - -""") - # Subpart 2 contains the machine parsable DSN information. It - # consists of two blocks of headers, represented by two nested Message - # objects. - subpart = msg.get_payload(1) - eq(subpart.get_content_type(), 'message/delivery-status') - eq(len(subpart.get_payload()), 2) - # message/delivery-status should treat each block as a bunch of - # headers, i.e. a bunch of Message objects. - dsn1 = subpart.get_payload(0) - unless(isinstance(dsn1, Message)) - eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu') - eq(dsn1.get_param('dns', header='reporting-mta'), '') - # Try a missing one - eq(dsn1.get_param('nsd', header='reporting-mta'), None) - dsn2 = subpart.get_payload(1) - unless(isinstance(dsn2, Message)) - eq(dsn2['action'], 'failed') - eq(dsn2.get_params(header='original-recipient'), - [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')]) - eq(dsn2.get_param('rfc822', header='final-recipient'), '') - # Subpart 3 is the original message - subpart = msg.get_payload(2) - eq(subpart.get_content_type(), 'message/rfc822') - payload = subpart.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - subsubpart = payload[0] - unless(isinstance(subsubpart, Message)) - eq(subsubpart.get_content_type(), 'text/plain') - eq(subsubpart['message-id'], - '<002001c144a6$8752e060$56104586@oxy.edu>') - - def test_epilogue(self): - eq = self.ndiffAssertEqual - fp = openfile('msg_21.txt') - try: - text = fp.read() - finally: - fp.close() - msg = Message() - msg['From'] = 'aperson@dom.ain' - msg['To'] = 'bperson@dom.ain' - msg['Subject'] = 'Test' - msg.preamble = 'MIME message' - msg.epilogue = 'End of MIME message\n' - msg1 = MIMEText('One') - msg2 = MIMEText('Two') - msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY') - msg.attach(msg1) - msg.attach(msg2) - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), text) - - def test_no_nl_preamble(self): - eq = self.ndiffAssertEqual - msg = Message() - msg['From'] = 'aperson@dom.ain' - msg['To'] = 'bperson@dom.ain' - msg['Subject'] = 'Test' - msg.preamble = 'MIME message' - msg.epilogue = '' - msg1 = MIMEText('One') - msg2 = MIMEText('Two') - msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY') - msg.attach(msg1) - msg.attach(msg2) - eq(msg.as_string(), """\ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: Test -Content-Type: multipart/mixed; boundary="BOUNDARY" - -MIME message ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -One ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -Two ---BOUNDARY-- -""") - - def test_default_type(self): - eq = self.assertEqual - fp = openfile('msg_30.txt') - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - container1 = msg.get_payload(0) - eq(container1.get_default_type(), 'message/rfc822') - eq(container1.get_content_type(), 'message/rfc822') - container2 = msg.get_payload(1) - eq(container2.get_default_type(), 'message/rfc822') - eq(container2.get_content_type(), 'message/rfc822') - container1a = container1.get_payload(0) - eq(container1a.get_default_type(), 'text/plain') - eq(container1a.get_content_type(), 'text/plain') - container2a = container2.get_payload(0) - eq(container2a.get_default_type(), 'text/plain') - eq(container2a.get_content_type(), 'text/plain') - - def test_default_type_with_explicit_container_type(self): - eq = self.assertEqual - fp = openfile('msg_28.txt') - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - container1 = msg.get_payload(0) - eq(container1.get_default_type(), 'message/rfc822') - eq(container1.get_content_type(), 'message/rfc822') - container2 = msg.get_payload(1) - eq(container2.get_default_type(), 'message/rfc822') - eq(container2.get_content_type(), 'message/rfc822') - container1a = container1.get_payload(0) - eq(container1a.get_default_type(), 'text/plain') - eq(container1a.get_content_type(), 'text/plain') - container2a = container2.get_payload(0) - eq(container2a.get_default_type(), 'text/plain') - eq(container2a.get_content_type(), 'text/plain') - - def test_default_type_non_parsed(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - # Set up container - container = MIMEMultipart('digest', 'BOUNDARY') - container.epilogue = '' - # Set up subparts - subpart1a = MIMEText('message 1\n') - subpart2a = MIMEText('message 2\n') - subpart1 = MIMEMessage(subpart1a) - subpart2 = MIMEMessage(subpart2a) - container.attach(subpart1) - container.attach(subpart2) - eq(subpart1.get_content_type(), 'message/rfc822') - eq(subpart1.get_default_type(), 'message/rfc822') - eq(subpart2.get_content_type(), 'message/rfc822') - eq(subpart2.get_default_type(), 'message/rfc822') - neq(container.as_string(0), '''\ -Content-Type: multipart/digest; boundary="BOUNDARY" -MIME-Version: 1.0 - ---BOUNDARY -Content-Type: message/rfc822 -MIME-Version: 1.0 - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 1 - ---BOUNDARY -Content-Type: message/rfc822 -MIME-Version: 1.0 - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 2 - ---BOUNDARY-- -''') - del subpart1['content-type'] - del subpart1['mime-version'] - del subpart2['content-type'] - del subpart2['mime-version'] - eq(subpart1.get_content_type(), 'message/rfc822') - eq(subpart1.get_default_type(), 'message/rfc822') - eq(subpart2.get_content_type(), 'message/rfc822') - eq(subpart2.get_default_type(), 'message/rfc822') - neq(container.as_string(0), '''\ -Content-Type: multipart/digest; boundary="BOUNDARY" -MIME-Version: 1.0 - ---BOUNDARY - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 1 - ---BOUNDARY - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 2 - ---BOUNDARY-- -''') - - def test_mime_attachments_in_constructor(self): - eq = self.assertEqual - text1 = MIMEText('') - text2 = MIMEText('') - msg = MIMEMultipart(_subparts=(text1, text2)) - eq(len(msg.get_payload()), 2) - eq(msg.get_payload(0), text1) - eq(msg.get_payload(1), text2) - - - -# A general test of parser->model->generator idempotency. IOW, read a message -# in, parse it into a message object tree, then without touching the tree, -# regenerate the plain text. The original text and the transformed text -# should be identical. Note: that we ignore the Unix-From since that may -# contain a changed date. -class TestIdempotent(TestEmailBase): - def _msgobj(self, filename): - fp = openfile(filename) - try: - data = fp.read() - finally: - fp.close() - msg = bongo.external.email.message_from_string(data) - return msg, data - - def _idempotent(self, msg, text): - eq = self.ndiffAssertEqual - s = StringIO() - g = Generator(s, maxheaderlen=0) - g.flatten(msg) - eq(text, s.getvalue()) - - def test_parse_text_message(self): - eq = self.assertEqual - msg, text = self._msgobj('msg_01.txt') - eq(msg.get_content_type(), 'text/plain') - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - eq(msg.get_params()[1], ('charset', 'us-ascii')) - eq(msg.get_param('charset'), 'us-ascii') - eq(msg.preamble, None) - eq(msg.epilogue, None) - self._idempotent(msg, text) - - def test_parse_untyped_message(self): - eq = self.assertEqual - msg, text = self._msgobj('msg_03.txt') - eq(msg.get_content_type(), 'text/plain') - eq(msg.get_params(), None) - eq(msg.get_param('charset'), None) - self._idempotent(msg, text) - - def test_simple_multipart(self): - msg, text = self._msgobj('msg_04.txt') - self._idempotent(msg, text) - - def test_MIME_digest(self): - msg, text = self._msgobj('msg_02.txt') - self._idempotent(msg, text) - - def test_long_header(self): - msg, text = self._msgobj('msg_27.txt') - self._idempotent(msg, text) - - def test_MIME_digest_with_part_headers(self): - msg, text = self._msgobj('msg_28.txt') - self._idempotent(msg, text) - - def test_mixed_with_image(self): - msg, text = self._msgobj('msg_06.txt') - self._idempotent(msg, text) - - def test_multipart_report(self): - msg, text = self._msgobj('msg_05.txt') - self._idempotent(msg, text) - - def test_dsn(self): - msg, text = self._msgobj('msg_16.txt') - self._idempotent(msg, text) - - def test_preamble_epilogue(self): - msg, text = self._msgobj('msg_21.txt') - self._idempotent(msg, text) - - def test_multipart_one_part(self): - msg, text = self._msgobj('msg_23.txt') - self._idempotent(msg, text) - - def test_multipart_no_parts(self): - msg, text = self._msgobj('msg_24.txt') - self._idempotent(msg, text) - - def test_no_start_boundary(self): - msg, text = self._msgobj('msg_31.txt') - self._idempotent(msg, text) - - def test_rfc2231_charset(self): - msg, text = self._msgobj('msg_32.txt') - self._idempotent(msg, text) - - def test_more_rfc2231_parameters(self): - msg, text = self._msgobj('msg_33.txt') - self._idempotent(msg, text) - - def test_text_plain_in_a_multipart_digest(self): - msg, text = self._msgobj('msg_34.txt') - self._idempotent(msg, text) - - def test_nested_multipart_mixeds(self): - msg, text = self._msgobj('msg_12a.txt') - self._idempotent(msg, text) - - def test_message_external_body_idempotent(self): - msg, text = self._msgobj('msg_36.txt') - self._idempotent(msg, text) - - def test_content_type(self): - eq = self.assertEqual - unless = self.assertTrue - # Get a message object and reset the seek pointer for other tests - msg, text = self._msgobj('msg_05.txt') - eq(msg.get_content_type(), 'multipart/report') - # Test the Content-Type: parameters - params = {} - for pk, pv in msg.get_params(): - params[pk] = pv - eq(params['report-type'], 'delivery-status') - eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com') - eq(msg.preamble, 'This is a MIME-encapsulated message.\n') - eq(msg.epilogue, '\n') - eq(len(msg.get_payload()), 3) - # Make sure the subparts are what we expect - msg1 = msg.get_payload(0) - eq(msg1.get_content_type(), 'text/plain') - eq(msg1.get_payload(), 'Yadda yadda yadda\n') - msg2 = msg.get_payload(1) - eq(msg2.get_content_type(), 'text/plain') - eq(msg2.get_payload(), 'Yadda yadda yadda\n') - msg3 = msg.get_payload(2) - eq(msg3.get_content_type(), 'message/rfc822') - self.assertTrue(isinstance(msg3, Message)) - payload = msg3.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - msg4 = payload[0] - unless(isinstance(msg4, Message)) - eq(msg4.get_payload(), 'Yadda yadda yadda\n') - - def test_parser(self): - eq = self.assertEqual - unless = self.assertTrue - msg, text = self._msgobj('msg_06.txt') - # Check some of the outer headers - eq(msg.get_content_type(), 'message/rfc822') - # Make sure the payload is a list of exactly one sub-Message, and that - # that submessage has a type of text/plain - payload = msg.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - msg1 = payload[0] - self.assertTrue(isinstance(msg1, Message)) - eq(msg1.get_content_type(), 'text/plain') - self.assertTrue(isinstance(msg1.get_payload(), str)) - eq(msg1.get_payload(), '\n') - - - -# Test various other bits of the package's functionality -class TestMiscellaneous(TestEmailBase): - def test_message_from_string(self): - fp = openfile('msg_01.txt') - try: - text = fp.read() - finally: - fp.close() - msg = bongo.external.email.message_from_string(text) - s = StringIO() - # Don't wrap/continue long headers since we're trying to test - # idempotency. - g = Generator(s, maxheaderlen=0) - g.flatten(msg) - self.ndiffAssertEqual(text, s.getvalue()) - - def test_message_from_file(self): - fp = openfile('msg_01.txt') - try: - text = fp.read() - fp.seek(0) - msg = bongo.external.email.message_from_file(fp) - s = StringIO() - # Don't wrap/continue long headers since we're trying to test - # idempotency. - g = Generator(s, maxheaderlen=0) - g.flatten(msg) - self.assertEqual(text, s.getvalue()) - finally: - fp.close() - - def test_message_from_string_with_class(self): - unless = self.assertTrue - fp = openfile('msg_01.txt') - try: - text = fp.read() - finally: - fp.close() - # Create a subclass - class MyMessage(Message): - pass - - msg = bongo.external.email.message_from_string(text, MyMessage) - unless(isinstance(msg, MyMessage)) - # Try something more complicated - fp = openfile('msg_02.txt') - try: - text = fp.read() - finally: - fp.close() - msg = bongo.external.email.message_from_string(text, MyMessage) - for subpart in msg.walk(): - unless(isinstance(subpart, MyMessage)) - - def test_message_from_file_with_class(self): - unless = self.assertTrue - # Create a subclass - class MyMessage(Message): - pass - - fp = openfile('msg_01.txt') - try: - msg = bongo.external.email.message_from_file(fp, MyMessage) - finally: - fp.close() - unless(isinstance(msg, MyMessage)) - # Try something more complicated - fp = openfile('msg_02.txt') - try: - msg = bongo.external.email.message_from_file(fp, MyMessage) - finally: - fp.close() - for subpart in msg.walk(): - unless(isinstance(subpart, MyMessage)) - - def test__all__(self): - module = __import__('email') - all = module.__all__ - all.sort() - self.assertEqual(all, [ - # Old names - 'Charset', 'Encoders', 'Errors', 'Generator', - 'Header', 'Iterators', 'MIMEAudio', 'MIMEBase', - 'MIMEImage', 'MIMEMessage', 'MIMEMultipart', - 'MIMENonMultipart', 'MIMEText', 'Message', - 'Parser', 'Utils', 'base64MIME', - # new names - 'base64mime', 'charset', 'encoders', 'errors', 'generator', - 'header', 'iterators', 'message', 'message_from_file', - 'message_from_string', 'mime', 'parser', - 'quopriMIME', 'quoprimime', 'utils', - ]) - - def test_formatdate(self): - now = time.time() - self.assertEqual(Utils.parsedate(Utils.formatdate(now))[:6], - time.gmtime(now)[:6]) - - def test_formatdate_localtime(self): - now = time.time() - self.assertEqual( - Utils.parsedate(Utils.formatdate(now, localtime=True))[:6], - time.localtime(now)[:6]) - - def test_formatdate_usegmt(self): - now = time.time() - self.assertEqual( - Utils.formatdate(now, localtime=False), - time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now))) - self.assertEqual( - Utils.formatdate(now, localtime=False, usegmt=True), - time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now))) - - def test_parsedate_none(self): - self.assertEqual(Utils.parsedate(''), None) - - def test_parsedate_compact(self): - # The FWS after the comma is optional - self.assertEqual(Utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'), - Utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800')) - - def test_parsedate_no_dayofweek(self): - eq = self.assertEqual - eq(Utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'), - (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800)) - - def test_parsedate_compact_no_dayofweek(self): - eq = self.assertEqual - eq(Utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'), - (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800)) - - def test_parsedate_acceptable_to_time_functions(self): - eq = self.assertEqual - timetup = Utils.parsedate('5 Feb 2003 13:47:26 -0800') - t = int(time.mktime(timetup)) - eq(time.localtime(t)[:6], timetup[:6]) - eq(int(time.strftime('%Y', timetup)), 2003) - timetup = Utils.parsedate_tz('5 Feb 2003 13:47:26 -0800') - t = int(time.mktime(timetup[:9])) - eq(time.localtime(t)[:6], timetup[:6]) - eq(int(time.strftime('%Y', timetup[:9])), 2003) - - def test_parseaddr_empty(self): - self.assertEqual(Utils.parseaddr('<>'), ('', '')) - self.assertEqual(Utils.formataddr(Utils.parseaddr('<>')), '') - - def test_noquote_dump(self): - self.assertEqual( - Utils.formataddr(('A Silly Person', 'person@dom.ain')), - 'A Silly Person ') - - def test_escape_dump(self): - self.assertEqual( - Utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')), - r'"A \(Very\) Silly Person" ') - a = r'A \(Special\) Person' - b = 'person@dom.ain' - self.assertEqual(Utils.parseaddr(Utils.formataddr((a, b))), (a, b)) - - def test_escape_backslashes(self): - self.assertEqual( - Utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')), - r'"Arthur \\Backslash\\ Foobar" ') - a = r'Arthur \Backslash\ Foobar' - b = 'person@dom.ain' - self.assertEqual(Utils.parseaddr(Utils.formataddr((a, b))), (a, b)) - - def test_name_with_dot(self): - x = 'John X. Doe ' - y = '"John X. Doe" ' - a, b = ('John X. Doe', 'jxd@example.com') - self.assertEqual(Utils.parseaddr(x), (a, b)) - self.assertEqual(Utils.parseaddr(y), (a, b)) - # formataddr() quotes the name if there's a dot in it - self.assertEqual(Utils.formataddr((a, b)), y) - - def test_quote_dump(self): - self.assertEqual( - Utils.formataddr(('A Silly; Person', 'person@dom.ain')), - r'"A Silly; Person" ') - - def test_fix_eols(self): - eq = self.assertEqual - eq(Utils.fix_eols('hello'), 'hello') - eq(Utils.fix_eols('hello\n'), 'hello\r\n') - eq(Utils.fix_eols('hello\r'), 'hello\r\n') - eq(Utils.fix_eols('hello\r\n'), 'hello\r\n') - eq(Utils.fix_eols('hello\n\r'), 'hello\r\n\r\n') - - def test_charset_richcomparisons(self): - eq = self.assertEqual - ne = self.assertNotEqual - cset1 = Charset() - cset2 = Charset() - eq(cset1, 'us-ascii') - eq(cset1, 'US-ASCII') - eq(cset1, 'Us-AsCiI') - eq('us-ascii', cset1) - eq('US-ASCII', cset1) - eq('Us-AsCiI', cset1) - ne(cset1, 'usascii') - ne(cset1, 'USASCII') - ne(cset1, 'UsAsCiI') - ne('usascii', cset1) - ne('USASCII', cset1) - ne('UsAsCiI', cset1) - eq(cset1, cset2) - eq(cset2, cset1) - - def test_getaddresses(self): - eq = self.assertEqual - eq(Utils.getaddresses(['aperson@dom.ain (Al Person)', - 'Bud Person ']), - [('Al Person', 'aperson@dom.ain'), - ('Bud Person', 'bperson@dom.ain')]) - - def test_getaddresses_nasty(self): - eq = self.assertEqual - eq(Utils.getaddresses(['foo: ;']), [('', '')]) - eq(Utils.getaddresses( - ['[]*-- =~$']), - [('', ''), ('', ''), ('', '*--')]) - eq(Utils.getaddresses( - ['foo: ;', '"Jason R. Mastaler" ']), - [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]) - - def test_getaddresses_embedded_comment(self): - """Test proper handling of a nested comment""" - eq = self.assertEqual - addrs = Utils.getaddresses(['User ((nested comment)) ']) - eq(addrs[0][1], 'foo@bar.com') - - def test_utils_quote_unquote(self): - eq = self.assertEqual - msg = Message() - msg.add_header('content-disposition', 'attachment', - filename='foo\\wacky"name') - eq(msg.get_filename(), 'foo\\wacky"name') - - def test_get_body_encoding_with_bogus_charset(self): - charset = Charset('not a charset') - self.assertEqual(charset.get_body_encoding(), 'base64') - - def test_get_body_encoding_with_uppercase_charset(self): - eq = self.assertEqual - msg = Message() - msg['Content-Type'] = 'text/plain; charset=UTF-8' - eq(msg['content-type'], 'text/plain; charset=UTF-8') - charsets = msg.get_charsets() - eq(len(charsets), 1) - eq(charsets[0], 'utf-8') - charset = Charset(charsets[0]) - eq(charset.get_body_encoding(), 'base64') - msg.set_payload('hello world', charset=charset) - eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n') - eq(msg.get_payload(decode=True), 'hello world') - eq(msg['content-transfer-encoding'], 'base64') - # Try another one - msg = Message() - msg['Content-Type'] = 'text/plain; charset="US-ASCII"' - charsets = msg.get_charsets() - eq(len(charsets), 1) - eq(charsets[0], 'us-ascii') - charset = Charset(charsets[0]) - eq(charset.get_body_encoding(), Encoders.encode_7or8bit) - msg.set_payload('hello world', charset=charset) - eq(msg.get_payload(), 'hello world') - eq(msg['content-transfer-encoding'], '7bit') - - def test_charsets_case_insensitive(self): - lc = Charset('us-ascii') - uc = Charset('US-ASCII') - self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding()) - - def test_partial_falls_inside_message_delivery_status(self): - eq = self.ndiffAssertEqual - # The Parser interface provides chunks of data to FeedParser in 8192 - # byte gulps. SF bug #1076485 found one of those chunks inside - # message/delivery-status header block, which triggered an - # unreadline() of NeedMoreData. - msg = self._msgobj('msg_43.txt') - sfp = StringIO() - Iterators._structure(msg, sfp) - eq(sfp.getvalue(), """\ -multipart/report - text/plain - message/delivery-status - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/rfc822-headers -""") - - - -# Test the iterator/generators -class TestIterators(TestEmailBase): - def test_body_line_iterator(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - # First a simple non-multipart message - msg = self._msgobj('msg_01.txt') - it = Iterators.body_line_iterator(msg) - lines = list(it) - eq(len(lines), 6) - neq(EMPTYSTRING.join(lines), msg.get_payload()) - # Now a more complicated multipart - msg = self._msgobj('msg_02.txt') - it = Iterators.body_line_iterator(msg) - lines = list(it) - eq(len(lines), 43) - fp = openfile('msg_19.txt') - try: - neq(EMPTYSTRING.join(lines), fp.read()) - finally: - fp.close() - - def test_typed_subpart_iterator(self): - eq = self.assertEqual - msg = self._msgobj('msg_04.txt') - it = Iterators.typed_subpart_iterator(msg, 'text') - lines = [] - subparts = 0 - for subpart in it: - subparts += 1 - lines.append(subpart.get_payload()) - eq(subparts, 2) - eq(EMPTYSTRING.join(lines), """\ -a simple kind of mirror -to reflect upon our own -a simple kind of mirror -to reflect upon our own -""") - - def test_typed_subpart_iterator_default_type(self): - eq = self.assertEqual - msg = self._msgobj('msg_03.txt') - it = Iterators.typed_subpart_iterator(msg, 'text', 'plain') - lines = [] - subparts = 0 - for subpart in it: - subparts += 1 - lines.append(subpart.get_payload()) - eq(subparts, 1) - eq(EMPTYSTRING.join(lines), """\ - -Hi, - -Do you like this message? - --Me -""") - - - -class TestParsers(TestEmailBase): - def test_header_parser(self): - eq = self.assertEqual - # Parse only the headers of a complex multipart MIME document - fp = openfile('msg_02.txt') - try: - msg = HeaderParser().parse(fp) - finally: - fp.close() - eq(msg['from'], 'ppp-request@zzz.org') - eq(msg['to'], 'ppp@zzz.org') - eq(msg.get_content_type(), 'multipart/mixed') - self.assertFalse(msg.is_multipart()) - self.assertTrue(isinstance(msg.get_payload(), str)) - - def test_whitespace_continuation(self): - eq = self.assertEqual - # This message contains a line after the Subject: header that has only - # whitespace, but it is not empty! - msg = bongo.external.email.message_from_string("""\ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: the next line has a space on it -\x20 -Date: Mon, 8 Apr 2002 15:09:19 -0400 -Message-ID: spam - -Here's the message body -""") - eq(msg['subject'], 'the next line has a space on it\n ') - eq(msg['message-id'], 'spam') - eq(msg.get_payload(), "Here's the message body\n") - - def test_whitespace_continuation_last_header(self): - eq = self.assertEqual - # Like the previous test, but the subject line is the last - # header. - msg = bongo.external.email.message_from_string("""\ -From: aperson@dom.ain -To: bperson@dom.ain -Date: Mon, 8 Apr 2002 15:09:19 -0400 -Message-ID: spam -Subject: the next line has a space on it -\x20 - -Here's the message body -""") - eq(msg['subject'], 'the next line has a space on it\n ') - eq(msg['message-id'], 'spam') - eq(msg.get_payload(), "Here's the message body\n") - - def test_crlf_separation(self): - eq = self.assertEqual - fp = openfile('msg_26.txt', mode='rb') - try: - msg = Parser().parse(fp) - finally: - fp.close() - eq(len(msg.get_payload()), 2) - part1 = msg.get_payload(0) - eq(part1.get_content_type(), 'text/plain') - eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n') - part2 = msg.get_payload(1) - eq(part2.get_content_type(), 'application/riscos') - - def test_multipart_digest_with_extra_mime_headers(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - fp = openfile('msg_28.txt') - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - # Structure is: - # multipart/digest - # message/rfc822 - # text/plain - # message/rfc822 - # text/plain - eq(msg.is_multipart(), 1) - eq(len(msg.get_payload()), 2) - part1 = msg.get_payload(0) - eq(part1.get_content_type(), 'message/rfc822') - eq(part1.is_multipart(), 1) - eq(len(part1.get_payload()), 1) - part1a = part1.get_payload(0) - eq(part1a.is_multipart(), 0) - eq(part1a.get_content_type(), 'text/plain') - neq(part1a.get_payload(), 'message 1\n') - # next message/rfc822 - part2 = msg.get_payload(1) - eq(part2.get_content_type(), 'message/rfc822') - eq(part2.is_multipart(), 1) - eq(len(part2.get_payload()), 1) - part2a = part2.get_payload(0) - eq(part2a.is_multipart(), 0) - eq(part2a.get_content_type(), 'text/plain') - neq(part2a.get_payload(), 'message 2\n') - - def test_three_lines(self): - # A bug report by Andrew McNamara - lines = ['From: Andrew Person From', 'From']) - eq(msg.get_payload(), 'body') - - def test_rfc2822_space_not_allowed_in_header(self): - eq = self.assertEqual - m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody' - msg = bongo.external.email.message_from_string(m) - eq(len(list(msg.keys())), 0) - - def test_rfc2822_one_character_header(self): - eq = self.assertEqual - m = 'A: first header\nB: second header\nCC: third header\n\nbody' - msg = bongo.external.email.message_from_string(m) - headers = list(msg.keys()) - headers.sort() - eq(headers, ['A', 'B', 'CC']) - eq(msg.get_payload(), 'body') - - - -class TestBase64(unittest.TestCase): - def test_len(self): - eq = self.assertEqual - eq(base64MIME.base64_len('hello'), - len(base64MIME.encode('hello', eol=''))) - for size in range(15): - if size == 0 : bsize = 0 - elif size <= 3 : bsize = 4 - elif size <= 6 : bsize = 8 - elif size <= 9 : bsize = 12 - elif size <= 12: bsize = 16 - else : bsize = 20 - eq(base64MIME.base64_len('x'*size), bsize) - - def test_decode(self): - eq = self.assertEqual - eq(base64MIME.decode(''), '') - eq(base64MIME.decode('aGVsbG8='), 'hello') - eq(base64MIME.decode('aGVsbG8=', 'X'), 'hello') - eq(base64MIME.decode('aGVsbG8NCndvcmxk\n', 'X'), 'helloXworld') - - def test_encode(self): - eq = self.assertEqual - eq(base64MIME.encode(''), '') - eq(base64MIME.encode('hello'), 'aGVsbG8=\n') - # Test the binary flag - eq(base64MIME.encode('hello\n'), 'aGVsbG8K\n') - eq(base64MIME.encode('hello\n', 0), 'aGVsbG8NCg==\n') - # Test the maxlinelen arg - eq(base64MIME.encode('xxxx ' * 20, maxlinelen=40), """\ -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg -eHh4eCB4eHh4IA== -""") - # Test the eol argument - eq(base64MIME.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r -eHh4eCB4eHh4IA==\r -""") - - def test_header_encode(self): - eq = self.assertEqual - he = base64MIME.header_encode - eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=') - eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=') - # Test the charset option - eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=') - # Test the keep_eols flag - eq(he('hello\nworld', keep_eols=True), - '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=') - # Test the maxlinelen argument - eq(he('xxxx ' * 20, maxlinelen=40), """\ -=?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?= - =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?= - =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?= - =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?= - =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?= - =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""") - # Test the eol argument - eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -=?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?=\r - =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?=\r - =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?=\r - =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?=\r - =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?=\r - =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""") - - - -class TestQuopri(unittest.TestCase): - def setUp(self): - self.hlit = [chr(x) for x in range(ord('a'), ord('z')+1)] + \ - [chr(x) for x in range(ord('A'), ord('Z')+1)] + \ - [chr(x) for x in range(ord('0'), ord('9')+1)] + \ - ['!', '*', '+', '-', '/', ' '] - self.hnon = [chr(x) for x in range(256) if chr(x) not in self.hlit] - assert len(self.hlit) + len(self.hnon) == 256 - self.blit = [chr(x) for x in range(ord(' '), ord('~')+1)] + ['\t'] - self.blit.remove('=') - self.bnon = [chr(x) for x in range(256) if chr(x) not in self.blit] - assert len(self.blit) + len(self.bnon) == 256 - - def test_header_quopri_check(self): - for c in self.hlit: - self.assertFalse(quopriMIME.header_quopri_check(c)) - for c in self.hnon: - self.assertTrue(quopriMIME.header_quopri_check(c)) - - def test_body_quopri_check(self): - for c in self.blit: - self.assertFalse(quopriMIME.body_quopri_check(c)) - for c in self.bnon: - self.assertTrue(quopriMIME.body_quopri_check(c)) - - def test_header_quopri_len(self): - eq = self.assertEqual - hql = quopriMIME.header_quopri_len - enc = quopriMIME.header_encode - for s in ('hello', 'h@e@l@l@o@'): - # Empty charset and no line-endings. 7 == RFC chrome - eq(hql(s), len(enc(s, charset='', eol=''))-7) - for c in self.hlit: - eq(hql(c), 1) - for c in self.hnon: - eq(hql(c), 3) - - def test_body_quopri_len(self): - eq = self.assertEqual - bql = quopriMIME.body_quopri_len - for c in self.blit: - eq(bql(c), 1) - for c in self.bnon: - eq(bql(c), 3) - - def test_quote_unquote_idempotent(self): - for x in range(256): - c = chr(x) - self.assertEqual(quopriMIME.unquote(quopriMIME.quote(c)), c) - - def test_header_encode(self): - eq = self.assertEqual - he = quopriMIME.header_encode - eq(he('hello'), '=?iso-8859-1?q?hello?=') - eq(he('hello\nworld'), '=?iso-8859-1?q?hello=0D=0Aworld?=') - # Test the charset option - eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?q?hello?=') - # Test the keep_eols flag - eq(he('hello\nworld', keep_eols=True), '=?iso-8859-1?q?hello=0Aworld?=') - # Test a non-ASCII character - eq(he('hello\xc7there'), '=?iso-8859-1?q?hello=C7there?=') - # Test the maxlinelen argument - eq(he('xxxx ' * 20, maxlinelen=40), """\ -=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?= - =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?= - =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?= - =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?= - =?iso-8859-1?q?x_xxxx_xxxx_?=""") - # Test the eol argument - eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=\r - =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=\r - =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?=\r - =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?=\r - =?iso-8859-1?q?x_xxxx_xxxx_?=""") - - def test_decode(self): - eq = self.assertEqual - eq(quopriMIME.decode(''), '') - eq(quopriMIME.decode('hello'), 'hello') - eq(quopriMIME.decode('hello', 'X'), 'hello') - eq(quopriMIME.decode('hello\nworld', 'X'), 'helloXworld') - - def test_encode(self): - eq = self.assertEqual - eq(quopriMIME.encode(''), '') - eq(quopriMIME.encode('hello'), 'hello') - # Test the binary flag - eq(quopriMIME.encode('hello\r\nworld'), 'hello\nworld') - eq(quopriMIME.encode('hello\r\nworld', 0), 'hello\nworld') - # Test the maxlinelen arg - eq(quopriMIME.encode('xxxx ' * 20, maxlinelen=40), """\ -xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx= - xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx= -x xxxx xxxx xxxx xxxx=20""") - # Test the eol argument - eq(quopriMIME.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r - xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r -x xxxx xxxx xxxx xxxx=20""") - eq(quopriMIME.encode("""\ -one line - -two line"""), """\ -one line - -two line""") - - - -# Test the Charset class -class TestCharset(unittest.TestCase): - def tearDown(self): - from bongo.external.email import charset as CharsetModule - try: - del CharsetModule.CHARSETS['fake'] - except KeyError: - pass - - def test_idempotent(self): - eq = self.assertEqual - # Make sure us-ascii = no Unicode conversion - c = Charset('us-ascii') - s = 'Hello World!' - sp = c.to_splittable(s) - eq(s, c.from_splittable(sp)) - # test 8-bit idempotency with us-ascii - s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa' - sp = c.to_splittable(s) - eq(s, c.from_splittable(sp)) - - def test_body_encode(self): - eq = self.assertEqual - # Try a charset with QP body encoding - c = Charset('iso-8859-1') - eq('hello w=F6rld', c.body_encode('hello w\xf6rld')) - # Try a charset with Base64 body encoding - c = Charset('utf-8') - eq('aGVsbG8gd29ybGQ=\n', c.body_encode('hello world')) - # Try a charset with None body encoding - c = Charset('us-ascii') - eq('hello world', c.body_encode('hello world')) - # Try the convert argument, where input codec <> output codec - c = Charset('euc-jp') - # With apologies to Tokio Kikuchi ;) - try: - eq('\x1b$B5FCO;~IW\x1b(B', - c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7')) - eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', - c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False)) - except LookupError: - # We probably don't have the Japanese codecs installed - pass - # Testing SF bug #625509, which we have to fake, since there are no - # built-in encodings where the header encoding is QP but the body - # encoding is not. - from bongo.external.email import charset as CharsetModule - CharsetModule.add_charset('fake', CharsetModule.QP, None) - c = Charset('fake') - eq('hello w\xf6rld', c.body_encode('hello w\xf6rld')) - - def test_unicode_charset_name(self): - charset = Charset('us-ascii') - self.assertEqual(str(charset), 'us-ascii') - self.assertRaises(Errors.CharsetError, Charset, 'asc\xffii') - - - -# Test multilingual MIME headers. -class TestHeader(TestEmailBase): - def test_simple(self): - eq = self.ndiffAssertEqual - h = Header('Hello World!') - eq(h.encode(), 'Hello World!') - h.append(' Goodbye World!') - eq(h.encode(), 'Hello World! Goodbye World!') - - def test_simple_surprise(self): - eq = self.ndiffAssertEqual - h = Header('Hello World!') - eq(h.encode(), 'Hello World!') - h.append('Goodbye World!') - eq(h.encode(), 'Hello World! Goodbye World!') - - def test_header_needs_no_decoding(self): - h = 'no decoding needed' - self.assertEqual(decode_header(h), [(h, None)]) - - def test_long(self): - h = Header("I am the very model of a modern Major-General; I've information vegetable, animal, and mineral; I know the kings of England, and I quote the fights historical from Marathon to Waterloo, in order categorical; I'm very well acquainted, too, with matters mathematical; I understand equations, both the simple and quadratical; about binomial theorem I'm teeming with a lot o' news, with many cheerful facts about the square of the hypotenuse.", - maxlinelen=76) - for l in h.encode(splitchars=' ').split('\n '): - self.assertTrue(len(l) <= 76) - - def test_multilingual(self): - eq = self.ndiffAssertEqual - g = Charset("iso-8859-1") - cz = Charset("iso-8859-2") - utf8 = Charset("utf-8") - g_head = "Die Mieter treten hier ein werden mit einem Foerderband komfortabel den Korridor entlang, an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, gegen die rotierenden Klingen bef\xf6rdert. " - cz_head = "Finan\xe8ni metropole se hroutily pod tlakem jejich d\xf9vtipu.. " - utf8_head = "\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066\u3044\u307e\u3059\u3002".encode("utf-8") - h = Header(g_head, g) - h.append(cz_head, cz) - h.append(utf8_head, utf8) - enc = h.encode() - eq(enc, """\ -=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_ko?= - =?iso-8859-1?q?mfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wan?= - =?iso-8859-1?q?dgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6?= - =?iso-8859-1?q?rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?= - =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?= - =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?= - =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?= - =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?= - =?utf-8?q?_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das_Oder_die_Fl?= - =?utf-8?b?aXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBo+OBpuOBhOOBvuOBmQ==?= - =?utf-8?b?44CC?=""") - eq(decode_header(enc), - [(g_head, "iso-8859-1"), (cz_head, "iso-8859-2"), - (utf8_head, "utf-8")]) - ustr = str(h) - eq(ustr.encode('utf-8'), - 'Die Mieter treten hier ein werden mit einem Foerderband ' - 'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen ' - 'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen ' - 'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod ' - 'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81' - '\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3' - '\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3' - '\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83' - '\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e' - '\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3' - '\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82' - '\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b' - '\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git ' - 'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt ' - 'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81' - '\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82') - # Test make_header() - newh = make_header(decode_header(enc)) - eq(newh, enc) - - def test_header_ctor_default_args(self): - eq = self.ndiffAssertEqual - h = Header() - eq(h, '') - h.append('foo', Charset('iso-8859-1')) - eq(h, '=?iso-8859-1?q?foo?=') - - def test_explicit_maxlinelen(self): - eq = self.ndiffAssertEqual - hstr = 'A very long line that must get split to something other than at the 76th character boundary to test the non-default behavior' - h = Header(hstr) - eq(h.encode(), '''\ -A very long line that must get split to something other than at the 76th - character boundary to test the non-default behavior''') - h = Header(hstr, header_name='Subject') - eq(h.encode(), '''\ -A very long line that must get split to something other than at the - 76th character boundary to test the non-default behavior''') - h = Header(hstr, maxlinelen=1024, header_name='Subject') - eq(h.encode(), hstr) - - def test_us_ascii_header(self): - eq = self.assertEqual - s = 'hello' - x = decode_header(s) - eq(x, [('hello', None)]) - h = make_header(x) - eq(s, h.encode()) - - def test_string_charset(self): - eq = self.assertEqual - h = Header() - h.append('hello', 'iso-8859-1') - eq(h, '=?iso-8859-1?q?hello?=') - -## def test_unicode_error(self): -## raises = self.assertRaises -## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii') -## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii') -## h = Header() -## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii') -## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii') -## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1') - - def test_utf8_shortest(self): - eq = self.assertEqual - h = Header('p\xf6stal', 'utf-8') - eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=') - h = Header('\u83ca\u5730\u6642\u592b', 'utf-8') - eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=') - - def test_bad_8bit_header(self): - raises = self.assertRaises - eq = self.assertEqual - x = 'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big' - raises(UnicodeError, Header, x) - h = Header() - raises(UnicodeError, h.append, x) - eq(str(Header(x, errors='replace')), x) - h.append(x, errors='replace') - eq(str(h), x) - - def test_encoded_adjacent_nonencoded(self): - eq = self.assertEqual - h = Header() - h.append('hello', 'iso-8859-1') - h.append('world') - s = h.encode() - eq(s, '=?iso-8859-1?q?hello?= world') - h = make_header(decode_header(s)) - eq(h.encode(), s) - - def test_whitespace_eater(self): - eq = self.assertEqual - s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.' - parts = decode_header(s) - eq(parts, [('Subject:', None), ('\xf0\xd2\xcf\xd7\xc5\xd2\xcb\xc1 \xce\xc1 \xc6\xc9\xce\xc1\xcc\xd8\xce\xd9\xca', 'koi8-r'), ('zz.', None)]) - hdr = make_header(parts) - eq(hdr.encode(), - 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.') - - def test_broken_base64_header(self): - raises = self.assertRaises - s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3IQ?=' - raises(Errors.HeaderParseError, decode_header, s) - - - -# Test RFC 2231 header parameters (en/de)coding -class TestRFC2231(TestEmailBase): - def test_get_param(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_29.txt') - eq(msg.get_param('title'), - ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!')) - eq(msg.get_param('title', unquote=False), - ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"')) - - def test_set_param(self): - eq = self.ndiffAssertEqual - msg = Message() - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii') - eq(msg.get_param('title'), - ('us-ascii', '', 'This is even more ***fun*** isn\'t it!')) - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii', language='en') - eq(msg.get_param('title'), - ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!')) - msg = self._msgobj('msg_01.txt') - - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii', language='en') - - correct = """\ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) -\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 -Content-Type: text/plain; charset=us-ascii; -\ttitle*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21" - - -Hi, - -Do you like this message? - --Me -""" - eq(msg.as_string(), correct) - - def test_del_param(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_01.txt') - msg.set_param('foo', 'bar', charset='us-ascii', language='en') - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii', language='en') - msg.del_param('foo', header='Content-Type') - eq(msg.as_string(), """\ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) -\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 -Content-Type: text/plain; charset="us-ascii"; -\ttitle*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21" - - -Hi, - -Do you like this message? - --Me -""") - - def test_rfc2231_get_content_charset(self): - eq = self.assertEqual - msg = self._msgobj('msg_32.txt') - eq(msg.get_content_charset(), 'us-ascii') - - def test_rfc2231_no_language_or_charset(self): - m = '''\ -Content-Transfer-Encoding: 8bit -Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm" -Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm - -''' - msg = bongo.external.email.message_from_string(m) - param = msg.get_param('NAME') - self.assertFalse(isinstance(param, tuple)) - self.assertEqual( - param, - 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm') - - def test_rfc2231_no_language_or_charset_in_filename(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="''This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_no_language_or_charset_in_filename_encoded(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="''This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_partly_encoded(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0="''This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual( - msg.get_filename(), - 'This%20is%20even%20more%20***fun*** is it not.pdf') - - def test_rfc2231_partly_nonencoded(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0="This%20is%20even%20more%20"; -\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual( - msg.get_filename(), - 'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf') - - def test_rfc2231_no_language_or_charset_in_boundary(self): - m = '''\ -Content-Type: multipart/alternative; -\tboundary*0*="''This%20is%20even%20more%20"; -\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tboundary*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_boundary(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_no_language_or_charset_in_charset(self): - # This is a nonsensical charset value, but tests the code anyway - m = '''\ -Content-Type: text/plain; -\tcharset*0*="This%20is%20even%20more%20"; -\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tcharset*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_content_charset(), - 'this is even more ***fun*** is it not.pdf') - - def test_rfc2231_bad_encoding_in_filename(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="bogus'xx'This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_bad_encoding_in_charset(self): - m = """\ -Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D - -""" - msg = bongo.external.email.message_from_string(m) - # This should return None because non-ascii characters in the charset - # are not allowed. - self.assertEqual(msg.get_content_charset(), None) - - def test_rfc2231_bad_character_in_charset(self): - m = """\ -Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D - -""" - msg = bongo.external.email.message_from_string(m) - # This should return None because non-ascii characters in the charset - # are not allowed. - self.assertEqual(msg.get_content_charset(), None) - - def test_rfc2231_bad_character_in_filename(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="ascii'xx'This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2*="is it not.pdf%E2" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf\ufffd') - - def test_rfc2231_unknown_encoding(self): - m = """\ -Content-Transfer-Encoding: 8bit -Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt - -""" - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), 'myfile.txt') - - def test_rfc2231_single_tick_in_filename_extended(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0*=\"Frank's\"; name*1*=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, None) - eq(language, None) - eq(s, "Frank's Document") - - def test_rfc2231_single_tick_in_filename(self): - m = """\ -Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - param = msg.get_param('name') - self.assertFalse(isinstance(param, tuple)) - self.assertEqual(param, "Frank's Document") - - def test_rfc2231_tick_attack_extended(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, 'us-ascii') - eq(language, 'en-us') - eq(s, "Frank's Document") - - def test_rfc2231_tick_attack(self): - m = """\ -Content-Type: application/x-foo; -\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - param = msg.get_param('name') - self.assertFalse(isinstance(param, tuple)) - self.assertEqual(param, "us-ascii'en-us'Frank's Document") - - def test_rfc2231_no_extended_values(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; name=\"Frank's Document\" - -""" - msg = bongo.external.email.message_from_string(m) - eq(msg.get_param('name'), "Frank's Document") - - def test_rfc2231_encoded_then_unencoded_segments(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0*=\"us-ascii'en-us'My\"; -\tname*1=\" Document\"; -\tname*2*=\" For You\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, 'us-ascii') - eq(language, 'en-us') - eq(s, 'My Document For You') - - def test_rfc2231_unencoded_then_encoded_segments(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0=\"us-ascii'en-us'My\"; -\tname*1*=\" Document\"; -\tname*2*=\" For You\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, 'us-ascii') - eq(language, 'en-us') - eq(s, 'My Document For You') - - - -def _testclasses(): - mod = sys.modules[__name__] - return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')] - - -def suite(): - suite = unittest.TestSuite() - for testclass in _testclasses(): - suite.addTest(unittest.makeSuite(testclass)) - return suite - - -def test_main(): - for testclass in _testclasses(): - run_unittest(testclass) - - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/src/libs/python/bongo/external/email/test/test_email_codecs.py b/src/libs/python/bongo/external/email/test/test_email_codecs.py deleted file mode 100644 index 718df48..0000000 --- a/src/libs/python/bongo/external/email/test/test_email_codecs.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Contact: email-sig@python.org -# email package unit tests for (optional) Asian codecs - -import unittest -from test.test_support import TestSkipped, run_unittest - -from bongo.external.email.test.test_email import TestEmailBase -from bongo.external.email.Charset import Charset -from bongo.external.email.Header import Header, decode_header -from bongo.external.email.Message import Message - -# We're compatible with Python 2.3, but it doesn't have the built-in Asian -# codecs, so we have to skip all these tests. -try: - str('foo', 'euc-jp') -except LookupError: - raise TestSkipped - - - -class TestEmailAsianCodecs(TestEmailBase): - def test_japanese_codecs(self): - eq = self.ndiffAssertEqual - j = Charset("euc-jp") - g = Charset("iso-8859-1") - h = Header("Hello World!") - jhello = '\xa5\xcf\xa5\xed\xa1\xbc\xa5\xef\xa1\xbc\xa5\xeb\xa5\xc9\xa1\xaa' - ghello = 'Gr\xfc\xdf Gott!' - h.append(jhello, j) - h.append(ghello, g) - # BAW: This used to -- and maybe should -- fold the two iso-8859-1 - # chunks into a single encoded word. However it doesn't violate the - # standard to have them as two encoded chunks and maybe it's - # reasonable for each .append() call to result in a separate - # encoded word. - eq(h.encode(), """\ -Hello World! =?iso-2022-jp?b?GyRCJU8lbSE8JW8hPCVrJUkhKhsoQg==?= - =?iso-8859-1?q?Gr=FC=DF?= =?iso-8859-1?q?_Gott!?=""") - eq(decode_header(h.encode()), - [('Hello World!', None), - ('\x1b$B%O%m!<%o!<%k%I!*\x1b(B', 'iso-2022-jp'), - ('Gr\xfc\xdf Gott!', 'iso-8859-1')]) - long = 'test-ja \xa4\xd8\xc5\xea\xb9\xc6\xa4\xb5\xa4\xec\xa4\xbf\xa5\xe1\xa1\xbc\xa5\xeb\xa4\xcf\xbb\xca\xb2\xf1\xbc\xd4\xa4\xce\xbe\xb5\xc7\xa7\xa4\xf2\xc2\xd4\xa4\xc3\xa4\xc6\xa4\xa4\xa4\xde\xa4\xb9' - h = Header(int, j, header_name="Subject") - # test a very long header - enc = h.encode() - # TK: splitting point may differ by codec design and/or Header encoding - eq(enc , """\ -=?iso-2022-jp?b?dGVzdC1qYSAbJEIkWEVqOUYkNSRsJD8lYSE8JWskTztKGyhC?= - =?iso-2022-jp?b?GyRCMnE8VCROPjVHJyRyQlQkQyRGJCQkXiQ5GyhC?=""") - # TK: full decode comparison - eq(h.__unicode__().encode('euc-jp'), int) - - def test_payload_encoding(self): - jhello = '\xa5\xcf\xa5\xed\xa1\xbc\xa5\xef\xa1\xbc\xa5\xeb\xa5\xc9\xa1\xaa' - jcode = 'euc-jp' - msg = Message() - msg.set_payload(jhello, jcode) - ustr = str(msg.get_payload(), msg.get_content_charset()) - self.assertEqual(jhello, ustr.encode(jcode)) - - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(TestEmailAsianCodecs)) - return suite - - -def test_main(): - run_unittest(TestEmailAsianCodecs) - - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/src/libs/python/bongo/external/email/test/test_email_codecs_renamed.py b/src/libs/python/bongo/external/email/test/test_email_codecs_renamed.py deleted file mode 100644 index d033437..0000000 --- a/src/libs/python/bongo/external/email/test/test_email_codecs_renamed.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Contact: email-sig@python.org -# email package unit tests for (optional) Asian codecs - -import unittest -from test.test_support import TestSkipped, run_unittest - -from bongo.external.email.test.test_email import TestEmailBase -from bongo.external.email.charset import Charset -from bongo.external.email.header import Header, decode_header -from bongo.external.email.message import Message - -# We're compatible with Python 2.3, but it doesn't have the built-in Asian -# codecs, so we have to skip all these tests. -try: - str('foo', 'euc-jp') -except LookupError: - raise TestSkipped - - - -class TestEmailAsianCodecs(TestEmailBase): - def test_japanese_codecs(self): - eq = self.ndiffAssertEqual - j = Charset("euc-jp") - g = Charset("iso-8859-1") - h = Header("Hello World!") - jhello = '\xa5\xcf\xa5\xed\xa1\xbc\xa5\xef\xa1\xbc\xa5\xeb\xa5\xc9\xa1\xaa' - ghello = 'Gr\xfc\xdf Gott!' - h.append(jhello, j) - h.append(ghello, g) - # BAW: This used to -- and maybe should -- fold the two iso-8859-1 - # chunks into a single encoded word. However it doesn't violate the - # standard to have them as two encoded chunks and maybe it's - # reasonable for each .append() call to result in a separate - # encoded word. - eq(h.encode(), """\ -Hello World! =?iso-2022-jp?b?GyRCJU8lbSE8JW8hPCVrJUkhKhsoQg==?= - =?iso-8859-1?q?Gr=FC=DF?= =?iso-8859-1?q?_Gott!?=""") - eq(decode_header(h.encode()), - [('Hello World!', None), - ('\x1b$B%O%m!<%o!<%k%I!*\x1b(B', 'iso-2022-jp'), - ('Gr\xfc\xdf Gott!', 'iso-8859-1')]) - long = 'test-ja \xa4\xd8\xc5\xea\xb9\xc6\xa4\xb5\xa4\xec\xa4\xbf\xa5\xe1\xa1\xbc\xa5\xeb\xa4\xcf\xbb\xca\xb2\xf1\xbc\xd4\xa4\xce\xbe\xb5\xc7\xa7\xa4\xf2\xc2\xd4\xa4\xc3\xa4\xc6\xa4\xa4\xa4\xde\xa4\xb9' - h = Header(int, j, header_name="Subject") - # test a very long header - enc = h.encode() - # TK: splitting point may differ by codec design and/or Header encoding - eq(enc , """\ -=?iso-2022-jp?b?dGVzdC1qYSAbJEIkWEVqOUYkNSRsJD8lYSE8JWskTztKGyhC?= - =?iso-2022-jp?b?GyRCMnE8VCROPjVHJyRyQlQkQyRGJCQkXiQ5GyhC?=""") - # TK: full decode comparison - eq(h.__unicode__().encode('euc-jp'), int) - - def test_payload_encoding(self): - jhello = '\xa5\xcf\xa5\xed\xa1\xbc\xa5\xef\xa1\xbc\xa5\xeb\xa5\xc9\xa1\xaa' - jcode = 'euc-jp' - msg = Message() - msg.set_payload(jhello, jcode) - ustr = str(msg.get_payload(), msg.get_content_charset()) - self.assertEqual(jhello, ustr.encode(jcode)) - - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(TestEmailAsianCodecs)) - return suite - - -def test_main(): - run_unittest(TestEmailAsianCodecs) - - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/src/libs/python/bongo/external/email/test/test_email_renamed.py b/src/libs/python/bongo/external/email/test/test_email_renamed.py deleted file mode 100644 index 27a6b27..0000000 --- a/src/libs/python/bongo/external/email/test/test_email_renamed.py +++ /dev/null @@ -1,3255 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Contact: email-sig@python.org -# email package unit tests - -import os -import sys -import time -import base64 -import difflib -import unittest -import warnings -from io import StringIO - -import email - -from bongo.external.email.charset import Charset -from bongo.external.email.header import Header, decode_header, make_header -from bongo.external.email.parser import Parser, HeaderParser -from bongo.external.email.generator import Generator, DecodedGenerator -from bongo.external.email.message import Message -from bongo.external.email.mime.application import MIMEApplication -from bongo.external.email.mime.audio import MIMEAudio -from bongo.external.email.mime.text import MIMEText -from bongo.external.email.mime.image import MIMEImage -from bongo.external.email.mime.base import MIMEBase -from bongo.external.email.mime.message import MIMEMessage -from bongo.external.email.mime.multipart import MIMEMultipart -from bongo.external.email import utils -from bongo.external.email import errors -from bongo.external.email import encoders -from bongo.external.email import iterators -from bongo.external.email import base64mime -from bongo.external.email import quoprimime - -from test.test_support import findfile, run_unittest -from bongo.external.email.test import __file__ as landmark - - -NL = '\n' -EMPTYSTRING = '' -SPACE = ' ' - - - -def openfile(filename, mode='r'): - path = os.path.join(os.path.dirname(landmark), 'data', filename) - return open(path, mode) - - - -# Base test class -class TestEmailBase(unittest.TestCase): - def ndiffAssertEqual(self, first, second): - """Like failUnlessEqual except use ndiff for readable output.""" - if first != second: - sfirst = str(first) - ssecond = str(second) - diff = difflib.ndiff(sfirst.splitlines(), ssecond.splitlines()) - fp = StringIO() - print(NL, NL.join(diff), file=fp) - raise self.failureException(fp.getvalue()) - - def _msgobj(self, filename): - fp = openfile(findfile(filename)) - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - return msg - - - -# Test various aspects of the Message class's API -class TestMessageAPI(TestEmailBase): - def test_get_all(self): - eq = self.assertEqual - msg = self._msgobj('msg_20.txt') - eq(msg.get_all('cc'), ['ccc@zzz.org', 'ddd@zzz.org', 'eee@zzz.org']) - eq(msg.get_all('xx', 'n/a'), 'n/a') - - def test_getset_charset(self): - eq = self.assertEqual - msg = Message() - eq(msg.get_charset(), None) - charset = Charset('iso-8859-1') - msg.set_charset(charset) - eq(msg['mime-version'], '1.0') - eq(msg.get_content_type(), 'text/plain') - eq(msg['content-type'], 'text/plain; charset="iso-8859-1"') - eq(msg.get_param('charset'), 'iso-8859-1') - eq(msg['content-transfer-encoding'], 'quoted-printable') - eq(msg.get_charset().input_charset, 'iso-8859-1') - # Remove the charset - msg.set_charset(None) - eq(msg.get_charset(), None) - eq(msg['content-type'], 'text/plain') - # Try adding a charset when there's already MIME headers present - msg = Message() - msg['MIME-Version'] = '2.0' - msg['Content-Type'] = 'text/x-weird' - msg['Content-Transfer-Encoding'] = 'quinted-puntable' - msg.set_charset(charset) - eq(msg['mime-version'], '2.0') - eq(msg['content-type'], 'text/x-weird; charset="iso-8859-1"') - eq(msg['content-transfer-encoding'], 'quinted-puntable') - - def test_set_charset_from_string(self): - eq = self.assertEqual - msg = Message() - msg.set_charset('us-ascii') - eq(msg.get_charset().input_charset, 'us-ascii') - eq(msg['content-type'], 'text/plain; charset="us-ascii"') - - def test_set_payload_with_charset(self): - msg = Message() - charset = Charset('iso-8859-1') - msg.set_payload('This is a string payload', charset) - self.assertEqual(msg.get_charset().input_charset, 'iso-8859-1') - - def test_get_charsets(self): - eq = self.assertEqual - - msg = self._msgobj('msg_08.txt') - charsets = msg.get_charsets() - eq(charsets, [None, 'us-ascii', 'iso-8859-1', 'iso-8859-2', 'koi8-r']) - - msg = self._msgobj('msg_09.txt') - charsets = msg.get_charsets('dingbat') - eq(charsets, ['dingbat', 'us-ascii', 'iso-8859-1', 'dingbat', - 'koi8-r']) - - msg = self._msgobj('msg_12.txt') - charsets = msg.get_charsets() - eq(charsets, [None, 'us-ascii', 'iso-8859-1', None, 'iso-8859-2', - 'iso-8859-3', 'us-ascii', 'koi8-r']) - - def test_get_filename(self): - eq = self.assertEqual - - msg = self._msgobj('msg_04.txt') - filenames = [p.get_filename() for p in msg.get_payload()] - eq(filenames, ['msg.txt', 'msg.txt']) - - msg = self._msgobj('msg_07.txt') - subpart = msg.get_payload(1) - eq(subpart.get_filename(), 'dingusfish.gif') - - def test_get_filename_with_name_parameter(self): - eq = self.assertEqual - - msg = self._msgobj('msg_44.txt') - filenames = [p.get_filename() for p in msg.get_payload()] - eq(filenames, ['msg.txt', 'msg.txt']) - - def test_get_boundary(self): - eq = self.assertEqual - msg = self._msgobj('msg_07.txt') - # No quotes! - eq(msg.get_boundary(), 'BOUNDARY') - - def test_set_boundary(self): - eq = self.assertEqual - # This one has no existing boundary parameter, but the Content-Type: - # header appears fifth. - msg = self._msgobj('msg_01.txt') - msg.set_boundary('BOUNDARY') - header, value = list(msg.items())[4] - eq(header.lower(), 'content-type') - eq(value, 'text/plain; charset="us-ascii"; boundary="BOUNDARY"') - # This one has a Content-Type: header, with a boundary, stuck in the - # middle of its headers. Make sure the order is preserved; it should - # be fifth. - msg = self._msgobj('msg_04.txt') - msg.set_boundary('BOUNDARY') - header, value = list(msg.items())[4] - eq(header.lower(), 'content-type') - eq(value, 'multipart/mixed; boundary="BOUNDARY"') - # And this one has no Content-Type: header at all. - msg = self._msgobj('msg_03.txt') - self.assertRaises(errors.HeaderParseError, - msg.set_boundary, 'BOUNDARY') - - def test_get_decoded_payload(self): - eq = self.assertEqual - msg = self._msgobj('msg_10.txt') - # The outer message is a multipart - eq(msg.get_payload(decode=True), None) - # Subpart 1 is 7bit encoded - eq(msg.get_payload(0).get_payload(decode=True), - 'This is a 7bit encoded message.\n') - # Subpart 2 is quopri - eq(msg.get_payload(1).get_payload(decode=True), - '\xa1This is a Quoted Printable encoded message!\n') - # Subpart 3 is base64 - eq(msg.get_payload(2).get_payload(decode=True), - 'This is a Base64 encoded message.') - # Subpart 4 has no Content-Transfer-Encoding: header. - eq(msg.get_payload(3).get_payload(decode=True), - 'This has no Content-Transfer-Encoding: header.\n') - - def test_get_decoded_uu_payload(self): - eq = self.assertEqual - msg = Message() - msg.set_payload('begin 666 -\n+:&5L;&\\@=V]R;&0 \n \nend\n') - for cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'): - msg['content-transfer-encoding'] = cte - eq(msg.get_payload(decode=True), 'hello world') - # Now try some bogus data - msg.set_payload('foo') - eq(msg.get_payload(decode=True), 'foo') - - def test_decoded_generator(self): - eq = self.assertEqual - msg = self._msgobj('msg_07.txt') - fp = openfile('msg_17.txt') - try: - text = fp.read() - finally: - fp.close() - s = StringIO() - g = DecodedGenerator(s) - g.flatten(msg) - eq(s.getvalue(), text) - - def test__contains__(self): - msg = Message() - msg['From'] = 'Me' - msg['to'] = 'You' - # Check for case insensitivity - self.assertTrue('from' in msg) - self.assertTrue('From' in msg) - self.assertTrue('FROM' in msg) - self.assertTrue('to' in msg) - self.assertTrue('To' in msg) - self.assertTrue('TO' in msg) - - def test_as_string(self): - eq = self.assertEqual - msg = self._msgobj('msg_01.txt') - fp = openfile('msg_01.txt') - try: - text = fp.read() - finally: - fp.close() - eq(text, msg.as_string()) - fullrepr = str(msg) - lines = fullrepr.split('\n') - self.assertTrue(lines[0].startswith('From ')) - eq(text, NL.join(lines[1:])) - - def test_bad_param(self): - msg = bongo.external.email.message_from_string("Content-Type: blarg; baz; boo\n") - self.assertEqual(msg.get_param('baz'), '') - - def test_missing_filename(self): - msg = bongo.external.email.message_from_string("From: foo\n") - self.assertEqual(msg.get_filename(), None) - - def test_bogus_filename(self): - msg = bongo.external.email.message_from_string( - "Content-Disposition: blarg; filename\n") - self.assertEqual(msg.get_filename(), '') - - def test_missing_boundary(self): - msg = bongo.external.email.message_from_string("From: foo\n") - self.assertEqual(msg.get_boundary(), None) - - def test_get_params(self): - eq = self.assertEqual - msg = bongo.external.email.message_from_string( - 'X-Header: foo=one; bar=two; baz=three\n') - eq(msg.get_params(header='x-header'), - [('foo', 'one'), ('bar', 'two'), ('baz', 'three')]) - msg = bongo.external.email.message_from_string( - 'X-Header: foo; bar=one; baz=two\n') - eq(msg.get_params(header='x-header'), - [('foo', ''), ('bar', 'one'), ('baz', 'two')]) - eq(msg.get_params(), None) - msg = bongo.external.email.message_from_string( - 'X-Header: foo; bar="one"; baz=two\n') - eq(msg.get_params(header='x-header'), - [('foo', ''), ('bar', 'one'), ('baz', 'two')]) - - def test_get_param_liberal(self): - msg = Message() - msg['Content-Type'] = 'Content-Type: Multipart/mixed; boundary = "CPIMSSMTPC06p5f3tG"' - self.assertEqual(msg.get_param('boundary'), 'CPIMSSMTPC06p5f3tG') - - def test_get_param(self): - eq = self.assertEqual - msg = bongo.external.email.message_from_string( - "X-Header: foo=one; bar=two; baz=three\n") - eq(msg.get_param('bar', header='x-header'), 'two') - eq(msg.get_param('quuz', header='x-header'), None) - eq(msg.get_param('quuz'), None) - msg = bongo.external.email.message_from_string( - 'X-Header: foo; bar="one"; baz=two\n') - eq(msg.get_param('foo', header='x-header'), '') - eq(msg.get_param('bar', header='x-header'), 'one') - eq(msg.get_param('baz', header='x-header'), 'two') - # XXX: We are not RFC-2045 compliant! We cannot parse: - # msg["Content-Type"] = 'text/plain; weird="hey; dolly? [you] @ <\\"home\\">?"' - # msg.get_param("weird") - # yet. - - def test_get_param_funky_continuation_lines(self): - msg = self._msgobj('msg_22.txt') - self.assertEqual(msg.get_payload(1).get_param('name'), 'wibble.JPG') - - def test_get_param_with_semis_in_quotes(self): - msg = bongo.external.email.message_from_string( - 'Content-Type: image/pjpeg; name="Jim&&Jill"\n') - self.assertEqual(msg.get_param('name'), 'Jim&&Jill') - self.assertEqual(msg.get_param('name', unquote=False), - '"Jim&&Jill"') - - def test_has_key(self): - msg = bongo.external.email.message_from_string('Header: exists') - self.assertTrue('header' in msg) - self.assertTrue('Header' in msg) - self.assertTrue('HEADER' in msg) - self.assertFalse('headeri' in msg) - - def test_set_param(self): - eq = self.assertEqual - msg = Message() - msg.set_param('charset', 'iso-2022-jp') - eq(msg.get_param('charset'), 'iso-2022-jp') - msg.set_param('importance', 'high value') - eq(msg.get_param('importance'), 'high value') - eq(msg.get_param('importance', unquote=False), '"high value"') - eq(msg.get_params(), [('text/plain', ''), - ('charset', 'iso-2022-jp'), - ('importance', 'high value')]) - eq(msg.get_params(unquote=False), [('text/plain', ''), - ('charset', '"iso-2022-jp"'), - ('importance', '"high value"')]) - msg.set_param('charset', 'iso-9999-xx', header='X-Jimmy') - eq(msg.get_param('charset', header='X-Jimmy'), 'iso-9999-xx') - - def test_del_param(self): - eq = self.assertEqual - msg = self._msgobj('msg_05.txt') - eq(msg.get_params(), - [('multipart/report', ''), ('report-type', 'delivery-status'), - ('boundary', 'D1690A7AC1.996856090/mail.example.com')]) - old_val = msg.get_param("report-type") - msg.del_param("report-type") - eq(msg.get_params(), - [('multipart/report', ''), - ('boundary', 'D1690A7AC1.996856090/mail.example.com')]) - msg.set_param("report-type", old_val) - eq(msg.get_params(), - [('multipart/report', ''), - ('boundary', 'D1690A7AC1.996856090/mail.example.com'), - ('report-type', old_val)]) - - def test_del_param_on_other_header(self): - msg = Message() - msg.add_header('Content-Disposition', 'attachment', filename='bud.gif') - msg.del_param('filename', 'content-disposition') - self.assertEqual(msg['content-disposition'], 'attachment') - - def test_set_type(self): - eq = self.assertEqual - msg = Message() - self.assertRaises(ValueError, msg.set_type, 'text') - msg.set_type('text/plain') - eq(msg['content-type'], 'text/plain') - msg.set_param('charset', 'us-ascii') - eq(msg['content-type'], 'text/plain; charset="us-ascii"') - msg.set_type('text/html') - eq(msg['content-type'], 'text/html; charset="us-ascii"') - - def test_set_type_on_other_header(self): - msg = Message() - msg['X-Content-Type'] = 'text/plain' - msg.set_type('application/octet-stream', 'X-Content-Type') - self.assertEqual(msg['x-content-type'], 'application/octet-stream') - - def test_get_content_type_missing(self): - msg = Message() - self.assertEqual(msg.get_content_type(), 'text/plain') - - def test_get_content_type_missing_with_default_type(self): - msg = Message() - msg.set_default_type('message/rfc822') - self.assertEqual(msg.get_content_type(), 'message/rfc822') - - def test_get_content_type_from_message_implicit(self): - msg = self._msgobj('msg_30.txt') - self.assertEqual(msg.get_payload(0).get_content_type(), - 'message/rfc822') - - def test_get_content_type_from_message_explicit(self): - msg = self._msgobj('msg_28.txt') - self.assertEqual(msg.get_payload(0).get_content_type(), - 'message/rfc822') - - def test_get_content_type_from_message_text_plain_implicit(self): - msg = self._msgobj('msg_03.txt') - self.assertEqual(msg.get_content_type(), 'text/plain') - - def test_get_content_type_from_message_text_plain_explicit(self): - msg = self._msgobj('msg_01.txt') - self.assertEqual(msg.get_content_type(), 'text/plain') - - def test_get_content_maintype_missing(self): - msg = Message() - self.assertEqual(msg.get_content_maintype(), 'text') - - def test_get_content_maintype_missing_with_default_type(self): - msg = Message() - msg.set_default_type('message/rfc822') - self.assertEqual(msg.get_content_maintype(), 'message') - - def test_get_content_maintype_from_message_implicit(self): - msg = self._msgobj('msg_30.txt') - self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message') - - def test_get_content_maintype_from_message_explicit(self): - msg = self._msgobj('msg_28.txt') - self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message') - - def test_get_content_maintype_from_message_text_plain_implicit(self): - msg = self._msgobj('msg_03.txt') - self.assertEqual(msg.get_content_maintype(), 'text') - - def test_get_content_maintype_from_message_text_plain_explicit(self): - msg = self._msgobj('msg_01.txt') - self.assertEqual(msg.get_content_maintype(), 'text') - - def test_get_content_subtype_missing(self): - msg = Message() - self.assertEqual(msg.get_content_subtype(), 'plain') - - def test_get_content_subtype_missing_with_default_type(self): - msg = Message() - msg.set_default_type('message/rfc822') - self.assertEqual(msg.get_content_subtype(), 'rfc822') - - def test_get_content_subtype_from_message_implicit(self): - msg = self._msgobj('msg_30.txt') - self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822') - - def test_get_content_subtype_from_message_explicit(self): - msg = self._msgobj('msg_28.txt') - self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822') - - def test_get_content_subtype_from_message_text_plain_implicit(self): - msg = self._msgobj('msg_03.txt') - self.assertEqual(msg.get_content_subtype(), 'plain') - - def test_get_content_subtype_from_message_text_plain_explicit(self): - msg = self._msgobj('msg_01.txt') - self.assertEqual(msg.get_content_subtype(), 'plain') - - def test_get_content_maintype_error(self): - msg = Message() - msg['Content-Type'] = 'no-slash-in-this-string' - self.assertEqual(msg.get_content_maintype(), 'text') - - def test_get_content_subtype_error(self): - msg = Message() - msg['Content-Type'] = 'no-slash-in-this-string' - self.assertEqual(msg.get_content_subtype(), 'plain') - - def test_replace_header(self): - eq = self.assertEqual - msg = Message() - msg.add_header('First', 'One') - msg.add_header('Second', 'Two') - msg.add_header('Third', 'Three') - eq(list(msg.keys()), ['First', 'Second', 'Third']) - eq(list(msg.values()), ['One', 'Two', 'Three']) - msg.replace_header('Second', 'Twenty') - eq(list(msg.keys()), ['First', 'Second', 'Third']) - eq(list(msg.values()), ['One', 'Twenty', 'Three']) - msg.add_header('First', 'Eleven') - msg.replace_header('First', 'One Hundred') - eq(list(msg.keys()), ['First', 'Second', 'Third', 'First']) - eq(list(msg.values()), ['One Hundred', 'Twenty', 'Three', 'Eleven']) - self.assertRaises(KeyError, msg.replace_header, 'Fourth', 'Missing') - - def test_broken_base64_payload(self): - x = 'AwDp0P7//y6LwKEAcPa/6Q=9' - msg = Message() - msg['content-type'] = 'audio/x-midi' - msg['content-transfer-encoding'] = 'base64' - msg.set_payload(x) - self.assertEqual(msg.get_payload(decode=True), x) - - - -# Test the bongo.external.email.encoders module -class TestEncoders(unittest.TestCase): - def test_encode_empty_payload(self): - eq = self.assertEqual - msg = Message() - msg.set_charset('us-ascii') - eq(msg['content-transfer-encoding'], '7bit') - - def test_default_cte(self): - eq = self.assertEqual - msg = MIMEText('hello world') - eq(msg['content-transfer-encoding'], '7bit') - - def test_default_cte(self): - eq = self.assertEqual - # With no explicit _charset its us-ascii, and all are 7-bit - msg = MIMEText('hello world') - eq(msg['content-transfer-encoding'], '7bit') - # Similar, but with 8-bit data - msg = MIMEText('hello \xf8 world') - eq(msg['content-transfer-encoding'], '8bit') - # And now with a different charset - msg = MIMEText('hello \xf8 world', _charset='iso-8859-1') - eq(msg['content-transfer-encoding'], 'quoted-printable') - - - -# Test long header wrapping -class TestLongHeaders(TestEmailBase): - def test_split_long_continuation(self): - eq = self.ndiffAssertEqual - msg = bongo.external.email.message_from_string("""\ -Subject: bug demonstration -\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 -\tmore text - -test -""") - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), """\ -Subject: bug demonstration -\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 -\tmore text - -test -""") - - def test_another_long_almost_unsplittable_header(self): - eq = self.ndiffAssertEqual - hstr = """\ -bug demonstration -\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 -\tmore text""" - h = Header(hstr, continuation_ws='\t') - eq(h.encode(), """\ -bug demonstration -\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 -\tmore text""") - h = Header(hstr) - eq(h.encode(), """\ -bug demonstration - 12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 - more text""") - - def test_long_nonstring(self): - eq = self.ndiffAssertEqual - g = Charset("iso-8859-1") - cz = Charset("iso-8859-2") - utf8 = Charset("utf-8") - g_head = "Die Mieter treten hier ein werden mit einem Foerderband komfortabel den Korridor entlang, an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, gegen die rotierenden Klingen bef\xf6rdert. " - cz_head = "Finan\xe8ni metropole se hroutily pod tlakem jejich d\xf9vtipu.. " - utf8_head = "\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066\u3044\u307e\u3059\u3002".encode("utf-8") - h = Header(g_head, g, header_name='Subject') - h.append(cz_head, cz) - h.append(utf8_head, utf8) - msg = Message() - msg['Subject'] = h - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), """\ -Subject: =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerd?= - =?iso-8859-1?q?erband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndi?= - =?iso-8859-1?q?schen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Kling?= - =?iso-8859-1?q?en_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_met?= - =?iso-8859-2?q?ropole_se_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= - =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE?= - =?utf-8?b?44G+44Gb44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB?= - =?utf-8?b?44GC44Go44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CM?= - =?utf-8?q?Wenn_ist_das_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das?= - =?utf-8?b?IE9kZXIgZGllIEZsaXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBow==?= - =?utf-8?b?44Gm44GE44G+44GZ44CC?= - -""") - eq(h.encode(), """\ -=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerd?= - =?iso-8859-1?q?erband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndi?= - =?iso-8859-1?q?schen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Kling?= - =?iso-8859-1?q?en_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_met?= - =?iso-8859-2?q?ropole_se_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= - =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE?= - =?utf-8?b?44G+44Gb44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB?= - =?utf-8?b?44GC44Go44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CM?= - =?utf-8?q?Wenn_ist_das_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das?= - =?utf-8?b?IE9kZXIgZGllIEZsaXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBow==?= - =?utf-8?b?44Gm44GE44G+44GZ44CC?=""") - - def test_long_header_encode(self): - eq = self.ndiffAssertEqual - h = Header('wasnipoop; giraffes="very-long-necked-animals"; ' - 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"', - header_name='X-Foobar-Spoink-Defrobnit') - eq(h.encode(), '''\ -wasnipoop; giraffes="very-long-necked-animals"; - spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''') - - def test_long_header_encode_with_tab_continuation(self): - eq = self.ndiffAssertEqual - h = Header('wasnipoop; giraffes="very-long-necked-animals"; ' - 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"', - header_name='X-Foobar-Spoink-Defrobnit', - continuation_ws='\t') - eq(h.encode(), '''\ -wasnipoop; giraffes="very-long-necked-animals"; -\tspooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''') - - def test_header_splitter(self): - eq = self.ndiffAssertEqual - msg = MIMEText('') - # It'd be great if we could use add_header() here, but that doesn't - # guarantee an order of the parameters. - msg['X-Foobar-Spoink-Defrobnit'] = ( - 'wasnipoop; giraffes="very-long-necked-animals"; ' - 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"') - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), '''\ -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -X-Foobar-Spoink-Defrobnit: wasnipoop; giraffes="very-long-necked-animals"; -\tspooge="yummy"; hippos="gargantuan"; marshmallows="gooey" - -''') - - def test_no_semis_header_splitter(self): - eq = self.ndiffAssertEqual - msg = Message() - msg['From'] = 'test@dom.ain' - msg['References'] = SPACE.join(['<%d@dom.ain>' % i for i in range(10)]) - msg.set_payload('Test') - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), """\ -From: test@dom.ain -References: <0@dom.ain> <1@dom.ain> <2@dom.ain> <3@dom.ain> <4@dom.ain> -\t<5@dom.ain> <6@dom.ain> <7@dom.ain> <8@dom.ain> <9@dom.ain> - -Test""") - - def test_no_split_long_header(self): - eq = self.ndiffAssertEqual - hstr = 'References: ' + 'x' * 80 - h = Header(hstr, continuation_ws='\t') - eq(h.encode(), """\ -References: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx""") - - def test_splitting_multiple_long_lines(self): - eq = self.ndiffAssertEqual - hstr = """\ -from babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for ; Sat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for ; Sat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for ; Sat, 2 Feb 2002 17:00:06 -0800 (PST) -""" - h = Header(hstr, continuation_ws='\t') - eq(h.encode(), """\ -from babylon.socal-raves.org (localhost [127.0.0.1]); -\tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; -\tfor ; -\tSat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); -\tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; -\tfor ; -\tSat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); -\tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; -\tfor ; -\tSat, 2 Feb 2002 17:00:06 -0800 (PST)""") - - def test_splitting_first_line_only_is_long(self): - eq = self.ndiffAssertEqual - hstr = """\ -from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] helo=cthulhu.gerg.ca) -\tby kronos.mems-exchange.org with esmtp (Exim 4.05) -\tid 17k4h5-00034i-00 -\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""" - h = Header(hstr, maxlinelen=78, header_name='Received', - continuation_ws='\t') - eq(h.encode(), """\ -from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] -\thelo=cthulhu.gerg.ca) -\tby kronos.mems-exchange.org with esmtp (Exim 4.05) -\tid 17k4h5-00034i-00 -\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""") - - def test_long_8bit_header(self): - eq = self.ndiffAssertEqual - msg = Message() - h = Header('Britische Regierung gibt', 'iso-8859-1', - header_name='Subject') - h.append('gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte') - msg['Subject'] = h - eq(msg.as_string(), """\ -Subject: =?iso-8859-1?q?Britische_Regierung_gibt?= =?iso-8859-1?q?gr=FCnes?= - =?iso-8859-1?q?_Licht_f=FCr_Offshore-Windkraftprojekte?= - -""") - - def test_long_8bit_header_no_charset(self): - eq = self.ndiffAssertEqual - msg = Message() - msg['Reply-To'] = 'Britische Regierung gibt gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte ' - eq(msg.as_string(), """\ -Reply-To: Britische Regierung gibt gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte - -""") - - def test_long_to_header(self): - eq = self.ndiffAssertEqual - to = '"Someone Test #A" ,,"Someone Test #B" , "Someone Test #C" , "Someone Test #D" ' - msg = Message() - msg['To'] = to - eq(msg.as_string(0), '''\ -To: "Someone Test #A" , , -\t"Someone Test #B" , -\t"Someone Test #C" , -\t"Someone Test #D" - -''') - - def test_long_line_after_append(self): - eq = self.ndiffAssertEqual - s = 'This is an example of string which has almost the limit of header length.' - h = Header(s) - h.append('Add another line.') - eq(h.encode(), """\ -This is an example of string which has almost the limit of header length. - Add another line.""") - - def test_shorter_line_with_append(self): - eq = self.ndiffAssertEqual - s = 'This is a shorter line.' - h = Header(s) - h.append('Add another sentence. (Surprise?)') - eq(h.encode(), - 'This is a shorter line. Add another sentence. (Surprise?)') - - def test_long_field_name(self): - eq = self.ndiffAssertEqual - fn = 'X-Very-Very-Very-Long-Header-Name' - gs = "Die Mieter treten hier ein werden mit einem Foerderband komfortabel den Korridor entlang, an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, gegen die rotierenden Klingen bef\xf6rdert. " - h = Header(gs, 'iso-8859-1', header_name=fn) - # BAW: this seems broken because the first line is too long - eq(h.encode(), """\ -=?iso-8859-1?q?Die_Mieter_treten_hier_?= - =?iso-8859-1?q?ein_werden_mit_einem_Foerderband_komfortabel_den_Korridor_?= - =?iso-8859-1?q?entlang=2C_an_s=FCdl=FCndischen_Wandgem=E4lden_vorbei=2C_g?= - =?iso-8859-1?q?egen_die_rotierenden_Klingen_bef=F6rdert=2E_?=""") - - def test_long_received_header(self): - h = 'from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP; Wed, 05 Mar 2003 18:10:18 -0700' - msg = Message() - msg['Received-1'] = Header(h, continuation_ws='\t') - msg['Received-2'] = h - self.assertEqual(msg.as_string(), """\ -Received-1: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by -\throthgar.la.mastaler.com (tmda-ofmipd) with ESMTP; -\tWed, 05 Mar 2003 18:10:18 -0700 -Received-2: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by -\throthgar.la.mastaler.com (tmda-ofmipd) with ESMTP; -\tWed, 05 Mar 2003 18:10:18 -0700 - -""") - - def test_string_headerinst_eq(self): - h = '<15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner\'s message of "Thu, 6 Mar 2003 13:58:21 +0100")' - msg = Message() - msg['Received-1'] = Header(h, header_name='Received-1', - continuation_ws='\t') - msg['Received-2'] = h - self.assertEqual(msg.as_string(), """\ -Received-1: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> -\t(David Bremner's message of "Thu, 6 Mar 2003 13:58:21 +0100") -Received-2: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> -\t(David Bremner's message of "Thu, 6 Mar 2003 13:58:21 +0100") - -""") - - def test_long_unbreakable_lines_with_continuation(self): - eq = self.ndiffAssertEqual - msg = Message() - t = """\ - iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 - locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp""" - msg['Face-1'] = t - msg['Face-2'] = Header(t, header_name='Face-2') - eq(msg.as_string(), """\ -Face-1: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 -\tlocQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp -Face-2: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 - locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp - -""") - - def test_another_long_multiline_header(self): - eq = self.ndiffAssertEqual - m = '''\ -Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with Microsoft SMTPSVC(5.0.2195.4905); -\tWed, 16 Oct 2002 07:41:11 -0700''' - msg = bongo.external.email.message_from_string(m) - eq(msg.as_string(), '''\ -Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with -\tMicrosoft SMTPSVC(5.0.2195.4905); Wed, 16 Oct 2002 07:41:11 -0700 - -''') - - def test_long_lines_with_different_header(self): - eq = self.ndiffAssertEqual - h = """\ -List-Unsubscribe: , - """ - msg = Message() - msg['List'] = h - msg['List'] = Header(h, header_name='List') - eq(msg.as_string(), """\ -List: List-Unsubscribe: , -\t -List: List-Unsubscribe: , - - -""") - - - -# Test mangling of "From " lines in the body of a message -class TestFromMangling(unittest.TestCase): - def setUp(self): - self.msg = Message() - self.msg['From'] = 'aaa@bbb.org' - self.msg.set_payload("""\ -From the desk of A.A.A.: -Blah blah blah -""") - - def test_mangled_from(self): - s = StringIO() - g = Generator(s, mangle_from_=True) - g.flatten(self.msg) - self.assertEqual(s.getvalue(), """\ -From: aaa@bbb.org - ->From the desk of A.A.A.: -Blah blah blah -""") - - def test_dont_mangle_from(self): - s = StringIO() - g = Generator(s, mangle_from_=False) - g.flatten(self.msg) - self.assertEqual(s.getvalue(), """\ -From: aaa@bbb.org - -From the desk of A.A.A.: -Blah blah blah -""") - - - -# Test the basic MIMEAudio class -class TestMIMEAudio(unittest.TestCase): - def setUp(self): - # Make sure we pick up the audiotest.au that lives in email/test/data. - # In Python, there's an audiotest.au living in Lib/test but that isn't - # included in some binary distros that don't include the test - # package. The trailing empty string on the .join() is significant - # since findfile() will do a dirname(). - datadir = os.path.join(os.path.dirname(landmark), 'data', '') - fp = open(findfile('audiotest.au', datadir), 'rb') - try: - self._audiodata = fp.read() - finally: - fp.close() - self._au = MIMEAudio(self._audiodata) - - def test_guess_minor_type(self): - self.assertEqual(self._au.get_content_type(), 'audio/basic') - - def test_encoding(self): - payload = self._au.get_payload() - self.assertEqual(base64.decodestring(payload), self._audiodata) - - def test_checkSetMinor(self): - au = MIMEAudio(self._audiodata, 'fish') - self.assertEqual(au.get_content_type(), 'audio/fish') - - def test_add_header(self): - eq = self.assertEqual - unless = self.assertTrue - self._au.add_header('Content-Disposition', 'attachment', - filename='audiotest.au') - eq(self._au['content-disposition'], - 'attachment; filename="audiotest.au"') - eq(self._au.get_params(header='content-disposition'), - [('attachment', ''), ('filename', 'audiotest.au')]) - eq(self._au.get_param('filename', header='content-disposition'), - 'audiotest.au') - missing = [] - eq(self._au.get_param('attachment', header='content-disposition'), '') - unless(self._au.get_param('foo', failobj=missing, - header='content-disposition') is missing) - # Try some missing stuff - unless(self._au.get_param('foobar', missing) is missing) - unless(self._au.get_param('attachment', missing, - header='foobar') is missing) - - - -# Test the basic MIMEImage class -class TestMIMEImage(unittest.TestCase): - def setUp(self): - fp = openfile('PyBanner048.gif') - try: - self._imgdata = fp.read() - finally: - fp.close() - self._im = MIMEImage(self._imgdata) - - def test_guess_minor_type(self): - self.assertEqual(self._im.get_content_type(), 'image/gif') - - def test_encoding(self): - payload = self._im.get_payload() - self.assertEqual(base64.decodestring(payload), self._imgdata) - - def test_checkSetMinor(self): - im = MIMEImage(self._imgdata, 'fish') - self.assertEqual(im.get_content_type(), 'image/fish') - - def test_add_header(self): - eq = self.assertEqual - unless = self.assertTrue - self._im.add_header('Content-Disposition', 'attachment', - filename='dingusfish.gif') - eq(self._im['content-disposition'], - 'attachment; filename="dingusfish.gif"') - eq(self._im.get_params(header='content-disposition'), - [('attachment', ''), ('filename', 'dingusfish.gif')]) - eq(self._im.get_param('filename', header='content-disposition'), - 'dingusfish.gif') - missing = [] - eq(self._im.get_param('attachment', header='content-disposition'), '') - unless(self._im.get_param('foo', failobj=missing, - header='content-disposition') is missing) - # Try some missing stuff - unless(self._im.get_param('foobar', missing) is missing) - unless(self._im.get_param('attachment', missing, - header='foobar') is missing) - - - -# Test the basic MIMEApplication class -class TestMIMEApplication(unittest.TestCase): - def test_headers(self): - eq = self.assertEqual - msg = MIMEApplication('\xfa\xfb\xfc\xfd\xfe\xff') - eq(msg.get_content_type(), 'application/octet-stream') - eq(msg['content-transfer-encoding'], 'base64') - - def test_body(self): - eq = self.assertEqual - bytes = '\xfa\xfb\xfc\xfd\xfe\xff' - msg = MIMEApplication(bytes) - eq(msg.get_payload(), '+vv8/f7/') - eq(msg.get_payload(decode=True), bytes) - - - -# Test the basic MIMEText class -class TestMIMEText(unittest.TestCase): - def setUp(self): - self._msg = MIMEText('hello there') - - def test_types(self): - eq = self.assertEqual - unless = self.assertTrue - eq(self._msg.get_content_type(), 'text/plain') - eq(self._msg.get_param('charset'), 'us-ascii') - missing = [] - unless(self._msg.get_param('foobar', missing) is missing) - unless(self._msg.get_param('charset', missing, header='foobar') - is missing) - - def test_payload(self): - self.assertEqual(self._msg.get_payload(), 'hello there') - self.assertTrue(not self._msg.is_multipart()) - - def test_charset(self): - eq = self.assertEqual - msg = MIMEText('hello there', _charset='us-ascii') - eq(msg.get_charset().input_charset, 'us-ascii') - eq(msg['content-type'], 'text/plain; charset="us-ascii"') - - - -# Test complicated multipart/* messages -class TestMultipart(TestEmailBase): - def setUp(self): - fp = openfile('PyBanner048.gif') - try: - data = fp.read() - finally: - fp.close() - - container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY') - image = MIMEImage(data, name='dingusfish.gif') - image.add_header('content-disposition', 'attachment', - filename='dingusfish.gif') - intro = MIMEText('''\ -Hi there, - -This is the dingus fish. -''') - container.attach(intro) - container.attach(image) - container['From'] = 'Barry ' - container['To'] = 'Dingus Lovers ' - container['Subject'] = 'Here is your dingus fish' - - now = 987809702.54848599 - timetuple = time.localtime(now) - if timetuple[-1] == 0: - tzsecs = time.timezone - else: - tzsecs = time.altzone - if tzsecs > 0: - sign = '-' - else: - sign = '+' - tzoffset = ' %s%04d' % (sign, tzsecs / 36) - container['Date'] = time.strftime( - '%a, %d %b %Y %H:%M:%S', - time.localtime(now)) + tzoffset - self._msg = container - self._im = image - self._txt = intro - - def test_hierarchy(self): - # convenience - eq = self.assertEqual - unless = self.assertTrue - raises = self.assertRaises - # tests - m = self._msg - unless(m.is_multipart()) - eq(m.get_content_type(), 'multipart/mixed') - eq(len(m.get_payload()), 2) - raises(IndexError, m.get_payload, 2) - m0 = m.get_payload(0) - m1 = m.get_payload(1) - unless(m0 is self._txt) - unless(m1 is self._im) - eq(m.get_payload(), [m0, m1]) - unless(not m0.is_multipart()) - unless(not m1.is_multipart()) - - def test_empty_multipart_idempotent(self): - text = """\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - - ---BOUNDARY - - ---BOUNDARY-- -""" - msg = Parser().parsestr(text) - self.ndiffAssertEqual(text, msg.as_string()) - - def test_no_parts_in_a_multipart_with_none_epilogue(self): - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.set_boundary('BOUNDARY') - self.ndiffAssertEqual(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY - ---BOUNDARY--''') - - def test_no_parts_in_a_multipart_with_empty_epilogue(self): - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.preamble = '' - outer.epilogue = '' - outer.set_boundary('BOUNDARY') - self.ndiffAssertEqual(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - - ---BOUNDARY - ---BOUNDARY-- -''') - - def test_one_part_in_a_multipart(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.set_boundary('BOUNDARY') - msg = MIMEText('hello world') - outer.attach(msg) - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - def test_seq_parts_in_a_multipart_with_empty_preamble(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.preamble = '' - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - - def test_seq_parts_in_a_multipart_with_none_preamble(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.preamble = None - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - - def test_seq_parts_in_a_multipart_with_none_epilogue(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.epilogue = None - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - - def test_seq_parts_in_a_multipart_with_empty_epilogue(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.epilogue = '' - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY-- -''') - - - def test_seq_parts_in_a_multipart_with_nl_epilogue(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.epilogue = '\n' - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY-- - -''') - - def test_message_external_body(self): - eq = self.assertEqual - msg = self._msgobj('msg_36.txt') - eq(len(msg.get_payload()), 2) - msg1 = msg.get_payload(1) - eq(msg1.get_content_type(), 'multipart/alternative') - eq(len(msg1.get_payload()), 2) - for subpart in msg1.get_payload(): - eq(subpart.get_content_type(), 'message/external-body') - eq(len(subpart.get_payload()), 1) - subsubpart = subpart.get_payload(0) - eq(subsubpart.get_content_type(), 'text/plain') - - def test_double_boundary(self): - # msg_37.txt is a multipart that contains two dash-boundary's in a - # row. Our interpretation of RFC 2046 calls for ignoring the second - # and subsequent boundaries. - msg = self._msgobj('msg_37.txt') - self.assertEqual(len(msg.get_payload()), 3) - - def test_nested_inner_contains_outer_boundary(self): - eq = self.ndiffAssertEqual - # msg_38.txt has an inner part that contains outer boundaries. My - # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say - # these are illegal and should be interpreted as unterminated inner - # parts. - msg = self._msgobj('msg_38.txt') - sfp = StringIO() - iterators._structure(msg, sfp) - eq(sfp.getvalue(), """\ -multipart/mixed - multipart/mixed - multipart/alternative - text/plain - text/plain - text/plain - text/plain -""") - - def test_nested_with_same_boundary(self): - eq = self.ndiffAssertEqual - # msg 39.txt is similarly evil in that it's got inner parts that use - # the same boundary as outer parts. Again, I believe the way this is - # parsed is closest to the spirit of RFC 2046 - msg = self._msgobj('msg_39.txt') - sfp = StringIO() - iterators._structure(msg, sfp) - eq(sfp.getvalue(), """\ -multipart/mixed - multipart/mixed - multipart/alternative - application/octet-stream - application/octet-stream - text/plain -""") - - def test_boundary_in_non_multipart(self): - msg = self._msgobj('msg_40.txt') - self.assertEqual(msg.as_string(), '''\ -MIME-Version: 1.0 -Content-Type: text/html; boundary="--961284236552522269" - -----961284236552522269 -Content-Type: text/html; -Content-Transfer-Encoding: 7Bit - - - -----961284236552522269-- -''') - - def test_boundary_with_leading_space(self): - eq = self.assertEqual - msg = bongo.external.email.message_from_string('''\ -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary=" XXXX" - --- XXXX -Content-Type: text/plain - - --- XXXX -Content-Type: text/plain - --- XXXX-- -''') - self.assertTrue(msg.is_multipart()) - eq(msg.get_boundary(), ' XXXX') - eq(len(msg.get_payload()), 2) - - def test_boundary_without_trailing_newline(self): - m = Parser().parsestr("""\ -Content-Type: multipart/mixed; boundary="===============0012394164==" -MIME-Version: 1.0 - ---===============0012394164== -Content-Type: image/file1.jpg -MIME-Version: 1.0 -Content-Transfer-Encoding: base64 - -YXNkZg== ---===============0012394164==--""") - self.assertEqual(m.get_payload(0).get_payload(), 'YXNkZg==') - - - -# Test some badly formatted messages -class TestNonConformant(TestEmailBase): - def test_parse_missing_minor_type(self): - eq = self.assertEqual - msg = self._msgobj('msg_14.txt') - eq(msg.get_content_type(), 'text/plain') - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - - def test_same_boundary_inner_outer(self): - unless = self.assertTrue - msg = self._msgobj('msg_15.txt') - # XXX We can probably eventually do better - inner = msg.get_payload(0) - unless(hasattr(inner, 'defects')) - self.assertEqual(len(inner.defects), 1) - unless(isinstance(inner.defects[0], - errors.StartBoundaryNotFoundDefect)) - - def test_multipart_no_boundary(self): - unless = self.assertTrue - msg = self._msgobj('msg_25.txt') - unless(isinstance(msg.get_payload(), str)) - self.assertEqual(len(msg.defects), 2) - unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect)) - unless(isinstance(msg.defects[1], - errors.MultipartInvariantViolationDefect)) - - def test_invalid_content_type(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - msg = Message() - # RFC 2045, $5.2 says invalid yields text/plain - msg['Content-Type'] = 'text' - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - eq(msg.get_content_type(), 'text/plain') - # Clear the old value and try something /really/ invalid - del msg['content-type'] - msg['Content-Type'] = 'foo' - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - eq(msg.get_content_type(), 'text/plain') - # Still, make sure that the message is idempotently generated - s = StringIO() - g = Generator(s) - g.flatten(msg) - neq(s.getvalue(), 'Content-Type: foo\n\n') - - def test_no_start_boundary(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_31.txt') - eq(msg.get_payload(), """\ ---BOUNDARY -Content-Type: text/plain - -message 1 - ---BOUNDARY -Content-Type: text/plain - -message 2 - ---BOUNDARY-- -""") - - def test_no_separating_blank_line(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_35.txt') - eq(msg.as_string(), """\ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: here's something interesting - -counter to RFC 2822, there's no separating newline here -""") - - def test_lying_multipart(self): - unless = self.assertTrue - msg = self._msgobj('msg_41.txt') - unless(hasattr(msg, 'defects')) - self.assertEqual(len(msg.defects), 2) - unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect)) - unless(isinstance(msg.defects[1], - errors.MultipartInvariantViolationDefect)) - - def test_missing_start_boundary(self): - outer = self._msgobj('msg_42.txt') - # The message structure is: - # - # multipart/mixed - # text/plain - # message/rfc822 - # multipart/mixed [*] - # - # [*] This message is missing its start boundary - bad = outer.get_payload(1).get_payload(0) - self.assertEqual(len(bad.defects), 1) - self.assertTrue(isinstance(bad.defects[0], - errors.StartBoundaryNotFoundDefect)) - - - -# Test RFC 2047 header encoding and decoding -class TestRFC2047(unittest.TestCase): - def test_rfc2047_multiline(self): - eq = self.assertEqual - s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz - foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?=""" - dh = decode_header(s) - eq(dh, [ - ('Re:', None), - ('r\x8aksm\x9arg\x8cs', 'mac-iceland'), - ('baz foo bar', None), - ('r\x8aksm\x9arg\x8cs', 'mac-iceland')]) - eq(str(make_header(dh)), - """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar - =?mac-iceland?q?r=8Aksm=9Arg=8Cs?=""") - - def test_whitespace_eater_unicode(self): - eq = self.assertEqual - s = '=?ISO-8859-1?Q?Andr=E9?= Pirard ' - dh = decode_header(s) - eq(dh, [('Andr\xe9', 'iso-8859-1'), ('Pirard ', None)]) - hu = str(make_header(dh)).encode('latin-1') - eq(hu, 'Andr\xe9 Pirard ') - - def test_whitespace_eater_unicode_2(self): - eq = self.assertEqual - s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?=' - dh = decode_header(s) - eq(dh, [('The', None), ('quick brown fox', 'iso-8859-1'), - ('jumped over the', None), ('lazy dog', 'iso-8859-1')]) - hu = make_header(dh).__unicode__() - eq(hu, 'The quick brown fox jumped over the lazy dog') - - - -# Test the MIMEMessage class -class TestMIMEMessage(TestEmailBase): - def setUp(self): - fp = openfile('msg_11.txt') - try: - self._text = fp.read() - finally: - fp.close() - - def test_type_error(self): - self.assertRaises(TypeError, MIMEMessage, 'a plain string') - - def test_valid_argument(self): - eq = self.assertEqual - unless = self.assertTrue - subject = 'A sub-message' - m = Message() - m['Subject'] = subject - r = MIMEMessage(m) - eq(r.get_content_type(), 'message/rfc822') - payload = r.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - subpart = payload[0] - unless(subpart is m) - eq(subpart['subject'], subject) - - def test_bad_multipart(self): - eq = self.assertEqual - msg1 = Message() - msg1['Subject'] = 'subpart 1' - msg2 = Message() - msg2['Subject'] = 'subpart 2' - r = MIMEMessage(msg1) - self.assertRaises(errors.MultipartConversionError, r.attach, msg2) - - def test_generate(self): - # First craft the message to be encapsulated - m = Message() - m['Subject'] = 'An enclosed message' - m.set_payload('Here is the body of the message.\n') - r = MIMEMessage(m) - r['Subject'] = 'The enclosing message' - s = StringIO() - g = Generator(s) - g.flatten(r) - self.assertEqual(s.getvalue(), """\ -Content-Type: message/rfc822 -MIME-Version: 1.0 -Subject: The enclosing message - -Subject: An enclosed message - -Here is the body of the message. -""") - - def test_parse_message_rfc822(self): - eq = self.assertEqual - unless = self.assertTrue - msg = self._msgobj('msg_11.txt') - eq(msg.get_content_type(), 'message/rfc822') - payload = msg.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - submsg = payload[0] - self.assertTrue(isinstance(submsg, Message)) - eq(submsg['subject'], 'An enclosed message') - eq(submsg.get_payload(), 'Here is the body of the message.\n') - - def test_dsn(self): - eq = self.assertEqual - unless = self.assertTrue - # msg 16 is a Delivery Status Notification, see RFC 1894 - msg = self._msgobj('msg_16.txt') - eq(msg.get_content_type(), 'multipart/report') - unless(msg.is_multipart()) - eq(len(msg.get_payload()), 3) - # Subpart 1 is a text/plain, human readable section - subpart = msg.get_payload(0) - eq(subpart.get_content_type(), 'text/plain') - eq(subpart.get_payload(), """\ -This report relates to a message you sent with the following header fields: - - Message-id: <002001c144a6$8752e060$56104586@oxy.edu> - Date: Sun, 23 Sep 2001 20:10:55 -0700 - From: "Ian T. Henry" - To: SoCal Raves - Subject: [scr] yeah for Ians!! - -Your message cannot be delivered to the following recipients: - - Recipient address: jangel1@cougar.noc.ucla.edu - Reason: recipient reached disk quota - -""") - # Subpart 2 contains the machine parsable DSN information. It - # consists of two blocks of headers, represented by two nested Message - # objects. - subpart = msg.get_payload(1) - eq(subpart.get_content_type(), 'message/delivery-status') - eq(len(subpart.get_payload()), 2) - # message/delivery-status should treat each block as a bunch of - # headers, i.e. a bunch of Message objects. - dsn1 = subpart.get_payload(0) - unless(isinstance(dsn1, Message)) - eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu') - eq(dsn1.get_param('dns', header='reporting-mta'), '') - # Try a missing one - eq(dsn1.get_param('nsd', header='reporting-mta'), None) - dsn2 = subpart.get_payload(1) - unless(isinstance(dsn2, Message)) - eq(dsn2['action'], 'failed') - eq(dsn2.get_params(header='original-recipient'), - [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')]) - eq(dsn2.get_param('rfc822', header='final-recipient'), '') - # Subpart 3 is the original message - subpart = msg.get_payload(2) - eq(subpart.get_content_type(), 'message/rfc822') - payload = subpart.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - subsubpart = payload[0] - unless(isinstance(subsubpart, Message)) - eq(subsubpart.get_content_type(), 'text/plain') - eq(subsubpart['message-id'], - '<002001c144a6$8752e060$56104586@oxy.edu>') - - def test_epilogue(self): - eq = self.ndiffAssertEqual - fp = openfile('msg_21.txt') - try: - text = fp.read() - finally: - fp.close() - msg = Message() - msg['From'] = 'aperson@dom.ain' - msg['To'] = 'bperson@dom.ain' - msg['Subject'] = 'Test' - msg.preamble = 'MIME message' - msg.epilogue = 'End of MIME message\n' - msg1 = MIMEText('One') - msg2 = MIMEText('Two') - msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY') - msg.attach(msg1) - msg.attach(msg2) - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), text) - - def test_no_nl_preamble(self): - eq = self.ndiffAssertEqual - msg = Message() - msg['From'] = 'aperson@dom.ain' - msg['To'] = 'bperson@dom.ain' - msg['Subject'] = 'Test' - msg.preamble = 'MIME message' - msg.epilogue = '' - msg1 = MIMEText('One') - msg2 = MIMEText('Two') - msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY') - msg.attach(msg1) - msg.attach(msg2) - eq(msg.as_string(), """\ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: Test -Content-Type: multipart/mixed; boundary="BOUNDARY" - -MIME message ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -One ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -Two ---BOUNDARY-- -""") - - def test_default_type(self): - eq = self.assertEqual - fp = openfile('msg_30.txt') - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - container1 = msg.get_payload(0) - eq(container1.get_default_type(), 'message/rfc822') - eq(container1.get_content_type(), 'message/rfc822') - container2 = msg.get_payload(1) - eq(container2.get_default_type(), 'message/rfc822') - eq(container2.get_content_type(), 'message/rfc822') - container1a = container1.get_payload(0) - eq(container1a.get_default_type(), 'text/plain') - eq(container1a.get_content_type(), 'text/plain') - container2a = container2.get_payload(0) - eq(container2a.get_default_type(), 'text/plain') - eq(container2a.get_content_type(), 'text/plain') - - def test_default_type_with_explicit_container_type(self): - eq = self.assertEqual - fp = openfile('msg_28.txt') - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - container1 = msg.get_payload(0) - eq(container1.get_default_type(), 'message/rfc822') - eq(container1.get_content_type(), 'message/rfc822') - container2 = msg.get_payload(1) - eq(container2.get_default_type(), 'message/rfc822') - eq(container2.get_content_type(), 'message/rfc822') - container1a = container1.get_payload(0) - eq(container1a.get_default_type(), 'text/plain') - eq(container1a.get_content_type(), 'text/plain') - container2a = container2.get_payload(0) - eq(container2a.get_default_type(), 'text/plain') - eq(container2a.get_content_type(), 'text/plain') - - def test_default_type_non_parsed(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - # Set up container - container = MIMEMultipart('digest', 'BOUNDARY') - container.epilogue = '' - # Set up subparts - subpart1a = MIMEText('message 1\n') - subpart2a = MIMEText('message 2\n') - subpart1 = MIMEMessage(subpart1a) - subpart2 = MIMEMessage(subpart2a) - container.attach(subpart1) - container.attach(subpart2) - eq(subpart1.get_content_type(), 'message/rfc822') - eq(subpart1.get_default_type(), 'message/rfc822') - eq(subpart2.get_content_type(), 'message/rfc822') - eq(subpart2.get_default_type(), 'message/rfc822') - neq(container.as_string(0), '''\ -Content-Type: multipart/digest; boundary="BOUNDARY" -MIME-Version: 1.0 - ---BOUNDARY -Content-Type: message/rfc822 -MIME-Version: 1.0 - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 1 - ---BOUNDARY -Content-Type: message/rfc822 -MIME-Version: 1.0 - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 2 - ---BOUNDARY-- -''') - del subpart1['content-type'] - del subpart1['mime-version'] - del subpart2['content-type'] - del subpart2['mime-version'] - eq(subpart1.get_content_type(), 'message/rfc822') - eq(subpart1.get_default_type(), 'message/rfc822') - eq(subpart2.get_content_type(), 'message/rfc822') - eq(subpart2.get_default_type(), 'message/rfc822') - neq(container.as_string(0), '''\ -Content-Type: multipart/digest; boundary="BOUNDARY" -MIME-Version: 1.0 - ---BOUNDARY - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 1 - ---BOUNDARY - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 2 - ---BOUNDARY-- -''') - - def test_mime_attachments_in_constructor(self): - eq = self.assertEqual - text1 = MIMEText('') - text2 = MIMEText('') - msg = MIMEMultipart(_subparts=(text1, text2)) - eq(len(msg.get_payload()), 2) - eq(msg.get_payload(0), text1) - eq(msg.get_payload(1), text2) - - - -# A general test of parser->model->generator idempotency. IOW, read a message -# in, parse it into a message object tree, then without touching the tree, -# regenerate the plain text. The original text and the transformed text -# should be identical. Note: that we ignore the Unix-From since that may -# contain a changed date. -class TestIdempotent(TestEmailBase): - def _msgobj(self, filename): - fp = openfile(filename) - try: - data = fp.read() - finally: - fp.close() - msg = bongo.external.email.message_from_string(data) - return msg, data - - def _idempotent(self, msg, text): - eq = self.ndiffAssertEqual - s = StringIO() - g = Generator(s, maxheaderlen=0) - g.flatten(msg) - eq(text, s.getvalue()) - - def test_parse_text_message(self): - eq = self.assertEqual - msg, text = self._msgobj('msg_01.txt') - eq(msg.get_content_type(), 'text/plain') - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - eq(msg.get_params()[1], ('charset', 'us-ascii')) - eq(msg.get_param('charset'), 'us-ascii') - eq(msg.preamble, None) - eq(msg.epilogue, None) - self._idempotent(msg, text) - - def test_parse_untyped_message(self): - eq = self.assertEqual - msg, text = self._msgobj('msg_03.txt') - eq(msg.get_content_type(), 'text/plain') - eq(msg.get_params(), None) - eq(msg.get_param('charset'), None) - self._idempotent(msg, text) - - def test_simple_multipart(self): - msg, text = self._msgobj('msg_04.txt') - self._idempotent(msg, text) - - def test_MIME_digest(self): - msg, text = self._msgobj('msg_02.txt') - self._idempotent(msg, text) - - def test_long_header(self): - msg, text = self._msgobj('msg_27.txt') - self._idempotent(msg, text) - - def test_MIME_digest_with_part_headers(self): - msg, text = self._msgobj('msg_28.txt') - self._idempotent(msg, text) - - def test_mixed_with_image(self): - msg, text = self._msgobj('msg_06.txt') - self._idempotent(msg, text) - - def test_multipart_report(self): - msg, text = self._msgobj('msg_05.txt') - self._idempotent(msg, text) - - def test_dsn(self): - msg, text = self._msgobj('msg_16.txt') - self._idempotent(msg, text) - - def test_preamble_epilogue(self): - msg, text = self._msgobj('msg_21.txt') - self._idempotent(msg, text) - - def test_multipart_one_part(self): - msg, text = self._msgobj('msg_23.txt') - self._idempotent(msg, text) - - def test_multipart_no_parts(self): - msg, text = self._msgobj('msg_24.txt') - self._idempotent(msg, text) - - def test_no_start_boundary(self): - msg, text = self._msgobj('msg_31.txt') - self._idempotent(msg, text) - - def test_rfc2231_charset(self): - msg, text = self._msgobj('msg_32.txt') - self._idempotent(msg, text) - - def test_more_rfc2231_parameters(self): - msg, text = self._msgobj('msg_33.txt') - self._idempotent(msg, text) - - def test_text_plain_in_a_multipart_digest(self): - msg, text = self._msgobj('msg_34.txt') - self._idempotent(msg, text) - - def test_nested_multipart_mixeds(self): - msg, text = self._msgobj('msg_12a.txt') - self._idempotent(msg, text) - - def test_message_external_body_idempotent(self): - msg, text = self._msgobj('msg_36.txt') - self._idempotent(msg, text) - - def test_content_type(self): - eq = self.assertEqual - unless = self.assertTrue - # Get a message object and reset the seek pointer for other tests - msg, text = self._msgobj('msg_05.txt') - eq(msg.get_content_type(), 'multipart/report') - # Test the Content-Type: parameters - params = {} - for pk, pv in msg.get_params(): - params[pk] = pv - eq(params['report-type'], 'delivery-status') - eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com') - eq(msg.preamble, 'This is a MIME-encapsulated message.\n') - eq(msg.epilogue, '\n') - eq(len(msg.get_payload()), 3) - # Make sure the subparts are what we expect - msg1 = msg.get_payload(0) - eq(msg1.get_content_type(), 'text/plain') - eq(msg1.get_payload(), 'Yadda yadda yadda\n') - msg2 = msg.get_payload(1) - eq(msg2.get_content_type(), 'text/plain') - eq(msg2.get_payload(), 'Yadda yadda yadda\n') - msg3 = msg.get_payload(2) - eq(msg3.get_content_type(), 'message/rfc822') - self.assertTrue(isinstance(msg3, Message)) - payload = msg3.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - msg4 = payload[0] - unless(isinstance(msg4, Message)) - eq(msg4.get_payload(), 'Yadda yadda yadda\n') - - def test_parser(self): - eq = self.assertEqual - unless = self.assertTrue - msg, text = self._msgobj('msg_06.txt') - # Check some of the outer headers - eq(msg.get_content_type(), 'message/rfc822') - # Make sure the payload is a list of exactly one sub-Message, and that - # that submessage has a type of text/plain - payload = msg.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - msg1 = payload[0] - self.assertTrue(isinstance(msg1, Message)) - eq(msg1.get_content_type(), 'text/plain') - self.assertTrue(isinstance(msg1.get_payload(), str)) - eq(msg1.get_payload(), '\n') - - - -# Test various other bits of the package's functionality -class TestMiscellaneous(TestEmailBase): - def test_message_from_string(self): - fp = openfile('msg_01.txt') - try: - text = fp.read() - finally: - fp.close() - msg = bongo.external.email.message_from_string(text) - s = StringIO() - # Don't wrap/continue long headers since we're trying to test - # idempotency. - g = Generator(s, maxheaderlen=0) - g.flatten(msg) - self.assertEqual(text, s.getvalue()) - - def test_message_from_file(self): - fp = openfile('msg_01.txt') - try: - text = fp.read() - fp.seek(0) - msg = bongo.external.email.message_from_file(fp) - s = StringIO() - # Don't wrap/continue long headers since we're trying to test - # idempotency. - g = Generator(s, maxheaderlen=0) - g.flatten(msg) - self.assertEqual(text, s.getvalue()) - finally: - fp.close() - - def test_message_from_string_with_class(self): - unless = self.assertTrue - fp = openfile('msg_01.txt') - try: - text = fp.read() - finally: - fp.close() - # Create a subclass - class MyMessage(Message): - pass - - msg = bongo.external.email.message_from_string(text, MyMessage) - unless(isinstance(msg, MyMessage)) - # Try something more complicated - fp = openfile('msg_02.txt') - try: - text = fp.read() - finally: - fp.close() - msg = bongo.external.email.message_from_string(text, MyMessage) - for subpart in msg.walk(): - unless(isinstance(subpart, MyMessage)) - - def test_message_from_file_with_class(self): - unless = self.assertTrue - # Create a subclass - class MyMessage(Message): - pass - - fp = openfile('msg_01.txt') - try: - msg = bongo.external.email.message_from_file(fp, MyMessage) - finally: - fp.close() - unless(isinstance(msg, MyMessage)) - # Try something more complicated - fp = openfile('msg_02.txt') - try: - msg = bongo.external.email.message_from_file(fp, MyMessage) - finally: - fp.close() - for subpart in msg.walk(): - unless(isinstance(subpart, MyMessage)) - - def test__all__(self): - module = __import__('email') - # Can't use sorted() here due to Python 2.3 compatibility - all = module.__all__[:] - all.sort() - self.assertEqual(all, [ - # Old names - 'Charset', 'Encoders', 'Errors', 'Generator', - 'Header', 'Iterators', 'MIMEAudio', 'MIMEBase', - 'MIMEImage', 'MIMEMessage', 'MIMEMultipart', - 'MIMENonMultipart', 'MIMEText', 'Message', - 'Parser', 'Utils', 'base64MIME', - # new names - 'base64mime', 'charset', 'encoders', 'errors', 'generator', - 'header', 'iterators', 'message', 'message_from_file', - 'message_from_string', 'mime', 'parser', - 'quopriMIME', 'quoprimime', 'utils', - ]) - - def test_formatdate(self): - now = time.time() - self.assertEqual(utils.parsedate(utils.formatdate(now))[:6], - time.gmtime(now)[:6]) - - def test_formatdate_localtime(self): - now = time.time() - self.assertEqual( - utils.parsedate(utils.formatdate(now, localtime=True))[:6], - time.localtime(now)[:6]) - - def test_formatdate_usegmt(self): - now = time.time() - self.assertEqual( - utils.formatdate(now, localtime=False), - time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now))) - self.assertEqual( - utils.formatdate(now, localtime=False, usegmt=True), - time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now))) - - def test_parsedate_none(self): - self.assertEqual(utils.parsedate(''), None) - - def test_parsedate_compact(self): - # The FWS after the comma is optional - self.assertEqual(utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'), - utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800')) - - def test_parsedate_no_dayofweek(self): - eq = self.assertEqual - eq(utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'), - (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800)) - - def test_parsedate_compact_no_dayofweek(self): - eq = self.assertEqual - eq(utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'), - (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800)) - - def test_parsedate_acceptable_to_time_functions(self): - eq = self.assertEqual - timetup = utils.parsedate('5 Feb 2003 13:47:26 -0800') - t = int(time.mktime(timetup)) - eq(time.localtime(t)[:6], timetup[:6]) - eq(int(time.strftime('%Y', timetup)), 2003) - timetup = utils.parsedate_tz('5 Feb 2003 13:47:26 -0800') - t = int(time.mktime(timetup[:9])) - eq(time.localtime(t)[:6], timetup[:6]) - eq(int(time.strftime('%Y', timetup[:9])), 2003) - - def test_parseaddr_empty(self): - self.assertEqual(utils.parseaddr('<>'), ('', '')) - self.assertEqual(utils.formataddr(utils.parseaddr('<>')), '') - - def test_noquote_dump(self): - self.assertEqual( - utils.formataddr(('A Silly Person', 'person@dom.ain')), - 'A Silly Person ') - - def test_escape_dump(self): - self.assertEqual( - utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')), - r'"A \(Very\) Silly Person" ') - a = r'A \(Special\) Person' - b = 'person@dom.ain' - self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b)) - - def test_escape_backslashes(self): - self.assertEqual( - utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')), - r'"Arthur \\Backslash\\ Foobar" ') - a = r'Arthur \Backslash\ Foobar' - b = 'person@dom.ain' - self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b)) - - def test_name_with_dot(self): - x = 'John X. Doe ' - y = '"John X. Doe" ' - a, b = ('John X. Doe', 'jxd@example.com') - self.assertEqual(utils.parseaddr(x), (a, b)) - self.assertEqual(utils.parseaddr(y), (a, b)) - # formataddr() quotes the name if there's a dot in it - self.assertEqual(utils.formataddr((a, b)), y) - - def test_quote_dump(self): - self.assertEqual( - utils.formataddr(('A Silly; Person', 'person@dom.ain')), - r'"A Silly; Person" ') - - def test_fix_eols(self): - eq = self.assertEqual - eq(utils.fix_eols('hello'), 'hello') - eq(utils.fix_eols('hello\n'), 'hello\r\n') - eq(utils.fix_eols('hello\r'), 'hello\r\n') - eq(utils.fix_eols('hello\r\n'), 'hello\r\n') - eq(utils.fix_eols('hello\n\r'), 'hello\r\n\r\n') - - def test_charset_richcomparisons(self): - eq = self.assertEqual - ne = self.assertNotEqual - cset1 = Charset() - cset2 = Charset() - eq(cset1, 'us-ascii') - eq(cset1, 'US-ASCII') - eq(cset1, 'Us-AsCiI') - eq('us-ascii', cset1) - eq('US-ASCII', cset1) - eq('Us-AsCiI', cset1) - ne(cset1, 'usascii') - ne(cset1, 'USASCII') - ne(cset1, 'UsAsCiI') - ne('usascii', cset1) - ne('USASCII', cset1) - ne('UsAsCiI', cset1) - eq(cset1, cset2) - eq(cset2, cset1) - - def test_getaddresses(self): - eq = self.assertEqual - eq(utils.getaddresses(['aperson@dom.ain (Al Person)', - 'Bud Person ']), - [('Al Person', 'aperson@dom.ain'), - ('Bud Person', 'bperson@dom.ain')]) - - def test_getaddresses_nasty(self): - eq = self.assertEqual - eq(utils.getaddresses(['foo: ;']), [('', '')]) - eq(utils.getaddresses( - ['[]*-- =~$']), - [('', ''), ('', ''), ('', '*--')]) - eq(utils.getaddresses( - ['foo: ;', '"Jason R. Mastaler" ']), - [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]) - - def test_getaddresses_embedded_comment(self): - """Test proper handling of a nested comment""" - eq = self.assertEqual - addrs = utils.getaddresses(['User ((nested comment)) ']) - eq(addrs[0][1], 'foo@bar.com') - - def test_utils_quote_unquote(self): - eq = self.assertEqual - msg = Message() - msg.add_header('content-disposition', 'attachment', - filename='foo\\wacky"name') - eq(msg.get_filename(), 'foo\\wacky"name') - - def test_get_body_encoding_with_bogus_charset(self): - charset = Charset('not a charset') - self.assertEqual(charset.get_body_encoding(), 'base64') - - def test_get_body_encoding_with_uppercase_charset(self): - eq = self.assertEqual - msg = Message() - msg['Content-Type'] = 'text/plain; charset=UTF-8' - eq(msg['content-type'], 'text/plain; charset=UTF-8') - charsets = msg.get_charsets() - eq(len(charsets), 1) - eq(charsets[0], 'utf-8') - charset = Charset(charsets[0]) - eq(charset.get_body_encoding(), 'base64') - msg.set_payload('hello world', charset=charset) - eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n') - eq(msg.get_payload(decode=True), 'hello world') - eq(msg['content-transfer-encoding'], 'base64') - # Try another one - msg = Message() - msg['Content-Type'] = 'text/plain; charset="US-ASCII"' - charsets = msg.get_charsets() - eq(len(charsets), 1) - eq(charsets[0], 'us-ascii') - charset = Charset(charsets[0]) - eq(charset.get_body_encoding(), encoders.encode_7or8bit) - msg.set_payload('hello world', charset=charset) - eq(msg.get_payload(), 'hello world') - eq(msg['content-transfer-encoding'], '7bit') - - def test_charsets_case_insensitive(self): - lc = Charset('us-ascii') - uc = Charset('US-ASCII') - self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding()) - - def test_partial_falls_inside_message_delivery_status(self): - eq = self.ndiffAssertEqual - # The Parser interface provides chunks of data to FeedParser in 8192 - # byte gulps. SF bug #1076485 found one of those chunks inside - # message/delivery-status header block, which triggered an - # unreadline() of NeedMoreData. - msg = self._msgobj('msg_43.txt') - sfp = StringIO() - iterators._structure(msg, sfp) - eq(sfp.getvalue(), """\ -multipart/report - text/plain - message/delivery-status - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/rfc822-headers -""") - - - -# Test the iterator/generators -class TestIterators(TestEmailBase): - def test_body_line_iterator(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - # First a simple non-multipart message - msg = self._msgobj('msg_01.txt') - it = iterators.body_line_iterator(msg) - lines = list(it) - eq(len(lines), 6) - neq(EMPTYSTRING.join(lines), msg.get_payload()) - # Now a more complicated multipart - msg = self._msgobj('msg_02.txt') - it = iterators.body_line_iterator(msg) - lines = list(it) - eq(len(lines), 43) - fp = openfile('msg_19.txt') - try: - neq(EMPTYSTRING.join(lines), fp.read()) - finally: - fp.close() - - def test_typed_subpart_iterator(self): - eq = self.assertEqual - msg = self._msgobj('msg_04.txt') - it = iterators.typed_subpart_iterator(msg, 'text') - lines = [] - subparts = 0 - for subpart in it: - subparts += 1 - lines.append(subpart.get_payload()) - eq(subparts, 2) - eq(EMPTYSTRING.join(lines), """\ -a simple kind of mirror -to reflect upon our own -a simple kind of mirror -to reflect upon our own -""") - - def test_typed_subpart_iterator_default_type(self): - eq = self.assertEqual - msg = self._msgobj('msg_03.txt') - it = iterators.typed_subpart_iterator(msg, 'text', 'plain') - lines = [] - subparts = 0 - for subpart in it: - subparts += 1 - lines.append(subpart.get_payload()) - eq(subparts, 1) - eq(EMPTYSTRING.join(lines), """\ - -Hi, - -Do you like this message? - --Me -""") - - - -class TestParsers(TestEmailBase): - def test_header_parser(self): - eq = self.assertEqual - # Parse only the headers of a complex multipart MIME document - fp = openfile('msg_02.txt') - try: - msg = HeaderParser().parse(fp) - finally: - fp.close() - eq(msg['from'], 'ppp-request@zzz.org') - eq(msg['to'], 'ppp@zzz.org') - eq(msg.get_content_type(), 'multipart/mixed') - self.assertFalse(msg.is_multipart()) - self.assertTrue(isinstance(msg.get_payload(), str)) - - def test_whitespace_continuation(self): - eq = self.assertEqual - # This message contains a line after the Subject: header that has only - # whitespace, but it is not empty! - msg = bongo.external.email.message_from_string("""\ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: the next line has a space on it -\x20 -Date: Mon, 8 Apr 2002 15:09:19 -0400 -Message-ID: spam - -Here's the message body -""") - eq(msg['subject'], 'the next line has a space on it\n ') - eq(msg['message-id'], 'spam') - eq(msg.get_payload(), "Here's the message body\n") - - def test_whitespace_continuation_last_header(self): - eq = self.assertEqual - # Like the previous test, but the subject line is the last - # header. - msg = bongo.external.email.message_from_string("""\ -From: aperson@dom.ain -To: bperson@dom.ain -Date: Mon, 8 Apr 2002 15:09:19 -0400 -Message-ID: spam -Subject: the next line has a space on it -\x20 - -Here's the message body -""") - eq(msg['subject'], 'the next line has a space on it\n ') - eq(msg['message-id'], 'spam') - eq(msg.get_payload(), "Here's the message body\n") - - def test_crlf_separation(self): - eq = self.assertEqual - fp = openfile('msg_26.txt', mode='rb') - try: - msg = Parser().parse(fp) - finally: - fp.close() - eq(len(msg.get_payload()), 2) - part1 = msg.get_payload(0) - eq(part1.get_content_type(), 'text/plain') - eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n') - part2 = msg.get_payload(1) - eq(part2.get_content_type(), 'application/riscos') - - def test_multipart_digest_with_extra_mime_headers(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - fp = openfile('msg_28.txt') - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - # Structure is: - # multipart/digest - # message/rfc822 - # text/plain - # message/rfc822 - # text/plain - eq(msg.is_multipart(), 1) - eq(len(msg.get_payload()), 2) - part1 = msg.get_payload(0) - eq(part1.get_content_type(), 'message/rfc822') - eq(part1.is_multipart(), 1) - eq(len(part1.get_payload()), 1) - part1a = part1.get_payload(0) - eq(part1a.is_multipart(), 0) - eq(part1a.get_content_type(), 'text/plain') - neq(part1a.get_payload(), 'message 1\n') - # next message/rfc822 - part2 = msg.get_payload(1) - eq(part2.get_content_type(), 'message/rfc822') - eq(part2.is_multipart(), 1) - eq(len(part2.get_payload()), 1) - part2a = part2.get_payload(0) - eq(part2a.is_multipart(), 0) - eq(part2a.get_content_type(), 'text/plain') - neq(part2a.get_payload(), 'message 2\n') - - def test_three_lines(self): - # A bug report by Andrew McNamara - lines = ['From: Andrew Person From', 'From']) - eq(msg.get_payload(), 'body') - - def test_rfc2822_space_not_allowed_in_header(self): - eq = self.assertEqual - m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody' - msg = bongo.external.email.message_from_string(m) - eq(len(list(msg.keys())), 0) - - def test_rfc2822_one_character_header(self): - eq = self.assertEqual - m = 'A: first header\nB: second header\nCC: third header\n\nbody' - msg = bongo.external.email.message_from_string(m) - headers = list(msg.keys()) - headers.sort() - eq(headers, ['A', 'B', 'CC']) - eq(msg.get_payload(), 'body') - - - -class TestBase64(unittest.TestCase): - def test_len(self): - eq = self.assertEqual - eq(base64mime.base64_len('hello'), - len(base64mime.encode('hello', eol=''))) - for size in range(15): - if size == 0 : bsize = 0 - elif size <= 3 : bsize = 4 - elif size <= 6 : bsize = 8 - elif size <= 9 : bsize = 12 - elif size <= 12: bsize = 16 - else : bsize = 20 - eq(base64mime.base64_len('x'*size), bsize) - - def test_decode(self): - eq = self.assertEqual - eq(base64mime.decode(''), '') - eq(base64mime.decode('aGVsbG8='), 'hello') - eq(base64mime.decode('aGVsbG8=', 'X'), 'hello') - eq(base64mime.decode('aGVsbG8NCndvcmxk\n', 'X'), 'helloXworld') - - def test_encode(self): - eq = self.assertEqual - eq(base64mime.encode(''), '') - eq(base64mime.encode('hello'), 'aGVsbG8=\n') - # Test the binary flag - eq(base64mime.encode('hello\n'), 'aGVsbG8K\n') - eq(base64mime.encode('hello\n', 0), 'aGVsbG8NCg==\n') - # Test the maxlinelen arg - eq(base64mime.encode('xxxx ' * 20, maxlinelen=40), """\ -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg -eHh4eCB4eHh4IA== -""") - # Test the eol argument - eq(base64mime.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r -eHh4eCB4eHh4IA==\r -""") - - def test_header_encode(self): - eq = self.assertEqual - he = base64mime.header_encode - eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=') - eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=') - # Test the charset option - eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=') - # Test the keep_eols flag - eq(he('hello\nworld', keep_eols=True), - '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=') - # Test the maxlinelen argument - eq(he('xxxx ' * 20, maxlinelen=40), """\ -=?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?= - =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?= - =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?= - =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?= - =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?= - =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""") - # Test the eol argument - eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -=?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?=\r - =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?=\r - =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?=\r - =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?=\r - =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?=\r - =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""") - - - -class TestQuopri(unittest.TestCase): - def setUp(self): - self.hlit = [chr(x) for x in range(ord('a'), ord('z')+1)] + \ - [chr(x) for x in range(ord('A'), ord('Z')+1)] + \ - [chr(x) for x in range(ord('0'), ord('9')+1)] + \ - ['!', '*', '+', '-', '/', ' '] - self.hnon = [chr(x) for x in range(256) if chr(x) not in self.hlit] - assert len(self.hlit) + len(self.hnon) == 256 - self.blit = [chr(x) for x in range(ord(' '), ord('~')+1)] + ['\t'] - self.blit.remove('=') - self.bnon = [chr(x) for x in range(256) if chr(x) not in self.blit] - assert len(self.blit) + len(self.bnon) == 256 - - def test_header_quopri_check(self): - for c in self.hlit: - self.assertFalse(quoprimime.header_quopri_check(c)) - for c in self.hnon: - self.assertTrue(quoprimime.header_quopri_check(c)) - - def test_body_quopri_check(self): - for c in self.blit: - self.assertFalse(quoprimime.body_quopri_check(c)) - for c in self.bnon: - self.assertTrue(quoprimime.body_quopri_check(c)) - - def test_header_quopri_len(self): - eq = self.assertEqual - hql = quoprimime.header_quopri_len - enc = quoprimime.header_encode - for s in ('hello', 'h@e@l@l@o@'): - # Empty charset and no line-endings. 7 == RFC chrome - eq(hql(s), len(enc(s, charset='', eol=''))-7) - for c in self.hlit: - eq(hql(c), 1) - for c in self.hnon: - eq(hql(c), 3) - - def test_body_quopri_len(self): - eq = self.assertEqual - bql = quoprimime.body_quopri_len - for c in self.blit: - eq(bql(c), 1) - for c in self.bnon: - eq(bql(c), 3) - - def test_quote_unquote_idempotent(self): - for x in range(256): - c = chr(x) - self.assertEqual(quoprimime.unquote(quoprimime.quote(c)), c) - - def test_header_encode(self): - eq = self.assertEqual - he = quoprimime.header_encode - eq(he('hello'), '=?iso-8859-1?q?hello?=') - eq(he('hello\nworld'), '=?iso-8859-1?q?hello=0D=0Aworld?=') - # Test the charset option - eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?q?hello?=') - # Test the keep_eols flag - eq(he('hello\nworld', keep_eols=True), '=?iso-8859-1?q?hello=0Aworld?=') - # Test a non-ASCII character - eq(he('hello\xc7there'), '=?iso-8859-1?q?hello=C7there?=') - # Test the maxlinelen argument - eq(he('xxxx ' * 20, maxlinelen=40), """\ -=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?= - =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?= - =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?= - =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?= - =?iso-8859-1?q?x_xxxx_xxxx_?=""") - # Test the eol argument - eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=\r - =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=\r - =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?=\r - =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?=\r - =?iso-8859-1?q?x_xxxx_xxxx_?=""") - - def test_decode(self): - eq = self.assertEqual - eq(quoprimime.decode(''), '') - eq(quoprimime.decode('hello'), 'hello') - eq(quoprimime.decode('hello', 'X'), 'hello') - eq(quoprimime.decode('hello\nworld', 'X'), 'helloXworld') - - def test_encode(self): - eq = self.assertEqual - eq(quoprimime.encode(''), '') - eq(quoprimime.encode('hello'), 'hello') - # Test the binary flag - eq(quoprimime.encode('hello\r\nworld'), 'hello\nworld') - eq(quoprimime.encode('hello\r\nworld', 0), 'hello\nworld') - # Test the maxlinelen arg - eq(quoprimime.encode('xxxx ' * 20, maxlinelen=40), """\ -xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx= - xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx= -x xxxx xxxx xxxx xxxx=20""") - # Test the eol argument - eq(quoprimime.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r - xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r -x xxxx xxxx xxxx xxxx=20""") - eq(quoprimime.encode("""\ -one line - -two line"""), """\ -one line - -two line""") - - - -# Test the Charset class -class TestCharset(unittest.TestCase): - def tearDown(self): - from bongo.external.email import charset as CharsetModule - try: - del CharsetModule.CHARSETS['fake'] - except KeyError: - pass - - def test_idempotent(self): - eq = self.assertEqual - # Make sure us-ascii = no Unicode conversion - c = Charset('us-ascii') - s = 'Hello World!' - sp = c.to_splittable(s) - eq(s, c.from_splittable(sp)) - # test 8-bit idempotency with us-ascii - s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa' - sp = c.to_splittable(s) - eq(s, c.from_splittable(sp)) - - def test_body_encode(self): - eq = self.assertEqual - # Try a charset with QP body encoding - c = Charset('iso-8859-1') - eq('hello w=F6rld', c.body_encode('hello w\xf6rld')) - # Try a charset with Base64 body encoding - c = Charset('utf-8') - eq('aGVsbG8gd29ybGQ=\n', c.body_encode('hello world')) - # Try a charset with None body encoding - c = Charset('us-ascii') - eq('hello world', c.body_encode('hello world')) - # Try the convert argument, where input codec <> output codec - c = Charset('euc-jp') - # With apologies to Tokio Kikuchi ;) - try: - eq('\x1b$B5FCO;~IW\x1b(B', - c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7')) - eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', - c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False)) - except LookupError: - # We probably don't have the Japanese codecs installed - pass - # Testing SF bug #625509, which we have to fake, since there are no - # built-in encodings where the header encoding is QP but the body - # encoding is not. - from bongo.external.email import charset as CharsetModule - CharsetModule.add_charset('fake', CharsetModule.QP, None) - c = Charset('fake') - eq('hello w\xf6rld', c.body_encode('hello w\xf6rld')) - - def test_unicode_charset_name(self): - charset = Charset('us-ascii') - self.assertEqual(str(charset), 'us-ascii') - self.assertRaises(errors.CharsetError, Charset, 'asc\xffii') - - - -# Test multilingual MIME headers. -class TestHeader(TestEmailBase): - def test_simple(self): - eq = self.ndiffAssertEqual - h = Header('Hello World!') - eq(h.encode(), 'Hello World!') - h.append(' Goodbye World!') - eq(h.encode(), 'Hello World! Goodbye World!') - - def test_simple_surprise(self): - eq = self.ndiffAssertEqual - h = Header('Hello World!') - eq(h.encode(), 'Hello World!') - h.append('Goodbye World!') - eq(h.encode(), 'Hello World! Goodbye World!') - - def test_header_needs_no_decoding(self): - h = 'no decoding needed' - self.assertEqual(decode_header(h), [(h, None)]) - - def test_long(self): - h = Header("I am the very model of a modern Major-General; I've information vegetable, animal, and mineral; I know the kings of England, and I quote the fights historical from Marathon to Waterloo, in order categorical; I'm very well acquainted, too, with matters mathematical; I understand equations, both the simple and quadratical; about binomial theorem I'm teeming with a lot o' news, with many cheerful facts about the square of the hypotenuse.", - maxlinelen=76) - for l in h.encode(splitchars=' ').split('\n '): - self.assertTrue(len(l) <= 76) - - def test_multilingual(self): - eq = self.ndiffAssertEqual - g = Charset("iso-8859-1") - cz = Charset("iso-8859-2") - utf8 = Charset("utf-8") - g_head = "Die Mieter treten hier ein werden mit einem Foerderband komfortabel den Korridor entlang, an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, gegen die rotierenden Klingen bef\xf6rdert. " - cz_head = "Finan\xe8ni metropole se hroutily pod tlakem jejich d\xf9vtipu.. " - utf8_head = "\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066\u3044\u307e\u3059\u3002".encode("utf-8") - h = Header(g_head, g) - h.append(cz_head, cz) - h.append(utf8_head, utf8) - enc = h.encode() - eq(enc, """\ -=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_ko?= - =?iso-8859-1?q?mfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wan?= - =?iso-8859-1?q?dgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6?= - =?iso-8859-1?q?rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?= - =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?= - =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?= - =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?= - =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?= - =?utf-8?q?_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das_Oder_die_Fl?= - =?utf-8?b?aXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBo+OBpuOBhOOBvuOBmQ==?= - =?utf-8?b?44CC?=""") - eq(decode_header(enc), - [(g_head, "iso-8859-1"), (cz_head, "iso-8859-2"), - (utf8_head, "utf-8")]) - ustr = str(h) - eq(ustr.encode('utf-8'), - 'Die Mieter treten hier ein werden mit einem Foerderband ' - 'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen ' - 'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen ' - 'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod ' - 'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81' - '\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3' - '\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3' - '\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83' - '\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e' - '\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3' - '\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82' - '\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b' - '\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git ' - 'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt ' - 'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81' - '\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82') - # Test make_header() - newh = make_header(decode_header(enc)) - eq(newh, enc) - - def test_header_ctor_default_args(self): - eq = self.ndiffAssertEqual - h = Header() - eq(h, '') - h.append('foo', Charset('iso-8859-1')) - eq(h, '=?iso-8859-1?q?foo?=') - - def test_explicit_maxlinelen(self): - eq = self.ndiffAssertEqual - hstr = 'A very long line that must get split to something other than at the 76th character boundary to test the non-default behavior' - h = Header(hstr) - eq(h.encode(), '''\ -A very long line that must get split to something other than at the 76th - character boundary to test the non-default behavior''') - h = Header(hstr, header_name='Subject') - eq(h.encode(), '''\ -A very long line that must get split to something other than at the - 76th character boundary to test the non-default behavior''') - h = Header(hstr, maxlinelen=1024, header_name='Subject') - eq(h.encode(), hstr) - - def test_us_ascii_header(self): - eq = self.assertEqual - s = 'hello' - x = decode_header(s) - eq(x, [('hello', None)]) - h = make_header(x) - eq(s, h.encode()) - - def test_string_charset(self): - eq = self.assertEqual - h = Header() - h.append('hello', 'iso-8859-1') - eq(h, '=?iso-8859-1?q?hello?=') - -## def test_unicode_error(self): -## raises = self.assertRaises -## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii') -## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii') -## h = Header() -## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii') -## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii') -## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1') - - def test_utf8_shortest(self): - eq = self.assertEqual - h = Header('p\xf6stal', 'utf-8') - eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=') - h = Header('\u83ca\u5730\u6642\u592b', 'utf-8') - eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=') - - def test_bad_8bit_header(self): - raises = self.assertRaises - eq = self.assertEqual - x = 'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big' - raises(UnicodeError, Header, x) - h = Header() - raises(UnicodeError, h.append, x) - eq(str(Header(x, errors='replace')), x) - h.append(x, errors='replace') - eq(str(h), x) - - def test_encoded_adjacent_nonencoded(self): - eq = self.assertEqual - h = Header() - h.append('hello', 'iso-8859-1') - h.append('world') - s = h.encode() - eq(s, '=?iso-8859-1?q?hello?= world') - h = make_header(decode_header(s)) - eq(h.encode(), s) - - def test_whitespace_eater(self): - eq = self.assertEqual - s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.' - parts = decode_header(s) - eq(parts, [('Subject:', None), ('\xf0\xd2\xcf\xd7\xc5\xd2\xcb\xc1 \xce\xc1 \xc6\xc9\xce\xc1\xcc\xd8\xce\xd9\xca', 'koi8-r'), ('zz.', None)]) - hdr = make_header(parts) - eq(hdr.encode(), - 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.') - - def test_broken_base64_header(self): - raises = self.assertRaises - s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3IQ?=' - raises(errors.HeaderParseError, decode_header, s) - - - -# Test RFC 2231 header parameters (en/de)coding -class TestRFC2231(TestEmailBase): - def test_get_param(self): - eq = self.assertEqual - msg = self._msgobj('msg_29.txt') - eq(msg.get_param('title'), - ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!')) - eq(msg.get_param('title', unquote=False), - ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"')) - - def test_set_param(self): - eq = self.assertEqual - msg = Message() - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii') - eq(msg.get_param('title'), - ('us-ascii', '', 'This is even more ***fun*** isn\'t it!')) - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii', language='en') - eq(msg.get_param('title'), - ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!')) - msg = self._msgobj('msg_01.txt') - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii', language='en') - eq(msg.as_string(), """\ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) -\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 -Content-Type: text/plain; charset=us-ascii; -\ttitle*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21" - - -Hi, - -Do you like this message? - --Me -""") - - def test_del_param(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_01.txt') - msg.set_param('foo', 'bar', charset='us-ascii', language='en') - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii', language='en') - msg.del_param('foo', header='Content-Type') - eq(msg.as_string(), """\ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) -\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 -Content-Type: text/plain; charset="us-ascii"; -\ttitle*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21" - - -Hi, - -Do you like this message? - --Me -""") - - def test_rfc2231_get_content_charset(self): - eq = self.assertEqual - msg = self._msgobj('msg_32.txt') - eq(msg.get_content_charset(), 'us-ascii') - - def test_rfc2231_no_language_or_charset(self): - m = '''\ -Content-Transfer-Encoding: 8bit -Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm" -Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm - -''' - msg = bongo.external.email.message_from_string(m) - param = msg.get_param('NAME') - self.assertFalse(isinstance(param, tuple)) - self.assertEqual( - param, - 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm') - - def test_rfc2231_no_language_or_charset_in_filename(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="''This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_no_language_or_charset_in_filename_encoded(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="''This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_partly_encoded(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0="''This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual( - msg.get_filename(), - 'This%20is%20even%20more%20***fun*** is it not.pdf') - - def test_rfc2231_partly_nonencoded(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0="This%20is%20even%20more%20"; -\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual( - msg.get_filename(), - 'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf') - - def test_rfc2231_no_language_or_charset_in_boundary(self): - m = '''\ -Content-Type: multipart/alternative; -\tboundary*0*="''This%20is%20even%20more%20"; -\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tboundary*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_boundary(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_no_language_or_charset_in_charset(self): - # This is a nonsensical charset value, but tests the code anyway - m = '''\ -Content-Type: text/plain; -\tcharset*0*="This%20is%20even%20more%20"; -\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tcharset*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_content_charset(), - 'this is even more ***fun*** is it not.pdf') - - def test_rfc2231_bad_encoding_in_filename(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="bogus'xx'This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_bad_encoding_in_charset(self): - m = """\ -Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D - -""" - msg = bongo.external.email.message_from_string(m) - # This should return None because non-ascii characters in the charset - # are not allowed. - self.assertEqual(msg.get_content_charset(), None) - - def test_rfc2231_bad_character_in_charset(self): - m = """\ -Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D - -""" - msg = bongo.external.email.message_from_string(m) - # This should return None because non-ascii characters in the charset - # are not allowed. - self.assertEqual(msg.get_content_charset(), None) - - def test_rfc2231_bad_character_in_filename(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="ascii'xx'This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2*="is it not.pdf%E2" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf\ufffd') - - def test_rfc2231_unknown_encoding(self): - m = """\ -Content-Transfer-Encoding: 8bit -Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt - -""" - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), 'myfile.txt') - - def test_rfc2231_single_tick_in_filename_extended(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0*=\"Frank's\"; name*1*=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, None) - eq(language, None) - eq(s, "Frank's Document") - - def test_rfc2231_single_tick_in_filename(self): - m = """\ -Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - param = msg.get_param('name') - self.assertFalse(isinstance(param, tuple)) - self.assertEqual(param, "Frank's Document") - - def test_rfc2231_tick_attack_extended(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, 'us-ascii') - eq(language, 'en-us') - eq(s, "Frank's Document") - - def test_rfc2231_tick_attack(self): - m = """\ -Content-Type: application/x-foo; -\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - param = msg.get_param('name') - self.assertFalse(isinstance(param, tuple)) - self.assertEqual(param, "us-ascii'en-us'Frank's Document") - - def test_rfc2231_no_extended_values(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; name=\"Frank's Document\" - -""" - msg = bongo.external.email.message_from_string(m) - eq(msg.get_param('name'), "Frank's Document") - - def test_rfc2231_encoded_then_unencoded_segments(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0*=\"us-ascii'en-us'My\"; -\tname*1=\" Document\"; -\tname*2*=\" For You\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, 'us-ascii') - eq(language, 'en-us') - eq(s, 'My Document For You') - - def test_rfc2231_unencoded_then_encoded_segments(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0=\"us-ascii'en-us'My\"; -\tname*1*=\" Document\"; -\tname*2*=\" For You\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, 'us-ascii') - eq(language, 'en-us') - eq(s, 'My Document For You') - - - -def _testclasses(): - mod = sys.modules[__name__] - return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')] - - -def suite(): - suite = unittest.TestSuite() - for testclass in _testclasses(): - suite.addTest(unittest.makeSuite(testclass)) - return suite - - -def test_main(): - for testclass in _testclasses(): - run_unittest(testclass) - - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/src/libs/python/bongo/external/email/test/test_email_torture.py b/src/libs/python/bongo/external/email/test/test_email_torture.py deleted file mode 100644 index 472a68c..0000000 --- a/src/libs/python/bongo/external/email/test/test_email_torture.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright (C) 2002-2004 Python Software Foundation -# -# A torture test of the email package. This should not be run as part of the -# standard Python test suite since it requires several meg of email messages -# collected in the wild. These source messages are not checked into the -# Python distro, but are available as part of the standalone email package at -# http://sf.net/projects/mimelib - -import sys -import os -import unittest -from io import StringIO -from types import ListType - -from bongo.external.email.test.test_email import TestEmailBase -from test.test_support import TestSkipped - -import bongo -import bongo.external -import bongo.external.email -from bongo.external.email import __file__ as testfile -from bongo.external.email.Iterators import _structure -from bongo.external.email import message_from_file -def openfile(filename): - from os.path import join, dirname, abspath - path = abspath(join(dirname(testfile), os.pardir, 'moredata', filename)) - return open(path, 'r') - -# Prevent this test from running in the Python distro -try: - openfile('crispin-torture.txt') -except IOError: - raise TestSkipped - - - -class TortureBase(TestEmailBase): - def _msgobj(self, filename): - fp = openfile(filename) - try: - msg = message_from_file(fp) - finally: - fp.close() - return msg - - - -class TestCrispinTorture(TortureBase): - # Mark Crispin's torture test from the SquirrelMail project - def test_mondo_message(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - msg = self._msgobj('crispin-torture.txt') - payload = msg.get_payload() - eq(type(payload), ListType) - eq(len(payload), 12) - eq(msg.preamble, None) - eq(msg.epilogue, '\n') - # Probably the best way to verify the message is parsed correctly is to - # dump its structure and compare it against the known structure. - fp = StringIO() - _structure(msg, fp=fp) - neq(fp.getvalue(), """\ -multipart/mixed - text/plain - message/rfc822 - multipart/alternative - text/plain - multipart/mixed - text/richtext - application/andrew-inset - message/rfc822 - audio/basic - audio/basic - image/pbm - message/rfc822 - multipart/mixed - multipart/mixed - text/plain - audio/x-sun - multipart/mixed - image/gif - image/gif - application/x-be2 - application/atomicmail - audio/x-sun - message/rfc822 - multipart/mixed - text/plain - image/pgm - text/plain - message/rfc822 - multipart/mixed - text/plain - image/pbm - message/rfc822 - application/postscript - image/gif - message/rfc822 - multipart/mixed - audio/basic - audio/basic - message/rfc822 - multipart/mixed - application/postscript - text/plain - message/rfc822 - multipart/mixed - text/plain - multipart/parallel - image/gif - audio/basic - application/atomicmail - message/rfc822 - audio/x-sun -""") - - -def _testclasses(): - mod = sys.modules[__name__] - return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')] - - -def suite(): - suite = unittest.TestSuite() - for testclass in _testclasses(): - suite.addTest(unittest.makeSuite(testclass)) - return suite - - -def test_main(): - for testclass in _testclasses(): - test_support.run_unittest(testclass) - - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/src/libs/python/bongo/external/email/utils.py b/src/libs/python/bongo/external/email/utils.py deleted file mode 100644 index f9b661c..0000000 --- a/src/libs/python/bongo/external/email/utils.py +++ /dev/null @@ -1,327 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Miscellaneous utilities.""" - -__all__ = [ - 'collapse_rfc2231_value', - 'decode_params', - 'decode_rfc2231', - 'encode_rfc2231', - 'formataddr', - 'formatdate', - 'getaddresses', - 'make_msgid', - 'parseaddr', - 'parsedate', - 'parsedate_tz', - 'unquote', - ] - -import os -import re -import time -import base64 -import random -import socket -import urllib.request, urllib.parse, urllib.error -import warnings -from io import StringIO - -from bongo.external.email._parseaddr import quote -from bongo.external.email._parseaddr import AddressList as _AddressList -from bongo.external.email._parseaddr import mktime_tz - -# We need wormarounds for bugs in these methods in older Pythons (see below) -from bongo.external.email._parseaddr import parsedate as _parsedate -from bongo.external.email._parseaddr import parsedate_tz as _parsedate_tz - -from quopri import decodestring as _qdecode - -# Intrapackage imports -from bongo.external.email.encoders import _bencode, _qencode - -COMMASPACE = ', ' -EMPTYSTRING = '' -UEMPTYSTRING = '' -CRLF = '\r\n' -TICK = "'" - -specialsre = re.compile(r'[][\\()<>@,:;".]') -escapesre = re.compile(r'[][\\()"]') - - - -# Helpers - -def _identity(s): - return s - - -def _bdecode(s): - # We can't quite use base64.encodestring() since it tacks on a "courtesy - # newline". Blech! - if not s: - return s - value = base64.decodestring(s) - if not s.endswith('\n') and value.endswith('\n'): - return value[:-1] - return value - - - -def fix_eols(s): - """Replace all line-ending characters with \r\n.""" - # Fix newlines with no preceding carriage return - s = re.sub(r'(?', name) - return '%s%s%s <%s>' % (quotes, name, quotes, address) - return address - - - -def getaddresses(fieldvalues): - """Return a list of (REALNAME, EMAIL) for each fieldvalue.""" - all = COMMASPACE.join(fieldvalues) - a = _AddressList(all) - return a.addresslist - - - -ecre = re.compile(r''' - =\? # literal =? - (?P[^?]*?) # non-greedy up to the next ? is the charset - \? # literal ? - (?P[qb]) # either a "q" or a "b", case insensitive - \? # literal ? - (?P.*?) # non-greedy up to the next ?= is the atom - \?= # literal ?= - ''', re.VERBOSE | re.IGNORECASE) - - - -def formatdate(timeval=None, localtime=False, usegmt=False): - """Returns a date string as specified by RFC 2822, e.g.: - - Fri, 09 Nov 2001 01:08:47 -0000 - - Optional timeval if given is a floating point time value as accepted by - gmtime() and localtime(), otherwise the current time is used. - - Optional localtime is a flag that when True, interprets timeval, and - returns a date relative to the local timezone instead of UTC, properly - taking daylight savings time into account. - - Optional argument usegmt means that the timezone is written out as - an ascii string, not numeric one (so "GMT" instead of "+0000"). This - is needed for HTTP, and is only used when localtime==False. - """ - # Note: we cannot use strftime() because that honors the locale and RFC - # 2822 requires that day and month names be the English abbreviations. - if timeval is None: - timeval = time.time() - if localtime: - now = time.localtime(timeval) - # Calculate timezone offset, based on whether the local zone has - # daylight savings time, and whether DST is in effect. - if time.daylight and now[-1]: - offset = time.altzone - else: - offset = time.timezone - hours, minutes = divmod(abs(offset), 3600) - # Remember offset is in seconds west of UTC, but the timezone is in - # minutes east of UTC, so the signs differ. - if offset > 0: - sign = '-' - else: - sign = '+' - zone = '%s%02d%02d' % (sign, hours, minutes // 60) - else: - now = time.gmtime(timeval) - # Timezone offset is always -0000 - if usegmt: - zone = 'GMT' - else: - zone = '-0000' - return '%s, %02d %s %04d %02d:%02d:%02d %s' % ( - ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][now[6]], - now[2], - ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now[1] - 1], - now[0], now[3], now[4], now[5], - zone) - - - -def make_msgid(idstring=None): - """Returns a string suitable for RFC 2822 compliant Message-ID, e.g: - - <20020201195627.33539.96671@nightshade.la.mastaler.com> - - Optional idstring if given is a string used to strengthen the - uniqueness of the message id. - """ - timeval = time.time() - utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval)) - pid = os.getpid() - randint = random.randrange(100000) - if idstring is None: - idstring = '' - else: - idstring = '.' + idstring - idhost = socket.getfqdn() - msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost) - return msgid - - - -# These functions are in the standalone mimelib version only because they've -# subsequently been fixed in the latest Python versions. We use this to worm -# around broken older Pythons. -def parsedate(data): - if not data: - return None - return _parsedate(data) - - -def parsedate_tz(data): - if not data: - return None - return _parsedate_tz(data) - - -def parseaddr(addr): - addrs = _AddressList(addr).addresslist - if not addrs: - return '', '' - return addrs[0] - - -# rfc822.unquote() doesn't properly de-backslash-ify in Python pre-2.3. -def unquote(str): - """Remove quotes from a string.""" - if len(str) > 1: - if str.startswith('"') and str.endswith('"'): - return str[1:-1].replace('\\\\', '\\').replace('\\"', '"') - if str.startswith('<') and str.endswith('>'): - return str[1:-1] - return str - - - -# RFC2231-related functions - parameter encoding and decoding -def decode_rfc2231(s): - """Decode string according to RFC 2231""" - parts = s.split(TICK, 2) - if len(parts) <= 2: - return None, None, s - if len(parts) > 3: - charset, language = parts[:2] - s = TICK.join(parts[2:]) - return charset, language, s - return parts - - -def encode_rfc2231(s, charset=None, language=None): - """Encode string according to RFC 2231. - - If neither charset nor language is given, then s is returned as-is. If - charset is given but not language, the string is encoded using the empty - string for language. - """ - import urllib.request, urllib.parse, urllib.error - s = urllib.parse.quote(s, safe='') - if charset is None and language is None: - return s - if language is None: - language = '' - return "%s'%s'%s" % (charset, language, s) - - -rfc2231_continuation = re.compile(r'^(?P\w+)\*((?P[0-9]+)\*?)?$') - -def decode_params(params): - """Decode parameters list according to RFC 2231. - - params is a sequence of 2-tuples containing (param name, string value). - """ - # Copy params so we don't mess with the original - params = params[:] - new_params = [] - # Map parameter's name to a list of continuations. The values are a - # 3-tuple of the continuation number, the string value, and a flag - # specifying whether a particular segment is %-encoded. - rfc2231_params = {} - name, value = params.pop(0) - new_params.append((name, value)) - while params: - name, value = params.pop(0) - if name.endswith('*'): - encoded = True - else: - encoded = False - value = unquote(value) - mo = rfc2231_continuation.match(name) - if mo: - name, num = mo.group('name', 'num') - if num is not None: - num = int(num) - rfc2231_params.setdefault(name, []).append((num, value, encoded)) - else: - new_params.append((name, '"%s"' % quote(value))) - if rfc2231_params: - for name, continuations in list(rfc2231_params.items()): - value = [] - extended = False - # Sort by number - continuations.sort() - # And now append all values in numerical order, converting - # %-encodings for the encoded segments. If any of the - # continuation names ends in a *, then the entire string, after - # decoding segments and concatenating, must have the charset and - # language specifiers at the beginning of the string. - for num, s, encoded in continuations: - if encoded: - s = urllib.parse.unquote(s) - extended = True - value.append(s) - value = quote(EMPTYSTRING.join(value)) - if extended: - charset, language, value = decode_rfc2231(value) - new_params.append((name, (charset, language, '"%s"' % value))) - else: - new_params.append((name, '"%s"' % value)) - return new_params - -def collapse_rfc2231_value(value, errors='replace', - fallback_charset='us-ascii'): - if isinstance(value, tuple): - rawval = unquote(value[2]) - charset = value[0] or 'us-ascii' - try: - return str(rawval, charset, errors) - except LookupError: - # XXX charset is unknown to Python. - return str(rawval, fallback_charset, errors) - else: - return unquote(value) diff --git a/src/libs/python/bongo/external/simplejson/__init__.py b/src/libs/python/bongo/external/simplejson/__init__.py deleted file mode 100644 index 63b2e24..0000000 --- a/src/libs/python/bongo/external/simplejson/__init__.py +++ /dev/null @@ -1,207 +0,0 @@ -r""" -A simple, fast, extensible JSON encoder and decoder - -JSON (JavaScript Object Notation) is a subset of -JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data -interchange format. - -simplejson exposes an API familiar to uses of the standard library -marshal and pickle modules. - -Encoding basic Python object hierarchies:: - - >>> import simplejson - >>> simplejson.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) - '["foo", {"bar": ["baz", null, 1.0, 2]}]' - >>> print simplejson.dumps("\"foo\bar") - "\"foo\bar" - >>> print simplejson.dumps(u'\u1234') - "\u1234" - >>> print simplejson.dumps('\\') - "\\" - >>> print simplejson.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) - {"a": 0, "b": 0, "c": 0} - >>> from StringIO import StringIO - >>> io = StringIO() - >>> simplejson.dump(['streaming API'], io) - >>> io.getvalue() - '["streaming API"]' - -Decoding JSON:: - - >>> import simplejson - >>> simplejson.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') - [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] - >>> simplejson.loads('"\\"foo\\bar"') - u'"foo\x08ar' - >>> from StringIO import StringIO - >>> io = StringIO('["streaming API"]') - >>> simplejson.load(io) - [u'streaming API'] - -Specializing JSON object decoding:: - - >>> import simplejson - >>> def as_complex(dct): - ... if '__complex__' in dct: - ... return complex(dct['real'], dct['imag']) - ... return dct - ... - >>> simplejson.loads('{"__complex__": true, "real": 1, "imag": 2}', - ... object_hook=as_complex) - (1+2j) - -Extending JSONEncoder:: - - >>> import simplejson - >>> class ComplexEncoder(simplejson.JSONEncoder): - ... def default(self, obj): - ... if isinstance(obj, complex): - ... return [obj.real, obj.imag] - ... return simplejson.JSONEncoder.default(self, obj) - ... - >>> dumps(2 + 1j, cls=ComplexEncoder) - '[2.0, 1.0]' - >>> ComplexEncoder().encode(2 + 1j) - '[2.0, 1.0]' - >>> list(ComplexEncoder().iterencode(2 + 1j)) - ['[', '2.0', ', ', '1.0', ']'] - - -Note that the JSON produced by this module is a subset of YAML, -so it may be used as a serializer for that as well. -""" -__version__ = '1.3' -__all__ = [ - 'dump', 'dumps', 'load', 'loads', - 'JSONDecoder', 'JSONEncoder', -] - -from .decoder import JSONDecoder -from .encoder import JSONEncoder - -def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, **kw): - """ - Serialize ``obj`` as a JSON formatted stream to ``fp`` (a - ``.write()``-supporting file-like object). - - If ``skipkeys`` is ``True`` then ``dict`` keys that are not basic types - (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) - will be skipped instead of raising a ``TypeError``. - - If ``ensure_ascii`` is ``False``, then the some chunks written to ``fp`` - may be ``unicode`` instances, subject to normal Python ``str`` to - ``unicode`` coercion rules. Unless ``fp.write()`` explicitly - understands ``unicode`` (as in ``codecs.getwriter()``) this is likely - to cause an error. - - If ``check_circular`` is ``False``, then the circular reference check - for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). - - If ``allow_nan`` is ``False``, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) - in strict compliance of the JSON specification, instead of using the - JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). - - To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the - ``.default()`` method to serialize additional types), specify it with - the ``cls`` kwarg. - """ - if cls is None: - cls = JSONEncoder - iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, - check_circular=check_circular, allow_nan=allow_nan, - **kw).iterencode(obj) - # could accelerate with writelines in some versions of Python, at - # a debuggability cost - for chunk in iterable: - fp.write(chunk) - -def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, **kw): - """ - Serialize ``obj`` to a JSON formatted ``str``. - - If ``skipkeys`` is ``True`` then ``dict`` keys that are not basic types - (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) - will be skipped instead of raising a ``TypeError``. - - If ``ensure_ascii`` is ``False``, then the return value will be a - ``unicode`` instance subject to normal Python ``str`` to ``unicode`` - coercion rules instead of being escaped to an ASCII ``str``. - - If ``check_circular`` is ``False``, then the circular reference check - for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). - - If ``allow_nan`` is ``False``, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in - strict compliance of the JSON specification, instead of using the - JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). - - To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the - ``.default()`` method to serialize additional types), specify it with - the ``cls`` kwarg. - """ - if cls is None: - cls = JSONEncoder - return cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, - check_circular=check_circular, allow_nan=allow_nan, **kw).encode(obj) - -def load(fp, encoding=None, cls=None, **kw): - """ - Deserialize ``fp`` (a ``.read()``-supporting file-like object containing - a JSON document) to a Python object. - - If the contents of ``fp`` is encoded with an ASCII based encoding other - than utf-8 (e.g. latin-1), then an appropriate ``encoding`` name must - be specified. Encodings that are not ASCII based (such as UCS-2) are - not allowed, and should be wrapped with - ``codecs.getreader(fp)(encoding)``, or simply decoded to a ``unicode`` - object and passed to ``loads()`` - - To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` - kwarg. - """ - if cls is None: - cls = JSONDecoder - return cls(encoding=encoding, **kw).decode(fp.read()) - -def loads(s, encoding=None, cls=None, **kw): - """ - Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON - document) to a Python object. - - If ``s`` is a ``str`` instance and is encoded with an ASCII based encoding - other than utf-8 (e.g. latin-1) then an appropriate ``encoding`` name - must be specified. Encodings that are not ASCII based (such as UCS-2) - are not allowed and should be decoded to ``unicode`` first. - - To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` - kwarg. - """ - if cls is None: - cls = JSONDecoder - return cls(encoding=encoding, **kw).decode(s) - -def read(s): - """ - json-py API compatibility hook. Use loads(s) instead. - """ - import warnings - warnings.warn("simplejson.loads(s) should be used instead of read(s)", - DeprecationWarning) - return loads(s) - -def write(obj): - """ - json-py API compatibility hook. Use dumps(s) instead. - """ - import warnings - warnings.warn("simplejson.dumps(s) should be used instead of write(s)", - DeprecationWarning) - return dumps(obj) - - diff --git a/src/libs/python/bongo/external/simplejson/decoder.py b/src/libs/python/bongo/external/simplejson/decoder.py deleted file mode 100644 index cb78ba3..0000000 --- a/src/libs/python/bongo/external/simplejson/decoder.py +++ /dev/null @@ -1,271 +0,0 @@ -""" -Implementation of JSONDecoder -""" -import re - -from .scanner import Scanner, pattern - -FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL - -def _floatconstants(): - import struct - import sys - _BYTES = '7FF80000000000007FF0000000000000'.decode('hex') - if sys.byteorder != 'big': - _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] - nan, inf = struct.unpack('dd', _BYTES) - return nan, inf, -inf - -NaN, PosInf, NegInf = _floatconstants() - -def linecol(doc, pos): - lineno = doc.count('\n', 0, pos) + 1 - if lineno == 1: - colno = pos - else: - colno = pos - doc.rindex('\n', 0, pos) - return lineno, colno - -def errmsg(msg, doc, pos, end=None): - lineno, colno = linecol(doc, pos) - if end is None: - return '%s: line %d column %d (char %d)' % (msg, lineno, colno, pos) - endlineno, endcolno = linecol(doc, end) - return '%s: line %d column %d - line %d column %d (char %d - %d)' % ( - msg, lineno, colno, endlineno, endcolno, pos, end) - -_CONSTANTS = { - '-Infinity': NegInf, - 'Infinity': PosInf, - 'NaN': NaN, - 'true': True, - 'false': False, - 'null': None, -} - -def JSONConstant(match, context, c=_CONSTANTS): - return c[match.group(0)], None -pattern('(-?Infinity|NaN|true|false|null)')(JSONConstant) - -def JSONNumber(match, context): - match = JSONNumber.regex.match(match.string, *match.span()) - integer, frac, exp = match.groups() - if frac or exp: - res = float(integer + (frac or '') + (exp or '')) - else: - res = int(integer) - return res, None -pattern(r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?')(JSONNumber) - -STRINGCHUNK = re.compile(r'(.*?)(["\\])', FLAGS) -BACKSLASH = { - '"': '"', '\\': '\\', '/': '/', - 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t', -} - -DEFAULT_ENCODING = "utf-8" - -def scanstring(s, end, encoding=None, _b=BACKSLASH, _m=STRINGCHUNK.match): - if encoding is None: - encoding = DEFAULT_ENCODING - chunks = [] - _append = chunks.append - begin = end - 1 - while 1: - chunk = _m(s, end) - if chunk is None: - raise ValueError( - errmsg("Unterminated string starting at", s, begin)) - end = chunk.end() - content, terminator = chunk.groups() - if content: - if not isinstance(content, str): - content = str(content, encoding) - _append(content) - if terminator == '"': - break - try: - esc = s[end] - except IndexError: - raise ValueError( - errmsg("Unterminated string starting at", s, begin)) - if esc != 'u': - try: - m = _b[esc] - except KeyError: - raise ValueError( - errmsg("Invalid \\escape: %r" % (esc,), s, end)) - end += 1 - else: - esc = s[end + 1:end + 5] - try: - m = chr(int(esc, 16)) - if len(esc) != 4 or not esc.isalnum(): - raise ValueError - except ValueError: - raise ValueError(errmsg("Invalid \\uXXXX escape", s, end)) - end += 5 - _append(m) - return ''.join(chunks), end - -def JSONString(match, context): - encoding = getattr(context, 'encoding', None) - return scanstring(match.string, match.end(), encoding) -pattern(r'"')(JSONString) - -WHITESPACE = re.compile(r'\s*', FLAGS) - -def JSONObject(match, context, _w=WHITESPACE.match): - pairs = {} - s = match.string - end = _w(s, match.end()).end() - nextchar = s[end:end + 1] - # trivial empty object - if nextchar == '}': - return pairs, end + 1 - if nextchar != '"': - raise ValueError(errmsg("Expecting property name", s, end)) - end += 1 - encoding = getattr(context, 'encoding', None) - while True: - key, end = scanstring(s, end, encoding) - end = _w(s, end).end() - if s[end:end + 1] != ':': - raise ValueError(errmsg("Expecting : delimiter", s, end)) - end = _w(s, end + 1).end() - try: - value, end = next(JSONScanner.iterscan(s, idx=end)) - except StopIteration: - raise ValueError(errmsg("Expecting object", s, end)) - pairs[key] = value - end = _w(s, end).end() - nextchar = s[end:end + 1] - end += 1 - if nextchar == '}': - break - if nextchar != ',': - raise ValueError(errmsg("Expecting , delimiter", s, end - 1)) - end = _w(s, end).end() - nextchar = s[end:end + 1] - end += 1 - if nextchar != '"': - raise ValueError(errmsg("Expecting property name", s, end - 1)) - object_hook = getattr(context, 'object_hook', None) - if object_hook is not None: - pairs = object_hook(pairs) - return pairs, end -pattern(r'{')(JSONObject) - -def JSONArray(match, context, _w=WHITESPACE.match): - values = [] - s = match.string - end = _w(s, match.end()).end() - # look-ahead for trivial empty array - nextchar = s[end:end + 1] - if nextchar == ']': - return values, end + 1 - while True: - try: - value, end = next(JSONScanner.iterscan(s, idx=end)) - except StopIteration: - raise ValueError(errmsg("Expecting object", s, end)) - values.append(value) - end = _w(s, end).end() - nextchar = s[end:end + 1] - end += 1 - if nextchar == ']': - break - if nextchar != ',': - raise ValueError(errmsg("Expecting , delimiter", s, end)) - end = _w(s, end).end() - return values, end -pattern(r'\[')(JSONArray) - -ANYTHING = [ - JSONObject, - JSONArray, - JSONString, - JSONConstant, - JSONNumber, -] - -JSONScanner = Scanner(ANYTHING) - -class JSONDecoder(object): - """ - Simple JSON decoder - - Performs the following translations in decoding: - - +---------------+-------------------+ - | JSON | Python | - +===============+===================+ - | object | dict | - +---------------+-------------------+ - | array | list | - +---------------+-------------------+ - | string | unicode | - +---------------+-------------------+ - | number (int) | int, long | - +---------------+-------------------+ - | number (real) | float | - +---------------+-------------------+ - | true | True | - +---------------+-------------------+ - | false | False | - +---------------+-------------------+ - | null | None | - +---------------+-------------------+ - - It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as - their corresponding ``float`` values, which is outside the JSON spec. - """ - - _scanner = Scanner(ANYTHING) - __all__ = ['__init__', 'decode', 'raw_decode'] - - def __init__(self, encoding=None, object_hook=None): - """ - ``encoding`` determines the encoding used to interpret any ``str`` - objects decoded by this instance (utf-8 by default). It has no - effect when decoding ``unicode`` objects. - - Note that currently only encodings that are a superset of ASCII work, - strings of other encodings should be passed in as ``unicode``. - - ``object_hook``, if specified, will be called with the result - of every JSON object decoded and its return value will be used in - place of the given ``dict``. This can be used to provide custom - deserializations (e.g. to support JSON-RPC class hinting). - """ - self.encoding = encoding - self.object_hook = object_hook - - def decode(self, s, _w=WHITESPACE.match): - """ - Return the Python representation of ``s`` (a ``str`` or ``unicode`` - instance containing a JSON document) - """ - obj, end = self.raw_decode(s, idx=_w(s, 0).end()) - end = _w(s, end).end() - if end != len(s): - raise ValueError(errmsg("Extra data", s, end, len(s))) - return obj - - def raw_decode(self, s, **kw): - """ - Decode a JSON document from ``s`` (a ``str`` or ``unicode`` beginning - with a JSON document) and return a 2-tuple of the Python - representation and the index in ``s`` where the document ended. - - This can be used to decode a JSON document from a string that may - have extraneous data at the end. - """ - kw.setdefault('context', self) - try: - obj, end = next(self._scanner.iterscan(s, **kw)) - except StopIteration: - raise ValueError("No JSON object could be decoded") - return obj, end - -__all__ = ['JSONDecoder'] diff --git a/src/libs/python/bongo/external/simplejson/encoder.py b/src/libs/python/bongo/external/simplejson/encoder.py deleted file mode 100644 index 4868cdb..0000000 --- a/src/libs/python/bongo/external/simplejson/encoder.py +++ /dev/null @@ -1,289 +0,0 @@ -""" -Implementation of JSONEncoder -""" -import re - -# this should match any kind of infinity -INFCHARS = re.compile(r'[infINF]') -ESCAPE = re.compile(r'[\x00-\x19\\"\b\f\n\r\t]') -ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') -ESCAPE_DCT = { - '\\': '\\\\', - '"': '\\"', - '\b': '\\b', - '\f': '\\f', - '\n': '\\n', - '\r': '\\r', - '\t': '\\t', -} -for i in range(20): - ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) - -def floatstr(o, allow_nan=True): - s = str(o) - # If the first non-sign is a digit then it's not a special value - if (o < 0.0 and s[1].isdigit()) or s[0].isdigit(): - return s - elif not allow_nan: - raise ValueError("Out of range float values are not JSON compliant: %r" - % (o,)) - # These are the string representations on the platforms I've tried - if s == 'nan': - return 'NaN' - if s == 'inf': - return 'Infinity' - if s == '-inf': - return '-Infinity' - # NaN should either be inequal to itself, or equal to everything - if o != o or o == 0.0: - return 'NaN' - # Last ditch effort, assume inf - if o < 0: - return '-Infinity' - return 'Infinity' - -def encode_basestring(s): - """ - Return a JSON representation of a Python string - """ - def replace(match): - return ESCAPE_DCT[match.group(0)] - return '"' + ESCAPE.sub(replace, s) + '"' - -def encode_basestring_ascii(s): - def replace(match): - s = match.group(0) - try: - return ESCAPE_DCT[s] - except KeyError: - return '\\u%04x' % (ord(s),) - return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' - - -class JSONEncoder(object): - """ - Extensible JSON encoder for Python data structures. - - Supports the following objects and types by default: - - +-------------------+---------------+ - | Python | JSON | - +===================+===============+ - | dict | object | - +-------------------+---------------+ - | list, tuple | array | - +-------------------+---------------+ - | str, unicode | string | - +-------------------+---------------+ - | int, long, float | number | - +-------------------+---------------+ - | True | true | - +-------------------+---------------+ - | False | false | - +-------------------+---------------+ - | None | null | - +-------------------+---------------+ - - To extend this to recognize other objects, subclass and implement a - ``.default()`` method with another method that returns a serializable - object for ``o`` if possible, otherwise it should call the superclass - implementation (to raise ``TypeError``). - """ - __all__ = ['__init__', 'default', 'encode', 'iterencode'] - def __init__(self, skipkeys=False, ensure_ascii=True, - check_circular=True, allow_nan=True, sort_keys=False): - """ - Constructor for JSONEncoder, with sensible defaults. - - If skipkeys is False, then it is a TypeError to attempt - encoding of keys that are not str, int, long, float or None. If - skipkeys is True, such items are simply skipped. - - If ensure_ascii is True, the output is guaranteed to be str - objects with all incoming unicode characters escaped. If - ensure_ascii is false, the output will be unicode object. - - If check_circular is True, then lists, dicts, and custom encoded - objects will be checked for circular references during encoding to - prevent an infinite recursion (which would cause an OverflowError). - Otherwise, no such check takes place. - - If allow_nan is True, then NaN, Infinity, and -Infinity will be - encoded as such. This behavior is not JSON specification compliant, - but is consistent with most JavaScript based encoders and decoders. - Otherwise, it will be a ValueError to encode such floats. - - If sort_keys is True, then the output of dictionaries will be - sorted by key; this is useful for regression tests to ensure - that JSON serializations can be compared on a day-to-day basis. - """ - - self.skipkeys = skipkeys - self.ensure_ascii = ensure_ascii - self.check_circular = check_circular - self.allow_nan = allow_nan - self.sort_keys = sort_keys - - def _iterencode_list(self, lst, markers=None): - if not lst: - yield '[]' - return - if markers is not None: - markerid = id(lst) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = lst - yield '[' - first = True - for value in lst: - if first: - first = False - else: - yield ', ' - for chunk in self._iterencode(value, markers): - yield chunk - yield ']' - if markers is not None: - del markers[markerid] - - def _iterencode_dict(self, dct, markers=None): - if not dct: - yield '{}' - return - if markers is not None: - markerid = id(dct) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = dct - yield '{' - first = True - if self.ensure_ascii: - encoder = encode_basestring_ascii - else: - encoder = encode_basestring - allow_nan = self.allow_nan - if self.sort_keys: - keys = list(dct.keys()) - keys.sort() - items = [(k,dct[k]) for k in keys] - else: - items = iter(dct.items()) - for key, value in items: - if isinstance(key, str): - pass - # JavaScript is weakly typed for these, so it makes sense to - # also allow them. Many encoders seem to do something like this. - elif isinstance(key, float): - key = floatstr(key, allow_nan) - elif isinstance(key, int): - key = str(key) - elif key is True: - key = 'true' - elif key is False: - key = 'false' - elif key is None: - key = 'null' - elif self.skipkeys: - continue - else: - raise TypeError("key %r is not a string" % (key,)) - if first: - first = False - else: - yield ', ' - yield encoder(key) - yield ': ' - for chunk in self._iterencode(value, markers): - yield chunk - yield '}' - if markers is not None: - del markers[markerid] - - def _iterencode(self, o, markers=None): - if isinstance(o, str): - if self.ensure_ascii: - encoder = encode_basestring_ascii - else: - encoder = encode_basestring - yield encoder(o) - elif o is None: - yield 'null' - elif o is True: - yield 'true' - elif o is False: - yield 'false' - elif isinstance(o, int): - yield str(o) - elif isinstance(o, float): - yield floatstr(o, self.allow_nan) - elif isinstance(o, (list, tuple)): - for chunk in self._iterencode_list(o, markers): - yield chunk - elif isinstance(o, dict): - for chunk in self._iterencode_dict(o, markers): - yield chunk - else: - if markers is not None: - markerid = id(o) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = o - for chunk in self._iterencode_default(o, markers): - yield chunk - if markers is not None: - del markers[markerid] - - def _iterencode_default(self, o, markers=None): - newobj = self.default(o) - return self._iterencode(newobj, markers) - - def default(self, o): - """ - Implement this method in a subclass such that it returns - a serializable object for ``o``, or calls the base implementation - (to raise a ``TypeError``). - - For example, to support arbitrary iterators, you could - implement default like this:: - - def default(self, o): - try: - iterable = iter(o) - except TypeError: - pass - else: - return list(iterable) - return JSONEncoder.default(self, o) - """ - raise TypeError("%r is not JSON serializable" % (o,)) - - def encode(self, o): - """ - Return a JSON string representation of a Python data structure. - - >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) - '{"foo":["bar", "baz"]}' - """ - # This doesn't pass the iterator directly to ''.join() because it - # sucks at reporting exceptions. It's going to do this internally - # anyway because it uses PySequence_Fast or similar. - chunks = list(self.iterencode(o)) - return ''.join(chunks) - - def iterencode(self, o): - """ - Encode the given object and yield each string - representation as available. - - For example:: - - for chunk in JSONEncoder().iterencode(bigobject): - mysocket.write(chunk) - """ - if self.check_circular: - markers = {} - else: - markers = None - return self._iterencode(o, markers) - -__all__ = ['JSONEncoder'] diff --git a/src/libs/python/bongo/external/simplejson/jsonfilter.py b/src/libs/python/bongo/external/simplejson/jsonfilter.py deleted file mode 100644 index 990bc60..0000000 --- a/src/libs/python/bongo/external/simplejson/jsonfilter.py +++ /dev/null @@ -1,40 +0,0 @@ -import simplejson -import cgi - -class JSONFilter(object): - def __init__(self, app, mime_type='text/x-json'): - self.app = app - self.mime_type = mime_type - - def __call__(self, environ, start_response): - # Read JSON POST input to jsonfilter.json if matching mime type - response = {'status': '200 OK', 'headers': []} - def json_start_response(status, headers): - response['status'] = status - response['headers'].extend(headers) - environ['jsonfilter.mime_type'] = self.mime_type - if environ.get('REQUEST_METHOD', '') == 'POST': - if environ.get('CONTENT_TYPE', '') == self.mime_type: - args = [_ for _ in [environ.get('CONTENT_LENGTH')] if _] - data = environ['wsgi.input'].read(*list(map(int, args))) - environ['jsonfilter.json'] = simplejson.loads(data) - res = simplejson.dumps(self.app(environ, json_start_response)) - jsonp = cgi.parse_qs(environ.get('QUERY_STRING', '')).get('jsonp') - if jsonp: - content_type = 'text/javascript' - res = ''.join(jsonp + ['(', res, ')']) - elif 'Opera' in environ.get('HTTP_USER_AGENT', ''): - # Opera has bunk XMLHttpRequest support for most mime types - content_type = 'text/plain' - else: - content_type = self.mime_type - headers = [ - ('Content-type', content_type), - ('Content-length', len(res)), - ] - headers.extend(response['headers']) - start_response(response['status'], headers) - return [res] - -def factory(app, global_conf, **kw): - return JSONFilter(app, **kw) diff --git a/src/libs/python/bongo/external/simplejson/scanner.py b/src/libs/python/bongo/external/simplejson/scanner.py deleted file mode 100644 index 64f4999..0000000 --- a/src/libs/python/bongo/external/simplejson/scanner.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Iterator based sre token scanner -""" -import sre_parse, sre_compile, sre_constants -from sre_constants import BRANCH, SUBPATTERN -from re import VERBOSE, MULTILINE, DOTALL -import re - -__all__ = ['Scanner', 'pattern'] - -FLAGS = (VERBOSE | MULTILINE | DOTALL) -class Scanner(object): - def __init__(self, lexicon, flags=FLAGS): - self.actions = [None] - # combine phrases into a compound pattern - s = sre_parse.Pattern() - s.flags = flags - p = [] - for idx, token in enumerate(lexicon): - phrase = token.pattern - try: - subpattern = sre_parse.SubPattern(s, - [(SUBPATTERN, (idx + 1, sre_parse.parse(phrase, flags)))]) - except sre_constants.error: - raise - p.append(subpattern) - self.actions.append(token) - - p = sre_parse.SubPattern(s, [(BRANCH, (None, p))]) - self.scanner = sre_compile.compile(p) - - - def iterscan(self, string, idx=0, context=None): - """ - Yield match, end_idx for each match - """ - match = self.scanner.scanner(string, idx).match - actions = self.actions - lastend = idx - end = len(string) - while True: - m = match() - if m is None: - break - matchbegin, matchend = m.span() - if lastend == matchend: - break - action = actions[m.lastindex] - if action is not None: - rval, next_pos = action(m, context) - if next_pos is not None and next_pos != matchend: - # "fast forward" the scanner - matchend = next_pos - match = self.scanner.scanner(string, matchend).match - yield rval, matchend - lastend = matchend - -def pattern(pattern, flags=FLAGS): - def decorator(fn): - fn.pattern = pattern - fn.regex = re.compile(pattern, flags) - return fn - return decorator diff --git a/src/libs/python/bongo/external/simpletal/simpleTAL.py b/src/libs/python/bongo/external/simpletal/simpleTAL.py index 84b00ac..536adfc 100644 --- a/src/libs/python/bongo/external/simpletal/simpleTAL.py +++ b/src/libs/python/bongo/external/simpletal/simpleTAL.py @@ -107,7 +107,7 @@ METAL_FILL_SLOT=16 METAL_DEFINE_MACRO=17 METAL_NAME_REGEX = re.compile ("[a-zA-Z_][a-zA-Z0-9_]*") -SINGLETON_XML_REGEX = re.compile ('^<[^\s/>]+(?:\s*[^=>]+="[^">]+")*\s*/>') +SINGLETON_XML_REGEX = re.compile (r'^<[^\s/>]+(?:\s*[^=>]+="[^">]+")*\s*/>') ENTITY_REF_REGEX = re.compile (r'(?:&[a-zA-Z][\-\.a-zA-Z0-9]*[^\-\.a-zA-Z0-9])|(?:&#[xX]?[a-eA-E0-9]*[^0-9a-eA-E])') # The list of elements in HTML that can not have end tags - done as a dictionary for fast diff --git a/src/libs/python/bongo/external/vobject/__init__.py b/src/libs/python/bongo/external/vobject/__init__.py deleted file mode 100644 index 2e7f088..0000000 --- a/src/libs/python/bongo/external/vobject/__init__.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -VObject Overview -================ - vobject parses vCard or vCalendar files, returning a tree of Python objects. - It also provids an API to create vCard or vCalendar data structures which - can then be serialized. - - Parsing existing streams - ------------------------ - Streams containing one or many L{Component}s can be - parsed using L{readComponents}. As each Component - is parsed, vobject will attempt to give it a L{Behavior}. - If an appropriate Behavior is found, any base64, quoted-printable, or - backslash escaped data will automatically be decoded. Dates and datetimes - will be transformed to datetime.date or datetime.datetime instances. - Components containing recurrence information will have a special rruleset - attribute (a dateutil.rrule.rruleset instance). - - Validation - ---------- - L{Behavior} classes implement validation for - L{Component}s. To validate, an object must have all - required children. There (TODO: will be) a toggle to raise an exception or - just log unrecognized, non-experimental children and parameters. - - Creating objects programatically - -------------------------------- - A L{Component} can be created from scratch. No encoding - is necessary, serialization will encode data automatically. Factory - functions (TODO: will be) available to create standard objects. - - Serializing objects - ------------------- - Serialization: - - Looks for missing required children that can be automatically generated, - like a UID or a PRODID, and adds them - - Encodes all values that can be automatically encoded - - Checks to make sure the object is valid (unless this behavior is - explicitly disabled) - - Appends the serialized object to a buffer, or fills a new - buffer and returns it - - Examples - -------- - - >>> import datetime - >>> import dateutil.rrule as rrule - >>> x = iCalendar() - >>> x.add('vevent') - - >>> x - ]> - >>> v = x.vevent - >>> utc = icalendar.utc - >>> v.add('dtstart').value = datetime.datetime(2004, 12, 15, 14, tzinfo = utc) - >>> v - ]> - >>> x - ]>]> - >>> newrule = rrule.rruleset() - >>> newrule.rrule(rrule.rrule(rrule.WEEKLY, count=2, dtstart=v.dtstart.value)) - >>> v.rruleset = newrule - >>> list(v.rruleset) - [datetime.datetime(2004, 12, 15, 14, 0, tzinfo=tzutc()), datetime.datetime(2004, 12, 22, 14, 0, tzinfo=tzutc())] - >>> v.add('uid').value = "randomuid@MYHOSTNAME" - >>> print x.serialize() - BEGIN:VCALENDAR - VERSION:2.0 - PRODID:-//PYVOBJECT//NONSGML Version 1//EN - BEGIN:VEVENT - UID:randomuid@MYHOSTNAME - DTSTART:20041215T140000Z - RRULE:FREQ=WEEKLY;COUNT=2 - END:VEVENT - END:VCALENDAR - -""" - -from . import base, icalendar, vcard -from .base import readComponents, readOne, newFromBehavior - -def iCalendar(): - return newFromBehavior('vcalendar', '2.0') - -def vCard(): - return newFromBehavior('vcard', '3.0') diff --git a/src/libs/python/bongo/external/vobject/base.py b/src/libs/python/bongo/external/vobject/base.py deleted file mode 100644 index bcee5ef..0000000 --- a/src/libs/python/bongo/external/vobject/base.py +++ /dev/null @@ -1,950 +0,0 @@ -"""vobject module for reading vCard and vCalendar files.""" - -# generator support for Python 2.3 - - -import re -import sys -import logging -import io -import string -import exceptions -import codecs - -#------------------------------------ Logging ---------------------------------- -logger = logging.getLogger('vobject') -if not logger.handlers: - handler=logging.StreamHandler() - formatter = logging.Formatter('%(name)s %(levelname)s %(message)s') - handler.setFormatter(formatter) - logger.addHandler(handler) -logger.setLevel(logging.ERROR) # Log errors -DEBUG = False # Don't waste time on debug calls -#----------------------------------- Constants --------------------------------- -CR = chr(13) -LF = chr(10) -CRLF = CR + LF -SPACE = chr(32) -TAB = chr(9) -SPACEORTAB = SPACE + TAB -#-------------------------------- Useful modules ------------------------------- -# use doctest, it kills two birds with one stone and docstrings often become -# more readable to boot (see parseLine's docstring). -# use logging, then when debugging we can just set our verbosity. -# use epydoc syntax for documenting code, please document every class and non- -# trivial method (see http://epydoc.sourceforge.net/epytext.html -# and http://epydoc.sourceforge.net/fields.html). Also, please -# follow http://www.python.org/peps/pep-0257.html for docstrings. -#------------------------------------------------------------------------------- - -#--------------------------------- Main classes -------------------------------- -class VBase(object): - """Base class for ContentLine and Component. - - @ivar behavior: - The Behavior class associated with this object, which controls - validation, transformations, and encoding. - @ivar parentBehavior: - The object's parent's behavior, or None if no behaviored parent exists. - @ivar isNative: - Boolean describing whether this component is a Native instance. - @ivar group: - An optional group prefix, should be used only to indicate sort order in - vCards, according to RFC2426 - """ - def __init__(self, group=None, *args, **kwds): - super(VBase, self).__init__(*args, **kwds) - self.group = group - self.behavior = None - self.parentBehavior = None - self.isNative = False - - def validate(self, *args, **kwds): - """Call the behavior's validate method, or return True.""" - if self.behavior: - return self.behavior.validate(self, *args, **kwds) - else: return True - - def getChildren(self): - """Return an iterable containing the contents of the object.""" - return [] - - def clearBehavior(self, cascade=True): - """Set behavior to None. Do for all descendants if cascading.""" - self.behavior=None - if cascade: self.transformChildrenFromNative() - - def autoBehavior(self, cascade=False): - """Set behavior if name is in self.parentBehavior.knownChildren. - - If cascade is True, unset behavior and parentBehavior for all - descendants, then recalculate behavior and parentBehavior. - - """ - parentBehavior = self.parentBehavior - if parentBehavior is not None: - knownChildTup = parentBehavior.knownChildren.get(self.name, None) - if knownChildTup is not None: - behavior = getBehavior(self.name, knownChildTup[2]) - if behavior is not None: - self.setBehavior(behavior, cascade) - if isinstance(self, ContentLine) and self.encoded: - self.behavior.decode(self) - - def setBehavior(self, behavior, cascade=True): - """Set behavior. If cascade is True, autoBehavior all descendants.""" - self.behavior=behavior - if cascade: - for obj in self.getChildren(): - obj.parentBehavior=behavior - obj.autoBehavior(True) - - def transformToNative(self): - """Transform this object into a custom VBase subclass. - - transformToNative should always return a representation of this object. - It may do so by modifying self in place then returning self, or by - creating a new object. - - """ - if self.isNative or not self.behavior or not self.behavior.hasNative: - return self - else: - try: - return self.behavior.transformToNative(self) - except Exception as e: - # wrap errors in transformation in a ParseError - lineNumber = getattr(self, 'lineNumber', None) - if isinstance(e, ParseError): - if lineNumber is not None: - e.lineNumber = lineNumber - raise - else: - msg = "In transformToNative, unhandled exception: %s: %s" - msg = msg % (sys.exc_info()[0], sys.exc_info()[1]) - new_error = ParseError(msg, lineNumber) - raise ParseError(new_error).with_traceback(sys.exc_info()[2]) - - - def transformFromNative(self): - """Return self transformed into a ContentLine or Component if needed. - - May have side effects. If it does, transformFromNative and - transformToNative MUST have perfectly inverse side effects. Allowing - such side effects is convenient for objects whose transformations only - change a few attributes. - - Note that it isn't always possible for transformFromNative to be a - perfect inverse of transformToNative, in such cases transformFromNative - should return a new object, not self after modifications. - - """ - if self.isNative and self.behavior and self.behavior.hasNative: - try: - return self.behavior.transformFromNative(self) - except Exception as e: - # wrap errors in transformation in a NativeError - lineNumber = getattr(self, 'lineNumber', None) - if isinstance(e, NativeError): - if lineNumber is not None: - e.lineNumber = lineNumber - raise - else: - msg = "In transformFromNative, unhandled exception: %s: %s" - msg = msg % (sys.exc_info()[0], sys.exc_info()[1]) - new_error = NativeError(msg, lineNumber) - raise NativeError(new_error).with_traceback(sys.exc_info()[2]) - else: return self - - def transformChildrenToNative(self): - """Recursively replace children with their native representation.""" - pass - - def transformChildrenFromNative(self, clearBehavior=True): - """Recursively transform native children to vanilla representations.""" - pass - - def serialize(self, buf=None, lineLength=75, validate=True): - """Serialize to buf if it exists, otherwise return a string. - - Use self.behavior.serialize if behavior exists. - - """ - undoTransform = False - if self.behavior: - if DEBUG: logger.debug("serializing %s with behavior" % self.name) - self.behavior.generateImplicitParameters(self) - if validate: self.behavior.validate(self, raiseException=True) - if self.isNative: - transformed = self.transformFromNative() - undoTransform = True - else: - transformed = self - out = self.behavior.serialize(transformed, buf, lineLength) - if undoTransform: self.transformToNative() - return out - else: - if DEBUG: logger.debug("serializing %s without behavior" % self.name) - return defaultSerialize(self, buf, lineLength) - -def ascii(s): - return str(s).encode('ascii', 'replace') - -class ContentLine(VBase): - """Holds one content line for formats like vCard and vCalendar. - - For example:: - - - @ivar name: - The uppercased name of the contentline. - @ivar params: - A dictionary of parameters and associated lists of values (the list may - be empty for empty parameters). - @ivar value: - The value of the contentline. - @ivar singletonparams: - A list of parameters for which it's unclear if the string represents the - parameter name or the parameter value. In vCard 2.1, "The value string - can be specified alone in those cases where the value is unambiguous". - This is crazy, but we have to deal with it. - @ivar encoded: - A boolean describing whether the data in the content line is encoded. - Generally, text read from a serialized vCard or vCalendar should be - considered encoded. Data added programmatically should not be encoded. - @ivar lineNumber: - An optional line number associated with the contentline. - """ - def __init__(self, name, params, value, group=None, - encoded=False, isNative=False, - lineNumber = None, *args, **kwds): - """Take output from parseLine, convert params list to dictionary.""" - # group is used as a positional argument to match parseLine's return - super(ContentLine, self).__init__(group, *args, **kwds) - self.name = name.upper() - self.value = value - self.encoded = encoded - self.params = {} - self.singletonparams = [] - self.isNative = isNative - self.lineNumber = lineNumber - def updateTable(x): - if len(x) == 1: - self.singletonparams += x - else: - paramlist = self.params.setdefault(x[0].upper(), []) - paramlist.extend(x[1:]) - list(map(updateTable, params)) - qp = False - if 'ENCODING' in self.params: - if 'QUOTED-PRINTABLE' in self.params['ENCODING']: - qp = True - self.params['ENCODING'].remove('QUOTED-PRINTABLE') - if 0==len(self.params['ENCODING']): - del self.params['ENCODING'] - if 'QUOTED-PRINTABLE' in self.singletonparams: - qp = True - self.singletonparams.remove('QUOTED-PRINTABLE') - if qp: - self.value = str(self.value).decode('quoted-printable') - - def __eq__(self, other): - try: - return (self.name == other.name) and (self.params == other.params) and (self.value == other.value) - except: - return False - - def __getattr__(self, name): - """Make params accessible via self.foo_param or self.foo_paramlist. - - Underscores, legal in python variable names, are converted to dashes, - which are legal in IANA tokens. - - """ - try: - if name.endswith('_param'): - return self.params[name[:-6].upper().replace('_', '-')][0] - elif name.endswith('_paramlist'): - return self.params[name[:-10].upper().replace('_', '-')] - else: - raise exceptions.AttributeError(name) - except KeyError: - raise exceptions.AttributeError(name) - - def __setattr__(self, name, value): - """Make params accessible via self.foo_param or self.foo_paramlist. - - Underscores, legal in python variable names, are converted to dashes, - which are legal in IANA tokens. - - """ - if name.endswith('_param'): - if type(value) == list: - self.params[name[:-6].upper().replace('_', '-')] = value - else: - self.params[name[:-6].upper().replace('_', '-')] = [value] - elif name.endswith('_paramlist'): - if type(value) == list: - self.params[name[:-10].upper().replace('_', '-')] = value - else: - raise VObjectError("Parameter list set to a non-list") - else: - prop = getattr(self.__class__, name, None) - if isinstance(prop, property): - prop.fset(self, value) - else: - object.__setattr__(self, name, value) - - def __delattr__(self, name): - try: - if name.endswith('_param'): - del self.params[name[:-6].upper().replace('_', '-')] - elif name.endswith('_paramlist'): - del self.params[name[:-10].upper().replace('_', '-')] - else: - object.__delattr__(self, name) - except KeyError: - raise exceptions.AttributeError(name) - - def __str__(self): - return "<"+ascii(self.name)+ascii(self.params)+ascii(self.value)+">" - - def __repr__(self): - return self.__str__().replace('\n', '\\n') - - def prettyPrint(self, level = 0, tabwidth=3): - pre = ' ' * level * tabwidth - print(pre, self.name + ":", ascii(self.value)) - if self.params: - lineKeys= list(self.params.keys()) - print(pre, "params for ", self.name +':') - for aKey in lineKeys: - print(pre + ' ' * tabwidth, aKey, ascii(self.params[aKey])) - -class Component(VBase): - """A complex property that can contain multiple ContentLines. - - For our purposes, a component must start with a BEGIN:xxxx line and end with - END:xxxx, or have a PROFILE:xxx line if a top-level component. - - @ivar contents: - A dictionary of lists of Component or ContentLine instances. The keys - are the lowercased names of child ContentLines or Components. - Note that BEGIN and END ContentLines are not included in contents. - @ivar name: - Uppercase string used to represent this Component, i.e VCARD if the - serialized object starts with BEGIN:VCARD. - @ivar useBegin: - A boolean flag determining whether BEGIN: and END: lines should - be serialized. - - """ - def __init__(self, name=None, *args, **kwds): - super(Component, self).__init__(*args, **kwds) - self.contents = {} - if name: - self.name=name.upper() - self.useBegin = True - else: - self.name = '' - self.useBegin = False - - self.autoBehavior() - - def setProfile(self, name): - """Assign a PROFILE to this unnamed component. - - Used by vCard, not by vCalendar. - - """ - if self.name or self.useBegin: - if self.name == name: return - raise VObjectError("This component already has a PROFILE or uses BEGIN.") - self.name = name.upper() - - def __getattr__(self, name): - """For convenience, make self.contents directly accessible. - - Underscores, legal in python variable names, are converted to dashes, - which are legal in IANA tokens. - - """ - try: - if name.endswith('_list'): - return self.contents[name[:-5].replace('_', '-')] - else: - return self.contents[name.replace('_', '-')][0] - except KeyError: - raise exceptions.AttributeError(name) - - normal_attributes = ['contents','name','behavior','parentBehavior','group'] - def __setattr__(self, name, value): - """For convenience, make self.contents directly accessible. - - Underscores, legal in python variable names, are converted to dashes, - which are legal in IANA tokens. - - """ - if name not in self.normal_attributes and name.lower()==name: - if type(value) == list: - self.contents[name.replace('_', '-')] = value - elif name.endswith('_list'): - raise VObjectError("Component list set to a non-list") - else: - self.contents[name.replace('_', '-')] = [value] - else: - prop = getattr(self.__class__, name, None) - if isinstance(prop, property): - prop.fset(self, value) - else: - object.__setattr__(self, name, value) - - def __delattr__(self, name): - try: - if name not in self.normal_attributes and name.lower()==name: - if name.endswith('_list'): - del self.contents[name[:-5].replace('_', '-')] - else: - del self.contents[name.replace('_', '-')] - else: - object.__delattr__(self, name) - except KeyError: - raise exceptions.AttributeError(name) - - def add(self, objOrName, group = None): - """Add objOrName to contents, set behavior if it can be inferred. - - If objOrName is a string, create an empty component or line based on - behavior. If no behavior is found for the object, add a ContentLine. - - group is an optional prefix to the name of the object (see - RFC 2425). - """ - if isinstance(objOrName, VBase): - obj = objOrName - if self.behavior: - obj.parentBehavior = self.behavior - obj.autoBehavior(True) - else: - name = objOrName.upper() - try: - id=self.behavior.knownChildren[name][2] - behavior = getBehavior(name, id) - if behavior.isComponent: - obj = Component(name) - else: - obj = ContentLine(name, [], '', group) - obj.parentBehavior = self.behavior - obj.behavior = behavior - obj = obj.transformToNative() - except (KeyError, AttributeError): - obj = ContentLine(objOrName, [], '', group) - self.contents.setdefault(obj.name.lower(), []).append(obj) - return obj - - def getChildren(self): - """Return an iterable of all children.""" - for objList in list(self.contents.values()): - for obj in objList: yield obj - - def components(self): - """Return an iterable of all Component children.""" - return [i for i in self.getChildren() if isinstance(i, Component)] - - def lines(self): - """Return an iterable of all ContentLine children.""" - return [i for i in self.getChildren() if isinstance(i, ContentLine)] - - def sortChildKeys(self): - try: - first = [s for s in self.behavior.sortFirst if s in self.contents] - except: - first = [] - - items = [k for k in list(self.contents.keys()) if k not in first] - items.sort() - return first + items - - def getSortedChildren(self): - return [obj for k in self.sortChildKeys() for obj in self.contents[k]] - - def setBehaviorFromVersionLine(self, versionLine): - """Set behavior if one matches name, versionLine.value.""" - v=getBehavior(self.name, versionLine.value) - if v: self.setBehavior(v) - - def transformChildrenToNative(self): - """Recursively replace children with their native representation.""" - #sort to get dependency order right, like vtimezone before vevent - for childArray in [self.contents[k] for k in self.sortChildKeys()]: - for i in range(len(childArray)): - childArray[i]=childArray[i].transformToNative() - childArray[i].transformChildrenToNative() - - def transformChildrenFromNative(self, clearBehavior=True): - """Recursively transform native children to vanilla representations.""" - for childArray in list(self.contents.values()): - for i in range(len(childArray)): - childArray[i]=childArray[i].transformFromNative() - childArray[i].transformChildrenFromNative(clearBehavior) - if clearBehavior: - childArray[i].behavior = None - childArray[i].parentBehavior = None - - def __str__(self): - if self.name: - return "<" + self.name + "| " + str(self.getSortedChildren()) + ">" - else: - return '<' + '*unnamed*' + '| ' + str(self.getSortedChildren()) + '>' - - def __repr__(self): - return self.__str__() - - def prettyPrint(self, level = 0, tabwidth=3): - pre = ' ' * level * tabwidth - print(pre, self.name) - if isinstance(self, Component): - for line in self.getChildren(): - line.prettyPrint(level + 1, tabwidth) - print() - -class VObjectError(Exception): - def __init__(self, message, lineNumber=None): - self.message = message - if lineNumber is not None: - self.lineNumber = lineNumber - def __str__(self): - if hasattr(self, 'lineNumber'): - return "At line %s: %s" % \ - (self.lineNumber, self.message) - else: - return repr(self.message) - -class ParseError(VObjectError): - pass - -class ValidateError(VObjectError): - pass - -class NativeError(VObjectError): - pass - -#-------------------------- Parsing functions ---------------------------------- - -# parseLine regular expressions - -patterns = {} - -patterns['name'] = '[a-zA-Z0-9\-]+' -patterns['safe_char'] = '[^";:,]' -patterns['qsafe_char'] = '[^"]' - -# the combined Python string replacement and regex syntax is a little confusing; -# remember that %(foobar)s is replaced with patterns['foobar'], so for instance -# param_value is any number of safe_chars or any number of qsaf_chars surrounded -# by double quotes. - -patterns['param_value'] = ' "%(qsafe_char)s * " | %(safe_char)s * ' % patterns - - -# get a tuple of two elements, one will be empty, the other will have the value -patterns['param_value_grouped'] = """ -" ( %(qsafe_char)s * )" | ( %(safe_char)s + ) -""" % patterns - -# get a parameter and its values, without any saved groups -patterns['param'] = r""" -; (?: %(name)s ) # parameter name -(?: = (?: %(param_value)s|, ))? -""" % patterns - -# get a parameter, saving groups for name and value (value still needs parsing) -patterns['params_grouped'] = r""" -; ( %(name)s ) - -(?: = - ( - (?: (?: %(param_value)s ) )? # 0 or more parameter values, multiple - (?: , (?: %(param_value)s ) )* # parameters are comma separated - ) -)? -""" % patterns - -# get a full content line, break it up into group, name, parameters, and value -patterns['line'] = r""" -^ ((?P %(name)s)\.)?(?P %(name)s) # name group - (?P (?: %(param)s )* ) # params group (may be empty) -: (?P .* )$ # value group -""" % patterns - -' "%(qsafe_char)s*" | %(safe_char)s* ' - -param_values_re = re.compile(patterns['param_value_grouped'], re.VERBOSE) -params_re = re.compile(patterns['params_grouped'], re.VERBOSE) -line_re = re.compile(patterns['line'], re.VERBOSE) - -def parseParams(string): - """ - >>> parseParams(';ALTREP="http://www.wiz.org"') - [['ALTREP', 'http://www.wiz.org']] - >>> parseParams('') - [] - >>> parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR') - [['ALTREP', 'http://www.wiz.org;;', 'Blah', 'Foo'], ['NEXT', 'Nope'], ['BAR']] - """ - all = params_re.findall(string) - allParameters = [] - for tup in all: - paramList = [tup[0]] # tup looks like (name, valuesString) - for pair in param_values_re.findall(tup[1]): - # pair looks like ('', value) or (value, '') - if pair[0] != '': - paramList.append(pair[0]) - else: - paramList.append(pair[1]) - allParameters.append(paramList) - return allParameters - - -def parseLine(line, lineNumber = None): - """ - >>> parseLine("BLAH:") - ('BLAH', [], '', None) - >>> parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904") - ('RDATE', [], 'VALUE=DATE:19970304,19970504,19970704,19970904', None) - >>> parseLine('DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA') - ('DESCRIPTION', [['ALTREP', 'http://www.wiz.org']], 'The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA', None) - >>> parseLine("EMAIL;PREF;INTERNET:john@nowhere.com") - ('EMAIL', [['PREF'], ['INTERNET']], 'john@nowhere.com', None) - >>> parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com') - ('EMAIL', [['TYPE', 'blah', 'hah'], ['INTERNET', 'DIGI', 'DERIDOO']], 'john@nowhere.com', None) - >>> parseLine('item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;') - ('ADR', [['type', 'HOME'], ['type', 'pref']], ';;Reeperbahn 116;Hamburg;;20359;', 'item1') - >>> parseLine(":") - Traceback (most recent call last): - ... - ParseError: 'Failed to parse line: :' - """ - - match = line_re.match(line) - if match is None: - raise ParseError("Failed to parse line: %s" % line, lineNumber) - return (match.group('name'), - parseParams(match.group('params')), - match.group('value'), match.group('group')) - -# logical line regular expressions - -patterns['lineend'] = r'(?:\r\n|\r|\n|$)' -patterns['wrap'] = r'%(lineend)s [\t ]' % patterns -patterns['logicallines'] = r""" -( - (?: [^\r\n] | %(wrap)s )+ - %(lineend)s -) -""" % patterns - -patterns['wraporend'] = r'(%(wrap)s | %(lineend)s )' % patterns - -wrap_re = re.compile(patterns['wraporend'], re.VERBOSE) -logical_lines_re = re.compile(patterns['logicallines'], re.VERBOSE) - -testLines=""" -Line 0 text - , Line 0 continued. -Line 1;encoding=quoted-printable:this is an evil= - evil= - format. -Line 2 is a new line, it does not start with whitespace. -""" - -def getLogicalLines(fp, allowQP=True): - """Iterate through a stream, yielding one logical line at a time. - - Because many applications still use vCard 2.1, we have to deal with the - quoted-printable encoding for long lines, as well as the vCard 3.0 and - vCalendar line folding technique, a whitespace character at the start - of the line. - - Quoted-printable data will be decoded in the Behavior decoding phase. - - >>> import StringIO - >>> f=StringIO.StringIO(testLines) - >>> for n, l in enumerate(getLogicalLines(f)): - ... print "Line %s: %s" % (n, l[0]) - ... - Line 0: Line 0 text, Line 0 continued. - Line 1: Line 1;encoding=quoted-printable:this is an evil= - evil= - format. - Line 2: Line 2 is a new line, it does not start with whitespace. - - """ - if not allowQP: - val = fp.read(-1) - lineNumber = 1 - for match in logical_lines_re.finditer(val): - line, n = wrap_re.subn('', match.group()) - if line != '': - if type(line[0]) != str: - line = line.decode('utf-8') - yield line, lineNumber - lineNumber += n - - else: - quotedPrintable=False - newbuffer = io.StringIO - logicalLine = newbuffer() - lineNumber = 0 - lineStartNumber = 0 - while True: - line = fp.readline() - if line == '': - break - else: - if type(line[0]) != str: - line = line.decode('utf-8') - line = line.rstrip(CRLF) - lineNumber += 1 - if line.rstrip() == '': - if logicalLine.pos > 0: - yield logicalLine.getvalue(), lineStartNumber - lineStartNumber = lineNumber - logicalLine = newbuffer() - quotedPrintable=False - continue - - if quotedPrintable and allowQP: - logicalLine.write('\n') - logicalLine.write(line) - quotedPrintable=False - elif line[0] in SPACEORTAB: - logicalLine.write(line[1:]) - elif logicalLine.pos > 0: - yield logicalLine.getvalue(), lineStartNumber - lineStartNumber = lineNumber - logicalLine = newbuffer() - logicalLine.write(line) - else: - logicalLine = newbuffer() - logicalLine.write(line) - - # hack to deal with the fact that vCard 2.1 allows parameters to be - # encoded without a parameter name. False positives are unlikely, but - # possible. - val = logicalLine.getvalue() - if val[-1]=='=' and val.lower().find('quoted-printable') >= 0: - quotedPrintable=True - - if logicalLine.pos > 0: - yield logicalLine.getvalue(), lineStartNumber - - -def textLineToContentLine(text, n=None): - return ContentLine(*parseLine(text, n), **{'encoded':True, 'lineNumber' : n}) - -def dquoteEscape(param): - """Return param, or "param" if ',' or ';' or ':' is in param.""" - if param.find('"') >= 0: - raise VObjectError("Double quotes aren't allowed in parameter values.") - for char in ',;:': - if param.find(char) >= 0: - return '"'+ param + '"' - return param - -def foldOneLine(outbuf, input, lineLength = 75): - if type(input) in (str, str): input = io.StringIO(input) - input.seek(0) - outbuf.write(input.read(lineLength) + CRLF) - brokenline = input.read(lineLength - 1) - while brokenline: - outbuf.write(' ' + brokenline + CRLF) - brokenline = input.read(lineLength - 1) - -def defaultSerialize(obj, buf, lineLength): - """Encode and fold obj and its children, write to buf or return a string.""" - - if buf: outbuf = buf - else: outbuf=io.StringIO() - - if isinstance(obj, Component): - if obj.group is None: - groupString = '' - else: - groupString = obj.group + '.' - if obj.useBegin: - foldOneLine(outbuf, groupString + "BEGIN:" + obj.name, lineLength) - for child in obj.getSortedChildren(): - #validate is recursive, we only need to validate once - child.serialize(outbuf, lineLength, validate=False) - if obj.useBegin: - foldOneLine(outbuf, groupString + "END:" + obj.name, lineLength) - if DEBUG: logger.debug("Finished %s" % obj.name.upper()) - - elif isinstance(obj, ContentLine): - startedEncoded = obj.encoded - #TODO: X- lines should be considered TEXT, and should be encoded as such - if obj.behavior and not startedEncoded: obj.behavior.encode(obj) - s=io.StringIO() #unfolded buffer - if obj.group is not None: - s.write(obj.group + '.') - if DEBUG: logger.debug("Serializing line" + str(obj)) - s.write(obj.name.upper()) - for key, paramvals in obj.params.items(): - s.write(';' + key + '=' + ','.join(map(dquoteEscape, paramvals))) - s.write(':' + obj.value) - if obj.behavior and not startedEncoded: obj.behavior.decode(obj) - foldOneLine(outbuf, s, lineLength) - if DEBUG: logger.debug("Finished %s line" % obj.name.upper()) - if not buf: - return outbuf.getvalue() - - -testVCalendar=""" -BEGIN:VCALENDAR -BEGIN:VEVENT -SUMMARY;blah=hi!:Bastille Day Party -END:VEVENT -END:VCALENDAR""" - -class Stack: - def __init__(self): - self.stack = [] - def __len__(self): - return len(self.stack) - def top(self): - if len(self) == 0: return None - else: return self.stack[-1] - def topName(self): - if len(self) == 0: return None - else: return self.stack[-1].name - def modifyTop(self, item): - top = self.top() - if top: - top.add(item) - else: - new = Component() - self.push(new) - new.add(item) #add sets behavior for item and children - def push(self, obj): self.stack.append(obj) - def pop(self): return self.stack.pop() - -def readComponents(streamOrString, validate=False, transform=True): - """Generate one Component at a time from a stream. - - >>> import StringIO - >>> f = StringIO.StringIO(testVCalendar) - >>> cal=readComponents(f).next() - >>> cal - ]>]> - >>> cal.vevent.summary - - - """ - if isinstance(streamOrString, str): - stream = io.StringIO(streamOrString) - else: - stream = streamOrString - stack = Stack() - versionLine = None - n = 0 - for line, n in getLogicalLines(stream, False): # not allowing vCard 2.1 - vline = textLineToContentLine(line, n) - if vline.name == "VERSION": - versionLine = vline - stack.modifyTop(vline) - elif vline.name == "BEGIN": - stack.push(Component(vline.value, group=vline.group)) - elif vline.name == "PROFILE": - if not stack.top(): stack.push(Component()) - stack.top().setProfile(vline.value) - elif vline.name == "END": - if len(stack) == 0: - err = "Attempted to end the %s component, \ - but it was never opened" % vline.value - raise ParseError(err, n) - if vline.value.upper() == stack.topName(): #START matches END - if len(stack) == 1: - component=stack.pop() - if versionLine is not None: - component.setBehaviorFromVersionLine(versionLine) - if validate: component.validate(raiseException=True) - if transform: component.transformChildrenToNative() - yield component #EXIT POINT - else: stack.modifyTop(stack.pop()) - else: - err = "%s component wasn't closed" - raise ParseError(err % stack.topName(), n) - else: stack.modifyTop(vline) #not a START or END line - if stack.top(): - if stack.topName() is None: - logger.warning("Top level component was never named") - elif stack.top().useBegin: - raise ParseError("Component %s was never closed" % (stack.topName()), n) - yield stack.pop() - - -def readOne(stream, validate=False, transform=True): - """Return the first component from stream.""" - return next(readComponents(stream, validate, transform)) - -#--------------------------- version registry ---------------------------------- -__behaviorRegistry={} - -def registerBehavior(behavior, name=None, default=False, id=None): - """Register the given behavior. - - If default is True (or if this is the first version registered with this - name), the version will be the default if no id is given. - - """ - if not name: name=behavior.name.upper() - if id is None: id=behavior.versionString - if name in __behaviorRegistry: - if default: - __behaviorRegistry[name].insert(0, (id, behavior)) - else: - __behaviorRegistry[name].append((id, behavior)) - else: - __behaviorRegistry[name]=[(id, behavior)] - -def getBehavior(name, id=None): - """Return a matching behavior if it exists, or None. - - If id is None, return the default for name. - - """ - name=name.upper() - if name in __behaviorRegistry: - if id: - for n, behavior in __behaviorRegistry[name]: - if n==id: - return behavior - else: - return __behaviorRegistry[name][0][1] - return None - -def newFromBehavior(name, id=None): - """Given a name, return a behaviored ContentLine or Component.""" - name = name.upper() - behavior = getBehavior(name, id) - if behavior is None: - raise VObjectError("No behavior found named %s" % name) - if behavior.isComponent: - obj = Component(name) - else: - obj = ContentLine(name, [], '') - obj.behavior = behavior - obj.isNative = True - return obj - - -#--------------------------- Helper function ----------------------------------- -def backslashEscape(s): - s=s.replace("\\","\\\\").replace(";","\;").replace(",","\,") - return s.replace("\r\n", "\\n").replace("\n","\\n").replace("\r","\\n") - -#------------------- Testing and running functions ----------------------------- -if __name__ == '__main__': - from . import tests - tests._test() diff --git a/src/libs/python/bongo/external/vobject/behavior.py b/src/libs/python/bongo/external/vobject/behavior.py deleted file mode 100644 index 7195556..0000000 --- a/src/libs/python/bongo/external/vobject/behavior.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Behavior (validation, encoding, and transformations) for vobjects.""" - -from . import base - -#------------------------ Abstract class for behavior -------------------------- -class Behavior(object): - """Abstract class to describe vobject options, requirements and encodings. - - Behaviors are used for root components like VCALENDAR, for subcomponents - like VEVENT, and for individual lines in components. - - Behavior subclasses are not meant to be instantiated, all methods should - be classmethods. - - @cvar name: - The uppercase name of the object described by the class, or a generic - name if the class defines behavior for many objects. - @cvar description: - A brief excerpt from the RFC explaining the function of the component or - line. - @cvar versionString: - The string associated with the component, for instance, 2.0 if there's a - line like VERSION:2.0, an empty string otherwise. - @cvar knownChildren: - A dictionary with uppercased component/property names as keys and a - tuple (min, max, id) as value, where id is the id used by - L{registerBehavior}, min and max are the limits on how many of this child - must occur. None is used to denote no max or no id. - @cvar quotedPrintable: - A boolean describing whether the object should be encoded and decoded - using quoted printable line folding and character escaping. - @cvar hasNative: - A boolean describing whether the object can be transformed into a more - Pythonic object. - @cvar isComponent: - A boolean, True if the object should be a Component. - @cvar sortFirst: - The lower-case list of children which should come first when sorting. - @cvar allowGroup: - Whether or not vCard style group prefixes are allowed. - """ - name='' - description='' - versionString='' - knownChildren = {} - quotedPrintable = False - hasNative= False - isComponent = False - allowGroup = False - sortFirst = [] - - def __init__(self): - err="Behavior subclasses are not meant to be instantiated" - raise base.VObjectError(err) - - def validate(cls, obj, raiseException=False, complainUnrecognized=False): - """Check if the object satisfies this behavior's requirements. - - @param obj: - The L{ContentLine} or - L{Component} to be validated. - @param raiseException: - If True, raise a L{base.ValidateError} on validation failure. - Otherwise return a boolean. - @param complainUnrecognized: - If True, fail to validate if an uncrecognized parameter or child is - found. Otherwise log the lack of recognition. - - """ - if not cls.allowGroup and obj.group is not None: - err = str(obj) + " has a group, but this object doesn't support groups" - raise base.VObjectError(err) - if isinstance(obj, base.ContentLine): - return cls.lineValidate(obj, raiseException, complainUnrecognized) - elif isinstance(obj, base.Component): - count = {} - for child in obj.getChildren(): - if not child.validate(raiseException, complainUnrecognized): - return False - name=child.name.upper() - count[name] = count.get(name, 0) + 1 - for key, val in cls.knownChildren.items(): - if count.get(key,0) < val[0]: - if raiseException: - m = "%s components must contain at least %i %s" - raise base.ValidateError(m % (cls.name, val[0], key)) - return False - if val[1] and count.get(key,0) > val[1]: - if raiseException: - m = "%s components cannot contain more than %i %s" - raise base.ValidateError(m % (cls.name, val[1], key)) - return False - return True - else: - err = str(obj) + " is not a Component or Contentline" - raise base.VObjectError(err) - - def lineValidate(cls, line, raiseException, complainUnrecognized): - """Examine a line's parameters and values, return True if valid.""" - return True - - def decode(cls, line): - if line.encoded: line.encoded=0 - - def encode(cls, line): - if not line.encoded: line.encoded=1 - - def transformToNative(cls, obj): - """Turn a ContentLine or Component into a Python-native representation. - - If appropriate, turn dates or datetime strings into Python objects. - Components containing VTIMEZONEs turn into VtimezoneComponents. - - """ - return obj - - def transformFromNative(cls, obj): - """Inverse of transformToNative.""" - raise base.NativeError("No transformFromNative defined") - - def generateImplicitParameters(cls, obj): - """Generate any required information that don't yet exist.""" - pass - - def serialize(cls, obj, buf, lineLength): - """Set implicit parameters, do encoding, return unicode string. - - If validate is True, raise VObjectError if the line doesn't validate - after implicit parameters are generated. - - Default is to call base.defaultSerialize. - - """ - return base.defaultSerialize(obj, buf, lineLength) - - lineValidate = classmethod(lineValidate) - validate = classmethod(validate) - decode = classmethod(decode) - encode = classmethod(encode) - serialize = classmethod(serialize) - transformToNative = classmethod(transformToNative) - transformFromNative = classmethod(transformFromNative) - generateImplicitParameters = classmethod(generateImplicitParameters) \ No newline at end of file diff --git a/src/libs/python/bongo/external/vobject/icalendar.py b/src/libs/python/bongo/external/vobject/icalendar.py deleted file mode 100644 index 923d65b..0000000 --- a/src/libs/python/bongo/external/vobject/icalendar.py +++ /dev/null @@ -1,1661 +0,0 @@ -"""Definitions and behavior for iCalendar, also known as vCalendar 2.0""" - -# generator support for Python 2.3 - - -import string -from . import behavior -import bongo.external.dateutil.rrule as rrule -import bongo.external.dateutil.tz as tz -import io -import datetime -import socket, random #for generating a UID -import itertools - -from .base import VObjectError, NativeError, ValidateError, ParseError, \ - VBase, Component, ContentLine, logger, defaultSerialize, \ - registerBehavior, backslashEscape, foldOneLine, \ - newFromBehavior, CRLF, LF - -#------------------------------- Constants ------------------------------------- -DATENAMES = ("rdate", "exdate") -RULENAMES = ("exrule", "rrule") -DATESANDRULES = ("exrule", "rrule", "rdate", "exdate") -PRODID = "-//PYVOBJECT//NONSGML Version 1//EN" - -WEEKDAYS = "MO", "TU", "WE", "TH", "FR", "SA", "SU" -FREQUENCIES = ('YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', - 'SECONDLY') - -zeroDelta = datetime.timedelta(0) -twoHours = datetime.timedelta(hours=2) - -#---------------------------- TZID registry ------------------------------------ -__tzidMap={} - -def registerTzid(tzid, tzinfo): - """Register a tzid -> tzinfo mapping.""" - __tzidMap[tzid]=tzinfo - -def getTzid(tzid): - """Return the tzid if it exists, or None.""" - return __tzidMap.get(tzid, None) - -utc = tz.tzutc() -registerTzid("UTC", utc) - -#-------------------- Helper subclasses ---------------------------------------- - -class TimezoneComponent(Component): - """A VTIMEZONE object. - - VTIMEZONEs are parsed by dateutil.tz.tzical, the resulting datetime.tzinfo - subclass is stored in self.tzinfo, self.tzid stores the TZID associated - with this timezone. - - @ivar name: - The uppercased name of the object, in this case always 'VTIMEZONE'. - @ivar tzinfo: - A datetime.tzinfo subclass representing this timezone. - @ivar tzid: - The string used to refer to this timezone. - - """ - def __init__(self, tzinfo=None, *args, **kwds): - """Accept an existing Component or a tzinfo class.""" - super(TimezoneComponent, self).__init__(*args, **kwds) - self.isNative=True - # hack to make sure a behavior is assigned - if self.behavior is None: - self.behavior = VTimezone - if tzinfo is not None: - self.tzinfo = tzinfo - if not hasattr(self, 'name') or self.name == '': - self.name = 'VTIMEZONE' - self.useBegin = True - - def registerTzinfo(obj, tzinfo): - """Register tzinfo if it's not already registered, return its tzid.""" - tzid = obj.pickTzid(tzinfo) - if tzid and not getTzid(tzid): - registerTzid(tzid, tzinfo) - return tzid - registerTzinfo = classmethod(registerTzinfo) - - def gettzinfo(self): - # workaround for dateutil failing to parse some experimental properties - good_lines = ('rdate', 'rrule', 'dtstart', 'tzname', 'tzoffsetfrom', - 'tzoffsetto', 'tzid') - buffer = io.StringIO() - def customSerialize(obj): - if isinstance(obj, Component): - foldOneLine(buffer, "BEGIN:" + obj.name) - for child in obj.lines(): - if child.name.lower() in good_lines: - child.serialize(buffer, 75, validate=False) - for comp in obj.components(): - customSerialize(comp) - foldOneLine(buffer, "END:" + obj.name) - customSerialize(self) - return tz.tzical(io.StringIO(str(buffer.getvalue()))).get() - - def settzinfo(self, tzinfo, start=2000, end=2030): - """Create appropriate objects in self to represent tzinfo. - - Collapse DST transitions to rrules as much as possible. - - Assumptions: - - DST <-> Standard transitions occur on the hour - - never within a month of one another - - twice or fewer times a year - - never in the month of December - - DST always moves offset exactly one hour later - - tzinfo classes dst method always treats times that could be in either - offset as being in the later regime - - """ - def fromLastWeek(dt): - """How many weeks from the end of the month dt is, starting from 1.""" - weekDelta = datetime.timedelta(weeks=1) - n = 1 - current = dt + weekDelta - while current.month == dt.month: - n += 1 - current += weekDelta - return n - - # lists of dictionaries defining rules which are no longer in effect - completed = {'daylight' : [], 'standard' : []} - - # dictionary defining rules which are currently in effect - working = {'daylight' : None, 'standard' : None} - - # rule may be based on the nth week of the month or the nth from the last - for year in range(start, end + 1): - newyear = datetime.datetime(year, 1, 1) - for transitionTo in 'daylight', 'standard': - transition = getTransition(transitionTo, year, tzinfo) - oldrule = working[transitionTo] - - if transition == newyear: - # transitionTo is in effect for the whole year - rule = {'end' : None, - 'start' : newyear, - 'month' : 1, - 'weekday' : None, - 'hour' : None, - 'plus' : None, - 'minus' : None, - 'name' : tzinfo.tzname(newyear), - 'offset' : tzinfo.utcoffset(newyear), - 'offsetfrom' : tzinfo.utcoffset(newyear)} - if oldrule is None: - # transitionTo was not yet in effect - working[transitionTo] = rule - else: - # transitionTo was already in effect - if (oldrule['offset'] != - tzinfo.utcoffset(newyear)): - # old rule was different, it shouldn't continue - oldrule['end'] = year - 1 - completed[transitionTo].append(oldrule) - working[transitionTo] = rule - elif transition is None: - # transitionTo is not in effect - if oldrule is not None: - # transitionTo used to be in effect - oldrule['end'] = year - 1 - completed[transitionTo].append(oldrule) - working[transitionTo] = None - else: - # an offset transition was found - old_offset = tzinfo.utcoffset(transition - twoHours) - rule = {'end' : None, # None, or an integer year - 'start' : transition, # the datetime of transition - 'month' : transition.month, - 'weekday' : transition.weekday(), - 'hour' : transition.hour, - 'name' : tzinfo.tzname(transition), - 'plus' : (transition.day - 1)/ 7 + 1,#nth week of the month - 'minus' : fromLastWeek(transition), #nth from last week - 'offset' : tzinfo.utcoffset(transition), - 'offsetfrom' : old_offset} - - if oldrule is None: - working[transitionTo] = rule - else: - plusMatch = rule['plus'] == oldrule['plus'] - minusMatch = rule['minus'] == oldrule['minus'] - truth = plusMatch or minusMatch - for key in 'month', 'weekday', 'hour', 'offset': - truth = truth and rule[key] == oldrule[key] - if truth: - # the old rule is still true, limit to plus or minus - if not plusMatch: - oldrule['plus'] = None - if not minusMatch: - oldrule['minus'] = None - else: - # the new rule did not match the old - oldrule['end'] = year - 1 - completed[transitionTo].append(oldrule) - working[transitionTo] = rule - - for transitionTo in 'daylight', 'standard': - if working[transitionTo] is not None: - completed[transitionTo].append(working[transitionTo]) - - self.tzid = [] - self.daylight = [] - self.standard = [] - - self.add('tzid').value = self.pickTzid(tzinfo) - - old = None - for transitionTo in 'daylight', 'standard': - for rule in completed[transitionTo]: - comp = self.add(transitionTo) - dtstart = comp.add('dtstart') - dtstart.value = rule['start'] - comp.add('tzname').value = rule['name'] - line = comp.add('tzoffsetto') - line.value = deltaToOffset(rule['offset']) - line = comp.add('tzoffsetfrom') - line.value = deltaToOffset(rule['offsetfrom']) - - if rule['plus'] is not None: - num = rule['plus'] - elif rule['minus'] is not None: - num = -1 * rule['minus'] - else: - num = None - if num is not None: - dayString = ";BYDAY=" + str(num) + WEEKDAYS[rule['weekday']] - else: - dayString = "" - if rule['end'] is not None: - if rule['hour'] is None: - # all year offset, with no rule - endDate = datetime.datetime(rule['end'], 1, 1) - else: - weekday = rrule.weekday(rule['weekday'], num) - du_rule = rrule.rrule(rrule.YEARLY, - bymonth = rule['month'],byweekday = weekday, - dtstart = datetime.datetime( - rule['end'], 1, 1, rule['hour']) - ) - endDate = du_rule[0] - endDate = endDate.replace(tzinfo = utc) - rule['offsetfrom'] - endString = ";UNTIL="+ dateTimeToString(endDate) - else: - endString = '' - rulestring = "FREQ=YEARLY%s;BYMONTH=%s%s" % \ - (dayString, str(rule['month']), endString) - - comp.add('rrule').value = rulestring - - tzinfo = property(gettzinfo, settzinfo) - # prevent Component's __setattr__ from overriding the tzinfo property - normal_attributes = Component.normal_attributes + ['tzinfo'] - - def pickTzid(tzinfo): - """ - Given a tzinfo class, use known APIs to determine TZID, or use tzname. - """ - if tzinfo is None or tzinfo_eq(tzinfo, utc): - #If tzinfo is UTC, we don't need a TZID - return None - # try PyICU's tzid key - if hasattr(tzinfo, 'tzid'): - return tzinfo.tzid - - # try tzical's tzid key - elif hasattr(tzinfo, '_tzid'): - return tzinfo._tzid - else: - # return tzname for standard (non-DST) time - notDST = datetime.timedelta(0) - for month in range(1,13): - dt = datetime.datetime(2000, month, 1) - if tzinfo.dst(dt) == notDST: - return tzinfo.tzname(dt) - # there was no standard time in 2000! - raise VObjectError("Unable to guess TZID for tzinfo %s" % str(tzinfo)) - pickTzid = staticmethod(pickTzid) - - def __str__(self): - return "" - - def __repr__(self): - return self.__str__() - - def prettyPrint(self, level, tabwidth): - pre = ' ' * level * tabwidth - print(pre, self.name) - print(pre, "TZID:", self.tzid) - print() - -class RecurringComponent(Component): - """A vCalendar component like VEVENT or VTODO which may recur. - - Any recurring component can have one or multiple RRULE, RDATE, - EXRULE, or EXDATE lines, and one or zero DTSTART lines. It can also have a - variety of children that don't have any recurrence information. - - In the example below, note that dtstart is included in the rruleset. - This is not the default behavior for dateutil's rrule implementation unless - dtstart would already have been a member of the recurrence rule, and as a - result, COUNT is wrong. This can be worked around when getting rruleset by - adjusting count down by one if an rrule has a count and dtstart isn't in its - result set, but by default, the rruleset property doesn't do this work - around, to access it getrruleset must be called with addRDate set True. - - >>> import dateutil.rrule, datetime - >>> vevent = RecurringComponent(name='VEVENT') - >>> vevent.add('rrule').value =u"FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH" - >>> vevent.add('dtstart').value = datetime.datetime(2005, 1, 19, 9) - - When creating rrule's programmatically it should be kept in - mind that count doesn't necessarily mean what rfc2445 says. - - >>> list(vevent.rruleset) - [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)] - >>> list(vevent.getrruleset(addRDate=True)) - [datetime.datetime(2005, 1, 19, 9, 0), datetime.datetime(2005, 1, 20, 9, 0)] - - Also note that dateutil will expand all-day events (datetime.date values) to - datetime.datetime value with time 0 and no timezone. - - >>> vevent.dtstart.value = datetime.date(2005,3,18) - >>> list(vevent.rruleset) - [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)] - >>> list(vevent.getrruleset(True)) - [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)] - - @ivar rruleset: - A U{rruleset}. - """ - def __init__(self, *args, **kwds): - super(RecurringComponent, self).__init__(*args, **kwds) - self.isNative=True - #self.clobberedRDates=[] - - - def getrruleset(self, addRDate = False): - """Get an rruleset created from self. - - If addRDate is True, add an RDATE for dtstart if it's not included in - an RRULE, and count is decremented if it exists. - - Note that for rules which don't match DTSTART, DTSTART may not appear - in list(rruleset), although it should. By default, an RDATE is not - created in these cases, and count isn't updated, so dateutil may list - a spurious occurrence. - - """ - rruleset = None - for name in DATESANDRULES: - addfunc = None - for line in self.contents.get(name, ()): - # don't bother creating a rruleset unless there's a rule - if rruleset is None: - rruleset = rrule.rruleset() - if addfunc is None: - addfunc=getattr(rruleset, name) - - if name in DATENAMES: - if type(line.value[0]) == datetime.datetime: - list(map(addfunc, line.value)) - elif type(line.value) == datetime.date: - for dt in line.value: - addfunc(datetime.datetime(dt.year, dt.month, dt.day)) - else: - # ignore RDATEs with PERIOD values for now - pass - elif name in RULENAMES: - try: - dtstart = self.dtstart.value - except AttributeError as KeyError: - # if there's no dtstart, just return None - return None - # rrulestr complains about unicode, so cast to str - addfunc(rrule.rrulestr(str(line.value), - dtstart=dtstart)) - if name == 'rrule' and addRDate: - try: - # dateutils does not work with all-day (datetime.date) items - # so we need to convert to a datetime.datetime - # (which is what dateutils does internally) - if not isinstance(dtstart, datetime.datetime): - adddtstart = datetime.datetime.fromordinal(dtstart.toordinal()) - else: - adddtstart = dtstart - if rruleset._rrule[-1][0] != adddtstart: - rruleset.rdate(adddtstart) - added = True - else: - added = False - except IndexError: - # it's conceivable that an rrule might have 0 datetimes - added = False - if added and rruleset._rrule[-1]._count != None: - rruleset._rrule[-1]._count -= 1 - return rruleset - - def setrruleset(self, rruleset): - dtstart = self.dtstart.value - isDate = datetime.date == type(dtstart) - if isDate: - dtstart = datetime.datetime(dtstart.year,dtstart.month, dtstart.day) - untilSerialize = dateToString - else: - # make sure to convert time zones to UTC - untilSerialize = lambda x: dateTimeToString(x, True) - - for name in DATESANDRULES: - if hasattr(self.contents, name): - del self.contents[name] - setlist = getattr(rruleset, '_' + name) - if name in DATENAMES: - setlist = list(setlist) # make a copy of the list - if name == 'rdate' and dtstart in setlist: - setlist.remove(dtstart) - if isDate: - setlist = [dt.date() for dt in setlist] - if len(setlist) > 0: - self.add(name).value = setlist - elif name in RULENAMES: - for rule in setlist: - buf = io.StringIO() - buf.write('FREQ=') - buf.write(FREQUENCIES[rule._freq]) - - values = {} - - if rule._interval != 1: - values['INTERVAL'] = [str(rule._interval)] - if rule._wkst != 0: # wkst defaults to Monday - values['WKST'] = [WEEKDAYS[rule._wkst]] - if rule._bysetpos is not None: - values['BYSETPOS'] = [str(i) for i in rule._bysetpos] - - if rule._count is not None: - values['COUNT'] = [str(rule._count)] - elif rule._until is not None: - values['UNTIL'] = [untilSerialize(rule._until)] - - days = [] - if (rule._byweekday is not None and ( - rrule.WEEKLY != rule._freq or - len(rule._byweekday) != 1 or - rule._dtstart.weekday() != rule._byweekday[0])): - # ignore byweekday if freq is WEEKLY and day correlates - # with dtstart because it was automatically set by - # dateutil - days.extend([WEEKDAYS[n] for n in rule._byweekday]) - - if rule._bynweekday is not None: - days.extend([str(n) + WEEKDAYS[day] for day, n in rule._bynweekday]) - - if len(days) > 0: - values['BYDAY'] = days - - if rule._bymonthday is not None and len(rule._bymonthday) > 0: - if not (rule._freq <= rrule.MONTHLY and - len(rule._bymonthday) == 1 and - rule._bymonthday[0] == rule._dtstart.day): - # ignore bymonthday if it's generated by dateutil - values['BYMONTHDAY'] = [str(n) for n in rule._bymonthday] - - if rule._bymonth is not None and len(rule._bymonth) > 0: - if not (rule._freq == rrule.YEARLY and - len(rule._bymonth) == 1 and - rule._bymonth[0] == rule._dtstart.month): - # ignore bymonth if it's generated by dateutil - values['BYMONTH'] = [str(n) for n in rule._bymonth] - - if rule._byyearday is not None: - values['BYYEARDAY'] = [str(n) for n in rule._byyearday] - if rule._byweekno is not None: - values['BYWEEKNO'] = [str(n) for n in rule._byweekno] - - # byhour, byminute, bysecond are always ignored for now - - - for key, paramvals in values.items(): - buf.write(';') - buf.write(key) - buf.write('=') - buf.write(','.join(paramvals)) - - self.add(name).value = buf.getvalue() - - - - rruleset = property(getrruleset, setrruleset) - - def __setattr__(self, name, value): - """For convenience, make self.contents directly accessible.""" - if name == 'rruleset': - self.setrruleset(value) - else: - super(RecurringComponent, self).__setattr__(name, value) - -class RecurringBehavior(behavior.Behavior): - """Parent Behavior for components which should be RecurringComponents.""" - hasNative = True - isComponent = True - - def transformToNative(obj): - """Turn a recurring Component into a RecurringComponent.""" - if not obj.isNative: - object.__setattr__(obj, '__class__', RecurringComponent) - obj.isNative = True - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - if obj.isNative: - object.__setattr__(obj, '__class__', Component) - obj.isNative = False - return obj - transformFromNative = staticmethod(transformFromNative) - - def generateImplicitParameters(obj): - """Generate a UID if one does not exist. - - This is just a dummy implementation, for now. - - """ - if not hasattr(obj, 'uid'): - rand = str(int(random.random() * 100000)) - now = datetime.datetime.now(utc) - now = dateTimeToString(now) - host = socket.gethostname() - obj.add(ContentLine('UID', [], now + '-' + rand + '@' + host)) - generateImplicitParameters = staticmethod(generateImplicitParameters) - - - -class DateTimeBehavior(behavior.Behavior): - """Parent Behavior for ContentLines containing one DATE-TIME.""" - hasNative = True - - def transformToNative(obj): - """Turn obj.value into a datetime. - - RFC2445 allows times without time zone information, "floating times" - in some properties. Mostly, this isn't what you want, but when parsing - a file, real floating times are noted by setting to 'TRUE' the - X-VOBJ-FLOATINGTIME-ALLOWED parameter. - - """ - if obj.isNative: return obj - obj.isNative = True - obj.value=str(obj.value) - #we're cheating a little here, parseDtstart allows DATE - obj.value=parseDtstart(obj) - if obj.value.tzinfo is None: - obj.params['X-VOBJ-FLOATINGTIME-ALLOWED'] = ['TRUE'] - if obj.params.get('TZID'): - del obj.params['TZID'] - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj, convertToUTC = False): - """Replace the datetime in obj.value with an ISO 8601 string.""" - if obj.isNative: - obj.isNative = False - tzid = TimezoneComponent.registerTzinfo(obj.value.tzinfo) - obj.value = dateTimeToString(obj.value, convertToUTC) - if not convertToUTC and tzid is not None: - obj.tzid_param = tzid - - return obj - transformFromNative = staticmethod(transformFromNative) - -class UTCDateTimeBehavior(DateTimeBehavior): - """A value which must be specified in UTC.""" - - def transformFromNative(obj): - return DateTimeBehavior.transformFromNative(obj, False) - transformFromNative = staticmethod(transformFromNative) - -class DateOrDateTimeBehavior(behavior.Behavior): - """Parent Behavior for ContentLines containing one DATE or DATE-TIME.""" - hasNative = True - - def transformToNative(obj): - """Turn obj.value into a date or datetime.""" - if obj.isNative: return obj - obj.isNative = True - if obj.value == '': return obj - obj.value=str(obj.value) - obj.value=parseDtstart(obj) - if getattr(obj, 'value_param', 'DATE-TIME').upper() == 'DATE-TIME': - if hasattr(obj, 'tzid_param'): del obj.tzid_param - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - """Replace the date or datetime in obj.value with an ISO 8601 string.""" - if type(obj.value) == datetime.date: - obj.isNative = False - obj.value_param = 'DATE' - obj.value = dateToString(obj.value) - return obj - else: return DateTimeBehavior.transformFromNative(obj) - transformFromNative = staticmethod(transformFromNative) - -class MultiDateBehavior(behavior.Behavior): - """ - Parent Behavior for ContentLines containing one or more DATE, DATE-TIME, or - PERIOD. - - """ - hasNative = True - - def transformToNative(obj): - """ - Turn obj.value into a list of dates, datetimes, or - (datetime, timedelta) tuples. - - """ - if obj.isNative: - return obj - obj.isNative = True - if obj.value == '': - obj.value = [] - return obj - tzinfo = getTzid(getattr(obj, 'tzid_param', None)) - valueParam = getattr(obj, 'value_param', "DATE-TIME").upper() - valTexts = obj.value.split(",") - if valueParam == "DATE": - obj.value = [stringToDate(x) for x in valTexts] - elif valueParam == "DATE-TIME": - obj.value = [stringToDateTime(x, tzinfo) for x in valTexts] - elif valueParam == "PERIOD": - obj.value = [stringToPeriod(x, tzinfo) for x in valTexts] - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - """ - Replace the date, datetime or period tuples in obj.value with - appropriate strings. - - """ - # Fixme: obj.value should be a list, so this test should never succeed - if type(obj.value) == datetime.date: - obj.isNative = False - obj.value_param = 'DATE' - obj.value = ','.join([dateToString(val) for val in obj.value]) - return obj - # Fixme: handle PERIOD case - else: - if obj.isNative: - obj.isNative = False - transformed = [] - tzid = None - for val in obj.value: - if tzid is None and type(val) == datetime.datetime: - tzid = TimezoneComponent.registerTzinfo(val.tzinfo) - if tzid is not None: - obj.tzid_param = tzid - transformed.append(dateTimeToString(val)) - obj.value = ','.join(transformed) - return obj - transformFromNative = staticmethod(transformFromNative) - -class TextBehavior(behavior.Behavior): - """Provide backslash escape encoding/decoding for single valued properties. - - TextBehavior also deals with base64 encoding if the ENCODING parameter is - explicitly set to BASE64. - - """ - base64string = 'BASE64' # vCard uses B - - def decode(cls, line): - """Remove backslash escaping from line.value.""" - if line.encoded: - encoding = getattr(line, 'encoding_param', None) - if encoding and encoding.upper() == cls.base64string: - line.value = line.value.decode('base64') - else: - line.value = stringToTextValues(line.value)[0] - line.encoded=False - decode = classmethod(decode) - - def encode(cls, line): - """Backslash escape line.value.""" - if not line.encoded: - encoding = getattr(line, 'encoding_param', None) - if encoding and encoding.upper() == cls.base64string: - line.value = line.value.encode('base64').replace('\n', '') - else: - line.value = backslashEscape(line.value) - line.encoded=True - encode = classmethod(encode) - -class MultiTextBehavior(behavior.Behavior): - """Provide backslash escape encoding/decoding of each of several values. - - After transformation, value is a list of strings. - - """ - - def decode(line): - """Remove backslash escaping from line.value, then split on commas.""" - if line.encoded: - line.value = stringToTextValues(line.value) - line.encoded=False - decode = staticmethod(decode) - - def encode(line): - """Backslash escape line.value.""" - if not line.encoded: - line.value = ','.join([backslashEscape(val) for val in line.value]) - line.encoded=True - encode = staticmethod(encode) - - -#------------------------ Registered Behavior subclasses ----------------------- -class VCalendar2_0(behavior.Behavior): - """vCalendar 2.0 behavior.""" - name = 'VCALENDAR' - description = 'vCalendar 2.0, also known as iCalendar.' - versionString = '2.0' - isComponent = True - sortFirst = ('version', 'calscale', 'method', 'prodid', 'vtimezone') - knownChildren = {'CALSCALE': (0, 1, None),#min, max, behaviorRegistry id - 'METHOD': (0, 1, None), - 'VERSION': (0, 1, None),#required, but auto-generated - 'PRODID': (1, 1, None), - 'VTIMEZONE': (0, None, None), - 'VEVENT': (0, None, None), - 'VTODO': (0, None, None), - 'VJOURNAL': (0, None, None), - 'VFREEBUSY': (0, None, None) - } - - def generateImplicitParameters(cls, obj): - """Create PRODID, VERSION, and VTIMEZONEs if needed. - - VTIMEZONEs will need to exist whenever TZID parameters exist or when - datetimes with tzinfo exist. - - """ - for comp in obj.components(): - if comp.behavior is not None: - comp.behavior.generateImplicitParameters(comp) - if not hasattr(obj, 'prodid'): - obj.add(ContentLine('PRODID', [], PRODID)) - if not hasattr(obj, 'version'): - obj.add(ContentLine('VERSION', [], cls.versionString)) - tzidsUsed = {} - - def findTzids(obj, table): - if isinstance(obj, ContentLine): - if getattr(obj, 'tzid_param', None): - table[obj.tzid_param] = 1 - else: - if type(obj.value) == list: - for item in obj.value: - tzinfo = getattr(obj.value, 'tzinfo', None) - tzid = TimezoneComponent.registerTzinfo(tzinfo) - if tzid: - table[tzid] = 1 - else: - tzinfo = getattr(obj.value, 'tzinfo', None) - tzid = TimezoneComponent.registerTzinfo(tzinfo) - if tzid: - table[tzid] = 1 - for child in obj.getChildren(): - if obj.name is not 'VTIMEZONE': - findTzids(child, table) - - findTzids(obj, tzidsUsed) - oldtzids = [x.tzid.value for x in getattr(obj, 'vtimezone_list', [])] - for tzid in list(tzidsUsed.keys()): - if tzid == 'UTC' or tzid in oldtzids: continue - obj.add(TimezoneComponent(tzinfo=getTzid(tzid))) - generateImplicitParameters = classmethod(generateImplicitParameters) -registerBehavior(VCalendar2_0) - -class VTimezone(behavior.Behavior): - """Timezone behavior.""" - name = 'VTIMEZONE' - hasNative = True - isComponent = True - description = 'A grouping of component properties that defines a time zone.' - sortFirst = ('tzid', 'last-modified', 'tzurl', 'standard', 'daylight') - knownChildren = {'TZID': (1, 1, None),#min, max, behaviorRegistry id - 'LAST-MODIFIED':(0, 1, None), - 'TZURL': (0, 1, None), - 'STANDARD': (0, None, None),#NOTE: One of Standard or - 'DAYLIGHT': (0, None, None) # Daylight must appear - } - - def validate(cls, obj, raiseException, *args): - return True #TODO: FIXME - if 'standard' in obj.contents or 'daylight' in obj.contents: - return super(VTimezone, cls).validate(obj, raiseException, *args) - else: - if raiseException: - m = "VTIMEZONE components must contain a STANDARD or a DAYLIGHT\ - component" - raise ValidateError(m) - return False - validate = classmethod(validate) - - - def transformToNative(obj): - if not obj.isNative: - object.__setattr__(obj, '__class__', TimezoneComponent) - obj.isNative = True - obj.registerTzinfo(obj.tzinfo) - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - return obj - transformFromNative = staticmethod(transformFromNative) - - -registerBehavior(VTimezone) - -class DaylightOrStandard(behavior.Behavior): - hasNative = False - isComponent = True - knownChildren = {'DTSTART': (1, 1, None)}#min, max, behaviorRegistry id - -registerBehavior(DaylightOrStandard, 'STANDARD') -registerBehavior(DaylightOrStandard, 'DAYLIGHT') - - -class VEvent(RecurringBehavior): - """Event behavior.""" - name='VEVENT' - sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend') - - description='A grouping of component properties, and possibly including \ - "VALARM" calendar components, that represents a scheduled \ - amount of time on a calendar.' - knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id - 'CLASS': (0, 1, None), - 'CREATED': (0, 1, None), - 'DESCRIPTION': (0, 1, None), - 'GEO': (0, 1, None), - 'LAST-MODIFIED':(0, 1, None), - 'LOCATION': (0, 1, None), - 'ORGANIZER': (0, 1, None), - 'PRIORITY': (0, 1, None), - 'DTSTAMP': (0, 1, None), - 'SEQUENCE': (0, 1, None), - 'STATUS': (0, 1, None), - 'SUMMARY': (0, 1, None), - 'TRANSP': (0, 1, None), - 'UID': (1, 1, None), - 'URL': (0, 1, None), - 'RECURRENCE-ID':(0, 1, None), - 'DTEND': (0, 1, None), #NOTE: Only one of DtEnd or - 'DURATION': (0, 1, None), # Duration can appear - 'ATTACH': (0, None, None), - 'ATTENDEE': (0, None, None), - 'CATEGORIES': (0, None, None), - 'COMMENT': (0, None, None), - 'CONTACT': (0, None, None), - 'EXDATE': (0, None, None), - 'EXRULE': (0, None, None), - 'REQUEST-STATUS': (0, None, None), - 'RELATED-TO': (0, None, None), - 'RESOURCES': (0, None, None), - 'RDATE': (0, None, None), - 'RRULE': (0, None, None), - 'VALARM': (0, None, None) - } - - def validate(cls, obj, raiseException, *args): - if 'DTEND' in obj.contents and 'DURATION' in obj.contents: - if raiseException: - m = "VEVENT components cannot contain both DTEND and DURATION\ - components" - raise ValidateError(m) - return False - else: - return super(VEvent, cls).validate(obj, raiseException, *args) - validate = classmethod(validate) - -registerBehavior(VEvent) - - -class VTodo(RecurringBehavior): - """To-do behavior.""" - name='VTODO' - description='A grouping of component properties and possibly "VALARM" \ - calendar components that represent an action-item or \ - assignment.' - knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id - 'CLASS': (0, 1, None), - 'COMPLETED': (0, 1, None), - 'CREATED': (0, 1, None), - 'DESCRIPTION': (0, 1, None), - 'GEO': (0, 1, None), - 'LAST-MODIFIED':(0, 1, None), - 'LOCATION': (0, 1, None), - 'ORGANIZER': (0, 1, None), - 'PERCENT': (0, 1, None), - 'PRIORITY': (0, 1, None), - 'DTSTAMP': (0, 1, None), - 'SEQUENCE': (0, 1, None), - 'STATUS': (0, 1, None), - 'SUMMARY': (0, 1, None), - 'UID': (0, 1, None), - 'URL': (0, 1, None), - 'RECURRENCE-ID':(0, 1, None), - 'DUE': (0, 1, None), #NOTE: Only one of Due or - 'DURATION': (0, 1, None), # Duration can appear - 'ATTACH': (0, None, None), - 'ATTENDEE': (0, None, None), - 'CATEGORIES': (0, None, None), - 'COMMENT': (0, None, None), - 'CONTACT': (0, None, None), - 'EXDATE': (0, None, None), - 'EXRULE': (0, None, None), - 'REQUEST-STATUS': (0, None, None), - 'RELATED-TO': (0, None, None), - 'RESOURCES': (0, None, None), - 'RDATE': (0, None, None), - 'RRULE': (0, None, None), - 'VALARM': (0, None, None) - } - - def validate(cls, obj, raiseException, *args): - if 'DUE' in obj.contents and 'DURATION' in obj.contents: - if raiseException: - m = "VTODO components cannot contain both DUE and DURATION\ - components" - raise ValidateError(m) - return False - else: - return super(VTodo, cls).validate(obj, raiseException, *args) - validate = classmethod(validate) - -registerBehavior(VTodo) - - -class VJournal(RecurringBehavior): - """Journal entry behavior.""" - name='VJOURNAL' - knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id - 'CLASS': (0, 1, None), - 'CREATED': (0, 1, None), - 'DESCRIPTION': (0, 1, None), - 'LAST-MODIFIED':(0, 1, None), - 'ORGANIZER': (0, 1, None), - 'DTSTAMP': (0, 1, None), - 'SEQUENCE': (0, 1, None), - 'STATUS': (0, 1, None), - 'SUMMARY': (0, 1, None), - 'UID': (0, 1, None), - 'URL': (0, 1, None), - 'RECURRENCE-ID':(0, 1, None), - 'ATTACH': (0, None, None), - 'ATTENDEE': (0, None, None), - 'CATEGORIES': (0, None, None), - 'COMMENT': (0, None, None), - 'CONTACT': (0, None, None), - 'EXDATE': (0, None, None), - 'EXRULE': (0, None, None), - 'REQUEST-STATUS': (0, None, None), - 'RELATED-TO': (0, None, None), - 'RDATE': (0, None, None), - 'RRULE': (0, None, None) - } -registerBehavior(VJournal) - - -class VFreeBusy(behavior.Behavior): - """Free/busy state behavior. - - >>> vfb = newFromBehavior('VFREEBUSY') - >>> vfb.add('uid').value = 'test' - >>> vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc) - >>> vfb.add('dtend').value = vfb.dtstart.value + twoHours - >>> vfb.add('freebusy').value = [(vfb.dtstart.value, twoHours / 2)] - >>> print vfb.serialize() - BEGIN:VFREEBUSY - UID:test - DTSTART:20060216T010000Z - DTEND:20060216T030000Z - FREEBUSY:20060216T010000Z/PT1H - END:VFREEBUSY - - """ - name='VFREEBUSY' - isComponent = True - description='A grouping of component properties that describe either a \ - request for free/busy time, describe a response to a request \ - for free/busy time or describe a published set of busy time.' - sortFirst = ('uid', 'dtstart', 'duration', 'dtend') - knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id - 'CONTACT': (0, 1, None), - 'DTEND': (0, 1, None), - 'DURATION': (0, 1, None), - 'ORGANIZER': (0, 1, None), - 'DTSTAMP': (0, 1, None), - 'UID': (0, 1, None), - 'URL': (0, 1, None), - 'ATTENDEE': (0, None, None), - 'COMMENT': (0, None, None), - 'FREEBUSY': (0, None, None), - 'REQUEST-STATUS': (0, None, None) - } -registerBehavior(VFreeBusy) - - -class VAlarm(behavior.Behavior): - """Alarm behavior.""" - name='VALARM' - isComponent = True - description='Alarms describe when and how to provide alerts about events \ - and to-dos.' - knownChildren = {'ACTION': (1, 1, None),#min, max, behaviorRegistry id - 'TRIGGER': (1, 1, None), - 'DURATION': (0, 1, None), - 'REPEAT': (0, 1, None), - 'DESCRIPTION': (0, 1, None) - } - - def generateImplicitParameters(obj): - """Create default ACTION and TRIGGER if they're not set.""" - try: - obj.action - except AttributeError: - obj.add('action').value = 'AUDIO' - try: - obj.trigger - except AttributeError: - obj.add('trigger').value = datetime.timedelta(0) - generateImplicitParameters = staticmethod(generateImplicitParameters) - - - def validate(cls, obj, raiseException, *args): - """ - #TODO - audioprop = 2*( - - ; 'action' and 'trigger' are both REQUIRED, - ; but MUST NOT occur more than once - - action / trigger / - - ; 'duration' and 'repeat' are both optional, - ; and MUST NOT occur more than once each, - ; but if one occurs, so MUST the other - - duration / repeat / - - ; the following is optional, - ; but MUST NOT occur more than once - - attach / - - dispprop = 3*( - - ; the following are all REQUIRED, - ; but MUST NOT occur more than once - - action / description / trigger / - - ; 'duration' and 'repeat' are both optional, - ; and MUST NOT occur more than once each, - ; but if one occurs, so MUST the other - - duration / repeat / - - emailprop = 5*( - - ; the following are all REQUIRED, - ; but MUST NOT occur more than once - - action / description / trigger / summary - - ; the following is REQUIRED, - ; and MAY occur more than once - - attendee / - - ; 'duration' and 'repeat' are both optional, - ; and MUST NOT occur more than once each, - ; but if one occurs, so MUST the other - - duration / repeat / - - procprop = 3*( - - ; the following are all REQUIRED, - ; but MUST NOT occur more than once - - action / attach / trigger / - - ; 'duration' and 'repeat' are both optional, - ; and MUST NOT occur more than once each, - ; but if one occurs, so MUST the other - - duration / repeat / - - ; 'description' is optional, - ; and MUST NOT occur more than once - - description / - if obj.contents.has_key('DTEND') and obj.contents.has_key('DURATION'): - if raiseException: - m = "VEVENT components cannot contain both DTEND and DURATION\ - components" - raise ValidateError(m) - return False - else: - return super(VEvent, cls).validate(obj, raiseException, *args) - """ - return True - validate = classmethod(validate) - -registerBehavior(VAlarm) - -class Duration(behavior.Behavior): - """Behavior for Duration ContentLines. Transform to datetime.timedelta.""" - name = 'DURATION' - hasNative = True - - def transformToNative(obj): - """Turn obj.value into a datetime.timedelta.""" - if obj.isNative: return obj - obj.isNative = True - obj.value=str(obj.value) - if obj.value == '': - return obj - else: - deltalist=stringToDurations(obj.value) - #When can DURATION have multiple durations? For now: - if len(deltalist) == 1: - obj.value = deltalist[0] - return obj - else: - raise ParseError("DURATION must have a single duration string.") - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - """Replace the datetime.timedelta in obj.value with an RFC2445 string. - """ - if not obj.isNative: return obj - obj.isNative = False - obj.value = timedeltaToString(obj.value) - return obj - transformFromNative = staticmethod(transformFromNative) - -registerBehavior(Duration) - -class Trigger(behavior.Behavior): - """DATE-TIME or DURATION""" - name='TRIGGER' - description='This property specifies when an alarm will trigger.' - hasNative = True - - def transformToNative(obj): - """Turn obj.value into a timedelta or datetime.""" - value = getattr(obj, 'value_param', 'DURATION').upper() - if hasattr(obj, 'value_param'): - del obj.value_param - if obj.value == '': - obj.isNative = True - return obj - elif value == 'DURATION': - return Duration.transformToNative(obj) - elif value == 'DATE-TIME': - #TRIGGERs with DATE-TIME values must be in UTC, we could validate - #that fact, for now we take it on faith. - return DateTimeBehavior.transformToNative(obj) - else: - raise ParseError("VALUE must be DURATION or DATE-TIME") - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - if type(obj.value) == datetime.datetime: - obj.value_param = 'DATE-TIME' - return DateTimeBehavior.transformFromNative(obj) - elif type(obj.value) == datetime.timedelta: - return Duration.transformFromNative(obj) - else: - raise NativeError("Native TRIGGER values must be timedelta or datetime") - transformFromNative = staticmethod(transformFromNative) - -registerBehavior(Trigger) - -class PeriodBehavior(behavior.Behavior): - """A list of (date-time, timedelta) tuples. - - >>> line = ContentLine('test', [], '', isNative=True) - >>> line.behavior = PeriodBehavior - >>> line.value = [(datetime.datetime(2006, 2, 16, 10), twoHours)] - >>> line.transformFromNative().value - '20060216T100000/PT2H' - >>> line.transformToNative().value - [(datetime.datetime(2006, 2, 16, 10, 0), datetime.timedelta(0, 7200))] - >>> line.value.append((datetime.datetime(2006, 5, 16, 10), twoHours)) - >>> print line.serialize().strip() - TEST:20060216T100000/PT2H,20060516T100000/PT2H - """ - hasNative = True - - def transformToNative(obj): - """Convert comma separated periods into tuples.""" - if obj.isNative: - return obj - obj.isNative = True - if obj.value == '': - obj.value = [] - return obj - tzinfo = getTzid(getattr(obj, 'tzid_param', None)) - obj.value = [stringToPeriod(x, tzinfo) for x in obj.value.split(",")] - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj, convertToUTC = False): - """Convert the list of tuples in obj.value to strings.""" - if obj.isNative: - obj.isNative = False - transformed = [] - for tup in obj.value: - transformed.append(periodToString(tup, convertToUTC)) - if len(transformed) > 0: - tzid = TimezoneComponent.registerTzinfo(tup[0].tzinfo) - if not convertToUTC and tzid is not None: - obj.tzid_param = tzid - - obj.value = ','.join(transformed) - - return obj - transformFromNative = staticmethod(transformFromNative) - -class FreeBusy(PeriodBehavior): - """Free or busy period of time, must be specified in UTC.""" - name = 'FREEBUSY' - - def transformFromNative(obj): - """Pass convertToUTC = True to parent method.""" - return PeriodBehavior.transformFromNative(obj, True) - transformFromNative = staticmethod(transformFromNative) -registerBehavior(FreeBusy) - -#------------------------ Registration of common classes ----------------------- - -utcDateTimeList = ['LAST-MODIFIED', 'CREATED', 'COMPLETED', 'DTSTAMP'] -list(map(lambda x: registerBehavior(UTCDateTimeBehavior, x), utcDateTimeList)) - -dateTimeOrDateList = ['DTEND', 'DTSTART', 'DUE', 'RECURRENCE-ID'] -list(map(lambda x: registerBehavior(DateOrDateTimeBehavior, x), - dateTimeOrDateList)) - -registerBehavior(MultiDateBehavior, 'RDATE') -registerBehavior(MultiDateBehavior, 'EXDATE') - - -textList = ['CALSCALE', 'METHOD', 'PRODID', 'CLASS', 'COMMENT', 'DESCRIPTION', - 'LOCATION', 'STATUS', 'SUMMARY', 'TRANSP', 'CONTACT', 'RELATED-TO', - 'UID', 'ACTION', 'REQUEST-STATUS', 'TZID'] -list(map(lambda x: registerBehavior(TextBehavior, x), textList)) - -multiTextList = ['CATEGORIES', 'RESOURCES'] -list(map(lambda x: registerBehavior(MultiTextBehavior, x), multiTextList)) - -#------------------------ Serializing helper functions ------------------------- - -def numToDigits(num, places): - """Helper, for converting numbers to textual digits.""" - s = str(num) - if len(s) < places: - return ("0" * (places - len(s))) + s - elif len(s) > places: - return s[len(s)-places: ] - else: - return s - -def timedeltaToString(delta): - """Convert timedelta to an rfc2445 DURATION.""" - if delta.days == 0: sign = 1 - else: sign = delta.days / abs(delta.days) - delta = abs(delta) - days = delta.days - hours = delta.seconds / 3600 - minutes = (delta.seconds % 3600) / 60 - seconds = delta.seconds % 60 - out = '' - if sign == -1: out = '-' - out += 'P' - if days: out += str(days) + 'D' - if hours or minutes or seconds: out += 'T' - elif not days: #Deal with zero duration - out += '0S' - if hours: out += str(hours) + 'H' - if minutes: out += str(minutes) + 'M' - if seconds: out += str(seconds) + 'S' - return out - -def dateToString(date): - year = numToDigits( date.year, 4 ) - month = numToDigits( date.month, 2 ) - day = numToDigits( date.day, 2 ) - return year + month + day - -def dateTimeToString(dateTime, convertToUTC=False): - """Ignore tzinfo unless convertToUTC. Output string.""" - if dateTime.tzinfo and convertToUTC: - dateTime = dateTime.astimezone(utc) - if tzinfo_eq(dateTime.tzinfo, utc): utcString = "Z" - else: utcString = "" - - year = numToDigits( dateTime.year, 4 ) - month = numToDigits( dateTime.month, 2 ) - day = numToDigits( dateTime.day, 2 ) - hour = numToDigits( dateTime.hour, 2 ) - mins = numToDigits( dateTime.minute, 2 ) - secs = numToDigits( dateTime.second, 2 ) - - return year + month + day + "T" + hour + mins + secs + utcString - -def deltaToOffset(delta): - absDelta = abs(delta) - hours = absDelta.seconds / 3600 - hoursString = numToDigits(hours, 2) - minutesString = '00' - if absDelta == delta: - signString = "+" - else: - signString = "-" - return signString + hoursString + minutesString - -def periodToString(period, convertToUTC=False): - txtstart = dateTimeToString(period[0], convertToUTC) - if isinstance(period[1], datetime.timedelta): - txtend = timedeltaToString(period[1]) - else: - txtend = dateTimeToString(period[1], convertToUTC) - return txtstart + "/" + txtend - -#----------------------- Parsing functions ------------------------------------- - -def isDuration(s): - s = string.upper(s) - return (string.find(s, "P") != -1) and (string.find(s, "P") < 2) - -def stringToDate(s, tzinfos=None): - if tzinfos != None: print("Didn't expect a tzinfos here") - year = int( s[0:4] ) - month = int( s[4:6] ) - day = int( s[6:8] ) - return datetime.date(year,month,day) - -def stringToDateTime(s, tzinfo=None): - """Returns datetime.datetime object.""" - try: - year = int( s[0:4] ) - month = int( s[4:6] ) - day = int( s[6:8] ) - hour = int( s[9:11] ) - minute = int( s[11:13] ) - second = int( s[13:15] ) - if len(s) > 15: - if s[15] == 'Z': - tzinfo = utc - except: - raise ParseError("'%s' is not a valid DATE-TIME" % s) - return datetime.datetime(year, month, day, hour, minute, second, 0, tzinfo) - - -escapableCharList = "\\;,Nn" - -def stringToTextValues(s, strict=False): - """Returns list of strings.""" - - def escapableChar (c): - return c in escapableCharList - - def error(msg): - if strict: - raise ParseError(msg) - else: - #logger.error(msg) - print(msg) - - #vars which control state machine - charIterator = enumerate(s) - state = "read normal" - - current = "" - results = [] - - while True: - try: - charIndex, char = next(charIterator) - except: - char = "eof" - - if state == "read normal": - if char == '\\': - state = "read escaped char" - elif char == ',': - state = "read normal" - results.append(current) - current = "" - elif char == "eof": - state = "end" - else: - state = "read normal" - current = current + char - - elif state == "read escaped char": - if escapableChar(char): - state = "read normal" - if char in 'nN': - current = current + '\n' - else: - current = current + char - else: - state = "read normal" - current = current + char #this is an error, but whatever - - elif state == "end": #an end state - if current != "" or len(results) == 0: - results.append(current) - return results - - elif state == "error": #an end state - return results - - else: - state = "error" - error("error: unknown state: '%s' reached in %s" % (state, s)) - -def stringToDurations(s, strict=False): - """Returns list of timedelta objects.""" - def makeTimedelta(sign, week, day, hour, minute, sec): - if sign == "-": sign = -1 - else: sign = 1 - week = int(week) - day = int(day) - hour = int(hour) - minute = int(minute) - sec = int(sec) - return sign * datetime.timedelta(weeks=week, days=day, hours=hour, minutes=minute, seconds=sec) - - def error(msg): - if strict: - raise ParseError(msg) - else: - raise ParseError(msg) - #logger.error(msg) - - #vars which control state machine - charIterator = enumerate(s) - state = "start" - - durations = [] - current = "" - sign = None - week = 0 - day = 0 - hour = 0 - minute = 0 - sec = 0 - - while True: - try: - charIndex, char = next(charIterator) - except: - charIndex += 1 - char = "eof" - - if state == "start": - if char == '+': - state = "start" - sign = char - elif char == '-': - state = "start" - sign = char - elif char.upper() == 'P': - state = "read field" - elif char == "eof": - state = "error" - error("got end-of-line while reading in duration: " + s) - elif char in string.digits: - state = "read field" - current = current + char #update this part when updating "read field" - else: - state = "error" - print("got unexpected character %s reading in duration: %s" % (char, s)) - error("got unexpected character %s reading in duration: %s" % (char, s)) - - elif state == "read field": - if (char in string.digits): - state = "read field" - current = current + char #update part above when updating "read field" - elif char.upper() == 'T': - state = "read field" - elif char.upper() == 'W': - state = "read field" - week = current - current = "" - elif char.upper() == 'D': - state = "read field" - day = current - current = "" - elif char.upper() == 'H': - state = "read field" - hour = current - current = "" - elif char.upper() == 'M': - state = "read field" - minute = current - current = "" - elif char.upper() == 'S': - state = "read field" - sec = current - current = "" - elif char == ",": - state = "start" - durations.append( makeTimedelta(sign, week, day, hour, minute, sec) ) - current = "" - sign = None - week = None - day = None - hour = None - minute = None - sec = None - elif char == "eof": - state = "end" - else: - state = "error" - error("got unexpected character reading in duration: " + s) - - elif state == "end": #an end state - #print "stuff: %s, durations: %s" % ([current, sign, week, day, hour, minute, sec], durations) - - if (sign or week or day or hour or minute or sec): - durations.append( makeTimedelta(sign, week, day, hour, minute, sec) ) - return durations - - elif state == "error": #an end state - error("in error state") - return durations - - else: - state = "error" - error("error: unknown state: '%s' reached in %s" % (state, s)) - -def parseDtstart(contentline): - tzinfo = getTzid(getattr(contentline, 'tzid_param', None)) - valueParam = getattr(contentline, 'value_param', 'DATE-TIME').upper() - if valueParam == "DATE": - return stringToDate(contentline.value) - elif valueParam == "DATE-TIME": - return stringToDateTime(contentline.value, tzinfo) - -def stringToPeriod(s, tzinfo=None): - values = string.split(s, "/") - start = stringToDateTime(values[0], tzinfo) - valEnd = values[1] - if isDuration(valEnd): #period-start = date-time "/" dur-value - delta = stringToDurations(valEnd)[0] - return (start, delta) - else: - return (start, stringToDateTime(valEnd, tzinfo) - start) - - -def getTransition(transitionTo, year, tzinfo): - """Return the datetime of the transition to/from DST, or None.""" - - def firstTransition(iterDates, test): - """ - Return the last date not matching test, or None if all tests matched. - """ - success = None - for dt in iterDates: - if not test(dt): - success = dt - else: - if success is not None: - return success - return success # may be None - - def generateDates(year, month=None, day=None): - """Iterate over possible dates with unspecified values.""" - months = list(range(1, 13)) - days = list(range(1, 32)) - hours = list(range(0, 24)) - if month is None: - for month in months: - yield datetime.datetime(year, month, 1) - elif day is None: - for day in days: - try: - yield datetime.datetime(year, month, day) - except ValueError: - pass - else: - for hour in hours: - yield datetime.datetime(year, month, day, hour) - - assert transitionTo in ('daylight', 'standard') - if transitionTo == 'daylight': - def test(dt): return tzinfo.dst(dt) != zeroDelta - elif transitionTo == 'standard': - def test(dt): return tzinfo.dst(dt) == zeroDelta - newyear = datetime.datetime(year, 1, 1) - monthDt = firstTransition(generateDates(year), test) - if monthDt is None: - return newyear - elif monthDt.month == 12: - return None - else: - # there was a good transition somewhere in a non-December month - month = monthDt.month - day = firstTransition(generateDates(year, month), test).day - uncorrected = firstTransition(generateDates(year, month, day), test) - if transitionTo == 'standard': - # assuming tzinfo.dst returns a new offset for the first - # possible hour, we need to add one hour for the offset change - # and another hour because firstTransition returns the hour - # before the transition - return uncorrected + datetime.timedelta(hours=2) - else: - return uncorrected + datetime.timedelta(hours=1) - -def tzinfo_eq(tzinfo1, tzinfo2, startYear = 2000, endYear=2020): - """Compare offsets and DST transitions from startYear to endYear.""" - if tzinfo1 == tzinfo2: - return True - elif tzinfo1 is None or tzinfo2 is None: - return False - - def dt_test(dt): - if dt is None: - return True - return tzinfo1.utcoffset(dt) == tzinfo2.utcoffset(dt) - - if not dt_test(datetime.datetime(startYear, 1, 1)): - return False - for year in range(startYear, endYear): - for transitionTo in 'daylight', 'standard': - t1=getTransition(transitionTo, year, tzinfo1) - t2=getTransition(transitionTo, year, tzinfo2) - if t1 != t2 or not dt_test(t1): - return False - return True - - -#------------------- Testing and running functions ----------------------------- -if __name__ == '__main__': - from . import tests - tests._test() diff --git a/src/libs/python/bongo/external/vobject/tests.py b/src/libs/python/bongo/external/vobject/tests.py deleted file mode 100644 index 84920df..0000000 --- a/src/libs/python/bongo/external/vobject/tests.py +++ /dev/null @@ -1,601 +0,0 @@ -"""Long or boring tests for vobjects.""" - -import base, behavior, io, icalendar, vcard, re, dateutil.tz, datetime - -# setuptools is required to run the unit tests -from pkg_resources import resource_stream - -#------------------- Testing and running functions ----------------------------- -def _test(): - import doctest, base, tests, icalendar, __init__, re - flags = doctest.NORMALIZE_WHITESPACE | doctest.REPORT_UDIFF \ - | doctest.REPORT_ONLY_FIRST_FAILURE - for mod in base, tests, icalendar, __init__: - doctest.testmod(mod, verbose=0, optionflags=flags) - try: - doctest.testfile('../../README.txt', optionflags=flags) - except IOError: #allow this test to fail if we can't find README.txt - pass - - -if __name__ == '__main__': - _test() - - -testSilly=""" -sillyname:name -profile:sillyprofile -stuff:folded - line -""" + "morestuff;asinine:this line is not folded, \ -but in practice probably ought to be, as it is exceptionally long, \ -and moreover demonstratively stupid" - -icaltest=r"""BEGIN:VCALENDAR -CALSCALE:GREGORIAN -X-WR-TIMEZONE;VALUE=TEXT:US/Pacific -METHOD:PUBLISH -PRODID:-//Apple Computer\, Inc//iCal 1.0//EN -X-WR-CALNAME;VALUE=TEXT:Example -VERSION:2.0 -BEGIN:VEVENT -SEQUENCE:5 -DTSTART;TZID=US/Pacific:20021028T140000 -RRULE:FREQ=Weekly;COUNT=10 -DTSTAMP:20021028T011706Z -SUMMARY:Coffee with Jason -UID:EC9439B1-FF65-11D6-9973-003065F99D04 -DTEND;TZID=US/Pacific:20021028T150000 -BEGIN:VALARM -TRIGGER;VALUE=DURATION:-P1D -ACTION:DISPLAY -DESCRIPTION:Event reminder\, with comma\nand line feed -END:VALARM -END:VEVENT -BEGIN:VTIMEZONE -X-LIC-LOCATION:Random location -TZID:US/Pacific -LAST-MODIFIED:19870101T000000Z -BEGIN:STANDARD -DTSTART:19671029T020000 -RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 -TZOFFSETFROM:-0700 -TZOFFSETTO:-0800 -TZNAME:PST -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19870405T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 -TZOFFSETFROM:-0800 -TZOFFSETTO:-0700 -TZNAME:PDT -END:DAYLIGHT -END:VTIMEZONE -END:VCALENDAR""" - -vcardtest = """BEGIN:VCARD -VERSION:3.0 -FN:Daffy Duck Knudson (with Bugs Bunny and Mr. Pluto) -N:Knudson;Daffy Duck (with Bugs Bunny and Mr. Pluto) -NICKNAME:gnat and gnu and pluto -BDAY;value=date:02-10;11-05;01-01 -TEL;type=HOME:+01-(0)2-765.43.21 -TEL;type=CELL:+01-(0)5-555.55.55 -ACCOUNT;type=HOME:010-1234567-05 -ADR;type=HOME:;;Haight Street 512;Novosibirsk;;80214;Gnuland -TEL;type=HOME:+01-(0)2-876.54.32 -ORG:University of Novosibirsk, Department of Octopus - Parthenogenesis -END:VCARD""" - -vcardWithGroups = r"""home.begin:vcard -version:3.0 -source:ldap://cn=Meister%20Berger,o=Universitaet%20Goerlitz,c=DE -name:Meister Berger -fn:Meister Berger -n:Berger;Meister -bday;value=date:1963-09-21 -o:Universit=E6t G=F6rlitz -title:Mayor -title;language=de;value=text:Burgermeister -note:The Mayor of the great city of - Goerlitz in the great country of Germany. -email;internet:mb@goerlitz.de -home.tel;type=fax,voice;type=msg:+49 3581 123456 -home.label:Hufenshlagel 1234\n - 02828 Goerlitz\n - Deutschland -END:VCARD""" - -lowercaseComponentNames = r"""begin:vcard -fn:Anders Bobo -n:Bobo;Anders -org:Bobo A/S;Vice President, Technical Support -adr:Rockfeller Center;;Mekastreet;Bobocity;;2100;Myworld -email;internet:bobo@example.com -tel;work:+123455 -tel;fax:+123456 -tel;cell:+123457 -x-mozilla-html:FALSE -url:http://www.example.com -version:2.1 -end:vcard""" - -badstream = r"""BEGIN:VCALENDAR -CALSCALE:GREGORIAN -X-WR-TIMEZONE;VALUE=TEXT:US/Pacific -METHOD:PUBLISH -PRODID:-//Apple Computer\, Inc//iCal 1.0//EN -X-WR-CALNAME;VALUE=TEXT:Example -VERSION:2.0 -BEGIN:VEVENT -DTSTART:20021028T140000Z -BEGIN:VALARM -TRIGGER:20021028T120000Z -ACTION:DISPLAY -DESCRIPTION:This trigger is a date-time without a VALUE=DATE-TIME parameter -END:VALARM -END:VEVENT -END:VCALENDAR""" - -timezones = r""" - -BEGIN:VTIMEZONE -TZID:US/Pacific -BEGIN:STANDARD -DTSTART:19671029T020000 -RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 -TZOFFSETFROM:-0700 -TZOFFSETTO:-0800 -TZNAME:PST -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19870405T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 -TZOFFSETFROM:-0800 -TZOFFSETTO:-0700 -TZNAME:PDT -END:DAYLIGHT -END:VTIMEZONE - -BEGIN:VTIMEZONE -TZID:US/Eastern -BEGIN:STANDARD -DTSTART:19671029T020000 -RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 -TZOFFSETFROM:-0400 -TZOFFSETTO:-0500 -TZNAME:EST -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19870405T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 -TZOFFSETFROM:-0500 -TZOFFSETTO:-0400 -TZNAME:EDT -END:DAYLIGHT -END:VTIMEZONE - -BEGIN:VTIMEZONE -TZID:Santiago -BEGIN:STANDARD -DTSTART:19700314T000000 -TZOFFSETFROM:-0300 -TZOFFSETTO:-0400 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SA -TZNAME:Pacific SA Standard Time -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19701010T000000 -TZOFFSETFROM:-0400 -TZOFFSETTO:-0300 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=2SA -TZNAME:Pacific SA Daylight Time -END:DAYLIGHT -END:VTIMEZONE - -BEGIN:VTIMEZONE -TZID:W. Europe -BEGIN:STANDARD -DTSTART:19701025T030000 -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU -TZNAME:W. Europe Standard Time -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19700329T020000 -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU -TZNAME:W. Europe Daylight Time -END:DAYLIGHT -END:VTIMEZONE - -BEGIN:VTIMEZONE -TZID:US/Fictitious-Eastern -LAST-MODIFIED:19870101T000000Z -BEGIN:STANDARD -DTSTART:19671029T020000 -RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 -TZOFFSETFROM:-0400 -TZOFFSETTO:-0500 -TZNAME:EST -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19870405T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T070000Z -TZOFFSETFROM:-0500 -TZOFFSETTO:-0400 -TZNAME:EDT -END:DAYLIGHT -END:VTIMEZONE - -BEGIN:VTIMEZONE -TZID:America/Montreal -LAST-MODIFIED:20051013T233643Z -BEGIN:DAYLIGHT -DTSTART:20050403T070000 -TZOFFSETTO:-0400 -TZOFFSETFROM:+0000 -TZNAME:EDT -END:DAYLIGHT -BEGIN:STANDARD -DTSTART:20051030T020000 -TZOFFSETTO:-0500 -TZOFFSETFROM:-0400 -TZNAME:EST -END:STANDARD -END:VTIMEZONE - -""" - -__test__ = { "Test readOne" : - r""" - >>> silly = base.readOne(testSilly) - >>> silly - , , ]> - >>> silly.stuff - - >>> original = silly.serialize() - >>> f3 = StringIO.StringIO(original) - >>> silly2 = base.readOne(f3) - >>> silly2.serialize()==original - True - >>> s3 = StringIO.StringIO('cn:Babs Jensen\r\ncn:Barbara J Jensen\r\nsn:Jensen\r\nemail:babs@umich.edu\r\nphone:+1 313 747-4454\r\nx-id:1234567890\r\n') - >>> ex1 = base.readOne(s3) - >>> ex1 - <*unnamed*| [, , , , , ]> - >>> ex1.serialize() - u'CN:Babs Jensen\r\nCN:Barbara J Jensen\r\nEMAIL:babs@umich.edu\r\nPHONE:+1 313 747-4454\r\nSN:Jensen\r\nX-ID:1234567890\r\n' - """, - - "Import icaltest" : - r""" - >>> c = base.readOne(icaltest, validate=True) - >>> c.vevent.valarm.trigger - - >>> c.vevent.dtstart.value - datetime.datetime(2002, 10, 28, 14, 0, tzinfo=) - >>> c.vevent.dtend.value - datetime.datetime(2002, 10, 28, 15, 0, tzinfo=) - >>> c.vevent.dtstamp.value - datetime.datetime(2002, 10, 28, 1, 17, 6, tzinfo=tzutc()) - >>> c.vevent.valarm.description.value - u'Event reminder, with comma\nand line feed' - >>> c.vevent.valarm.description.serialize() - u'DESCRIPTION:Event reminder\\, with comma\\nand line feed\r\n' - >>> vevent = c.vevent.transformFromNative() - >>> vevent.rrule - - """, - - "Parsing tests" : - """ - >>> parseRDate = icalendar.MultiDateBehavior.transformToNative - >>> icalendar.stringToTextValues('') - [''] - >>> icalendar.stringToTextValues('abcd,efgh') - ['abcd', 'efgh'] - >>> icalendar.stringToPeriod("19970101T180000Z/19970102T070000Z") - (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), datetime.timedelta(0, 46800)) - >>> parseRDate(base.textLineToContentLine("RDATE;VALUE=DATE:19970304,19970504,19970704,19970904")) - - >>> parseRDate(base.textLineToContentLine("RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H")) - - """, - - "read failure" : - """ - >>> f = StringIO.StringIO(badstream) - >>> vevent = base.readOne(f) - Traceback (most recent call last): - ... - ParseError: At line 11: got unexpected character reading in duration: 20021028T120000Z - """, - - "unicode test" : - r""" - >>> f = resource_stream(__name__, 'utf8_test.ics') - >>> vevent = base.readOne(f).vevent - >>> vevent.summary.value - u'The title \u3053\u3093\u306b\u3061\u306f\u30ad\u30c6\u30a3' - >>> summary = vevent.summary.value - """, - - "regular expression test" : - """ - >>> re.findall(base.patterns['name'], '12foo-bar:yay') - ['12foo-bar', 'yay'] - >>> re.findall(base.patterns['safe_char'], 'a;b"*,cd') - ['a', 'b', '*', 'c', 'd'] - >>> re.findall(base.patterns['qsafe_char'], 'a;b"*,cd') - ['a', ';', 'b', '*', ',', 'c', 'd'] - >>> re.findall(base.patterns['param_value'], '"quoted";not-quoted;start"after-illegal-quote', re.VERBOSE) - ['"quoted"', '', 'not-quoted', '', 'start', '', 'after-illegal-quote', ''] - >>> match = base.line_re.match('TEST;ALTREP="http://www.wiz.org":value:;"') - >>> match.group('value') - 'value:;"' - >>> match.group('name') - 'TEST' - >>> match.group('params') - ';ALTREP="http://www.wiz.org"' - """, - - "VTIMEZONE creation test:" : - - """ - >>> f = StringIO.StringIO(timezones) - >>> tzs = dateutil.tz.tzical(f) - >>> tzs.get("US/Pacific") - - >>> icalendar.TimezoneComponent(_) - > - >>> pacific = _ - >>> print pacific.serialize() - BEGIN:VTIMEZONE - TZID:US/Pacific - BEGIN:STANDARD - DTSTART:20001029T020000 - RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 - TZNAME:PST - TZOFFSETFROM:-0700 - TZOFFSETTO:-0800 - END:STANDARD - BEGIN:DAYLIGHT - DTSTART:20000402T020000 - RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 - TZNAME:PDT - TZOFFSETFROM:-0800 - TZOFFSETTO:-0700 - END:DAYLIGHT - END:VTIMEZONE - >>> (_) - > - >>> santiago = icalendar.TimezoneComponent(tzs.get('Santiago')) - >>> ser = santiago.serialize() - >>> print ser - BEGIN:VTIMEZONE - TZID:Santiago - BEGIN:STANDARD - DTSTART:20000311T000000 - RRULE:FREQ=YEARLY;BYDAY=2SA;BYMONTH=3 - TZNAME:Pacific SA Standard Time - TZOFFSETFROM:-0300 - TZOFFSETTO:-0400 - END:STANDARD - BEGIN:DAYLIGHT - DTSTART:20001014T000000 - RRULE:FREQ=YEARLY;BYDAY=2SA;BYMONTH=10 - TZNAME:Pacific SA Daylight Time - TZOFFSETFROM:-0400 - TZOFFSETTO:-0300 - END:DAYLIGHT - END:VTIMEZONE - >>> roundtrip = dateutil.tz.tzical(StringIO.StringIO(str(ser))).get() - >>> for year in range(2001, 2010): - ... for month in (2, 9): - ... dt = datetime.datetime(year, month, 15, tzinfo = roundtrip) - ... if dt.replace(tzinfo=tzs.get('Santiago')) != dt: - ... print "Failed for:", dt - >>> fict = icalendar.TimezoneComponent(tzs.get('US/Fictitious-Eastern')) - >>> print fict.serialize() - BEGIN:VTIMEZONE - TZID:US/Fictitious-Eastern - BEGIN:STANDARD - DTSTART:20001029T020000 - RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 - TZNAME:EST - TZOFFSETFROM:-0400 - TZOFFSETTO:-0500 - END:STANDARD - BEGIN:DAYLIGHT - DTSTART:20000402T020000 - RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T070000Z - TZNAME:EDT - TZOFFSETFROM:-0500 - TZOFFSETTO:-0400 - END:DAYLIGHT - END:VTIMEZONE - """, - - "Serializing with timezones test" : - - """ - >>> from dateutil.rrule import rrule, rruleset, WEEKLY - >>> pacific = dateutil.tz.tzical(StringIO.StringIO(timezones)).get('US/Pacific') - >>> cal = base.Component('VCALENDAR') - >>> cal.setBehavior(icalendar.VCalendar2_0) - >>> ev = cal.add('vevent') - >>> ev.add('dtstart').value = datetime.datetime(2005, 10, 12, 9, tzinfo = pacific) - >>> set = rruleset() - >>> set.rrule(rrule(WEEKLY, interval=2, byweekday=[2,4], until=datetime.datetime(2005, 12, 15, 9))) - >>> set.exdate(datetime.datetime(2005, 10, 14, 9, tzinfo = pacific)) - >>> ev.rruleset = set - >>> ev.add('uid').value = "uid could be generated but doctest complains" - >>> ev.add('duration').value = datetime.timedelta(hours=1) - >>> print cal.serialize() - BEGIN:VCALENDAR - VERSION:2.0 - PRODID:-//PYVOBJECT//NONSGML Version 1//EN - BEGIN:VTIMEZONE - TZID:US/Pacific - BEGIN:STANDARD - DTSTART:20001029T020000 - RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 - TZNAME:PST - TZOFFSETFROM:-0700 - TZOFFSETTO:-0800 - END:STANDARD - BEGIN:DAYLIGHT - DTSTART:20000402T020000 - RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 - TZNAME:PDT - TZOFFSETFROM:-0800 - TZOFFSETTO:-0700 - END:DAYLIGHT - END:VTIMEZONE - BEGIN:VEVENT - UID:uid could be generated but doctest complains - DTSTART;TZID=US/Pacific:20051012T090000 - DURATION:PT1H - EXDATE;TZID=US/Pacific:20051014T090000 - RRULE:FREQ=WEEKLY;BYDAY=WE,FR;INTERVAL=2;UNTIL=20051215T090000 - END:VEVENT - END:VCALENDAR - >>> apple = dateutil.tz.tzical(StringIO.StringIO(timezones)).get('America/Montreal') - >>> ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo = apple) - >>> print cal.serialize() - BEGIN:VCALENDAR - VERSION:2.0 - PRODID:-//PYVOBJECT//NONSGML Version 1//EN - BEGIN:VTIMEZONE - TZID:US/Pacific - BEGIN:STANDARD - DTSTART:20001029T020000 - RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 - TZNAME:PST - TZOFFSETFROM:-0700 - TZOFFSETTO:-0800 - END:STANDARD - BEGIN:DAYLIGHT - DTSTART:20000402T020000 - RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 - TZNAME:PDT - TZOFFSETFROM:-0800 - TZOFFSETTO:-0700 - END:DAYLIGHT - END:VTIMEZONE - BEGIN:VTIMEZONE - TZID:America/Montreal - BEGIN:STANDARD - DTSTART:20000101T000000 - RRULE:FREQ=YEARLY;BYMONTH=1;UNTIL=20040101T050000Z - TZNAME:EST - TZOFFSETFROM:-0500 - TZOFFSETTO:-0500 - END:STANDARD - BEGIN:STANDARD - DTSTART:20051030T020000 - RRULE:FREQ=YEARLY;BYDAY=5SU;BYMONTH=10 - TZNAME:EST - TZOFFSETFROM:-0400 - TZOFFSETTO:-0500 - END:STANDARD - BEGIN:DAYLIGHT - DTSTART:20050403T070000 - RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T120000Z - TZNAME:EDT - TZOFFSETFROM:-0500 - TZOFFSETTO:-0400 - END:DAYLIGHT - END:VTIMEZONE - BEGIN:VEVENT - UID:uid could be generated but doctest complains - DTSTART;TZID=America/Montreal:20051012T090000 - DURATION:PT1H - EXDATE;TZID=US/Pacific:20051014T090000 - RRULE:FREQ=WEEKLY;BYDAY=WE,FR;INTERVAL=2;UNTIL=20051215T090000 - END:VEVENT - END:VCALENDAR - """, - - "Generate UIDs automatically test:" : - - """ - >>> cal = base.newFromBehavior('vcalendar') - >>> cal.add('vevent').add('dtstart').value = datetime.datetime(2006,2,2,10) - >>> ser = cal.serialize() - >>> len(cal.vevent.uid_list) - 1 - """, - - "VCARD 3.0 parse test:" : - - r""" - >>> card = base.readOne(vcardtest) - >>> card.adr.value - - >>> print card.adr.value - Haight Street 512 - Novosibirsk, 80214 - Gnuland - >>> card.org.value - u'University of Novosibirsk, Department of Octopus Parthenogenesis' - >>> print card.serialize() - BEGIN:VCARD - VERSION:3.0 - ACCOUNT;TYPE=HOME:010-1234567-05 - ADR;TYPE=HOME:;;Haight Street 512;Novosibirsk;;80214;Gnuland - BDAY;VALUE=date:02-10;11-05;01-01 - FN:Daffy Duck Knudson (with Bugs Bunny and Mr. Pluto) - N:Knudson;Daffy Duck (with Bugs Bunny and Mr. Pluto);;; - NICKNAME:gnat and gnu and pluto - ORG:University of Novosibirsk, Department of Octopus Parthenogenesis - TEL;TYPE=HOME:+01-(0)2-765.43.21 - TEL;TYPE=CELL:+01-(0)5-555.55.55 - TEL;TYPE=HOME:+01-(0)2-876.54.32 - END:VCARD - """, - - "Multi-text serialization test:" : - - """ - >>> category = base.newFromBehavior('categories') - >>> category.value = ['Random category'] - >>> print category.serialize().strip() - CATEGORIES:Random category - >>> category.value.append('Other category') - >>> print category.serialize().strip() - CATEGORIES:Random category,Other category - """, - - "vCard groups test:" : - - """ - >>> card = base.readOne(vcardWithGroups) - >>> card.group - u'home' - >>> card.tel.group - u'home' - >>> card.group = card.tel.group = 'new' - >>> card.tel.serialize().strip() - u'new.TEL;TYPE=fax,voice,msg:+49 3581 123456' - >>> card.serialize().splitlines()[0] - u'new.BEGIN:VCARD' - >>> dtstart = base.newFromBehavior('dtstart') - >>> dtstart.group = "badgroup" - >>> dtstart.serialize() - Traceback (most recent call last): - ... - VObjectError: " has a group, but this object doesn't support groups" - """, - - "Lowercase components test:" : - - """ - >>> card = base.readOne(lowercaseComponentNames) - >>> card.version - - """ - } \ No newline at end of file diff --git a/src/libs/python/bongo/external/vobject/vcard.py b/src/libs/python/bongo/external/vobject/vcard.py deleted file mode 100644 index 6961667..0000000 --- a/src/libs/python/bongo/external/vobject/vcard.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Definitions and behavior for vCard 3.0""" - -from . import behavior -import itertools - -from .base import VObjectError, NativeError, ValidateError, ParseError, \ - VBase, Component, ContentLine, logger, defaultSerialize, \ - registerBehavior, backslashEscape - -from .icalendar import TextBehavior - -#------------------------ vCard structs ---------------------------------------- - -class Name(object): - def __init__(self, family = '', given = '', additional = '', prefix = '', - suffix = ''): - """Each name attribute can be a string or a list of strings.""" - self.family = family - self.given = given - self.additional = additional - self.prefix = prefix - self.suffix = suffix - - def toString(val): - """Turn a string or array value into a string.""" - if type(val) in (list, tuple): - return ' '.join(val) - return val - toString = staticmethod(toString) - - def __str__(self): - eng_order = ('prefix', 'given', 'additional', 'family', 'suffix') - return ' '.join([self.toString(getattr(self, val)) for val in eng_order]) - - def __repr__(self): - return "" % self.__str__() - -class Address(object): - def __init__(self, street = '', city = '', region = '', code = '', - country = '', box = '', extended = ''): - """Each name attribute can be a string or a list of strings.""" - self.box = box - self.extended = extended - self.street = street - self.city = city - self.region = region - self.code = code - self.country = country - - def toString(val, join_char='\n'): - """Turn a string or array value into a string.""" - if type(val) in (list, tuple): - return join_char.join(val) - return val - toString = staticmethod(toString) - - lines = ('box', 'extended', 'street') - one_line = ('city', 'region', 'code') - - def __str__(self): - lines = '\n'.join([self.toString(getattr(self, val)) for val in self.lines if getattr(self, val)]) - one_line = tuple([self.toString(getattr(self, val), ' ') for val in self.one_line]) - lines += "\n%s, %s %s" % one_line - if self.country: - lines += '\n' + self.toString(self.country) - return lines - - def __repr__(self): - return "" % self.__str__().replace('\n', '\\n') - -#------------------------ Registered Behavior subclasses ----------------------- -class VCardBehavior(behavior.Behavior): - allowGroup = True - -class VCard3_0(VCardBehavior): - """vCard 3.0 behavior.""" - name = 'VCARD' - description = 'vCard 3.0, defined in rfc2426' - versionString = '3.0' - isComponent = True - sortFirst = ('version', 'prodid', 'uid') - knownChildren = {'N': (1, 1, None),#min, max, behaviorRegistry id - 'FN': (1, 1, None), - 'VERSION': (1, 1, None),#required, auto-generated - 'PRODID': (0, 1, None), - 'LABEL': (0, None, None), - 'UID': (0, None, None), - 'ADR': (0, None, None) - } - - def generateImplicitParameters(cls, obj): - """Create PRODID, VERSION, and VTIMEZONEs if needed. - - VTIMEZONEs will need to exist whenever TZID parameters exist or when - datetimes with tzinfo exist. - - """ - if not hasattr(obj, 'version'): - obj.add(ContentLine('VERSION', [], cls.versionString)) - generateImplicitParameters = classmethod(generateImplicitParameters) -registerBehavior(VCard3_0, default=True) - -class VCardTextBehavior(TextBehavior): - allowGroup = True - base64string = 'B' - -class FN(VCardTextBehavior): - name = "FN" - description = 'Formatted name' -registerBehavior(FN) - -class Label(VCardTextBehavior): - name = "Label" - description = 'Formatted address' -registerBehavior(FN) - -def toListOrString(string): - if string.find(',') >= 0: - return string.split(',') - return string - -def splitFields(string): - """Return a list of strings or lists from a Name or Address.""" - return [toListOrString(i) for i in string.split(';')] - -def toList(stringOrList): - if isinstance(stringOrList, str): - return [stringOrList] - return stringOrList - -def serializeFields(obj, order): - """Turn an object's fields into a ';' and ',' seperated string.""" - return ';'.join([','.join(toList(getattr(obj, val))) for val in order]) - -NAME_ORDER = ('family', 'given', 'additional', 'prefix', 'suffix') - -class NameBehavior(VCardBehavior): - """A structured name.""" - hasNative = True - - def transformToNative(obj): - """Turn obj.value into a Name.""" - if obj.isNative: return obj - obj.isNative = True - obj.value = Name(**dict(list(zip(NAME_ORDER, splitFields(obj.value))))) - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - """Replace the Name in obj.value with a string.""" - obj.isNative = False - obj.value = serializeFields(obj.value, NAME_ORDER) - return obj - transformFromNative = staticmethod(transformFromNative) -registerBehavior(NameBehavior, 'N') - -ADDRESS_ORDER = ('box', 'extended', 'street', 'city', 'region', 'code', - 'country') - -class AddressBehavior(VCardBehavior): - """A structured address.""" - hasNative = True - - def transformToNative(obj): - """Turn obj.value into an Address.""" - if obj.isNative: return obj - obj.isNative = True - obj.value = Address(**dict(list(zip(ADDRESS_ORDER, splitFields(obj.value))))) - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - """Replace the Address in obj.value with a string.""" - obj.isNative = False - obj.value = serializeFields(obj.value, ADDRESS_ORDER) - return obj - transformFromNative = staticmethod(transformFromNative) -registerBehavior(AddressBehavior, 'ADR') - diff --git a/src/libs/python/bongo/external/vobject/win32tz.py b/src/libs/python/bongo/external/vobject/win32tz.py deleted file mode 100644 index 4f76aaf..0000000 --- a/src/libs/python/bongo/external/vobject/win32tz.py +++ /dev/null @@ -1,156 +0,0 @@ -import winreg -import struct -import datetime - -handle=winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) -tzparent=winreg.OpenKey(handle, - "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones") -parentsize=winreg.QueryInfoKey(tzparent)[0] - -localkey=winreg.OpenKey(handle, - "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation") -WEEKS=datetime.timedelta(7) - -def list_timezones(): - """Return a list of all time zones known to the system.""" - l=[] - for i in range(parentsize): - l.append(winreg.EnumKey(tzparent, i)) - return l - -class win32tz(datetime.tzinfo): - """tzinfo class based on win32's timezones available in the registry. - - >>> local = win32tz('Central Standard Time') - >>> oct1 = datetime.datetime(month=10, year=2004, day=1, tzinfo=local) - >>> dec1 = datetime.datetime(month=12, year=2004, day=1, tzinfo=local) - >>> oct1.dst() - datetime.timedelta(0, 3600) - >>> dec1.dst() - datetime.timedelta(0) - >>> braz = win32tz('E. South America Standard Time') - >>> braz.dst(oct1) - datetime.timedelta(0) - >>> braz.dst(dec1) - datetime.timedelta(0, 3600) - - """ - def __init__(self, name): - self.data=win32tz_data(name) - - def utcoffset(self, dt): - if self._isdst(dt): - return datetime.timedelta(minutes=self.data.dstoffset) - else: - return datetime.timedelta(minutes=self.data.stdoffset) - - def dst(self, dt): - if self._isdst(dt): - minutes = self.data.dstoffset - self.data.stdoffset - return datetime.timedelta(minutes=minutes) - else: - return datetime.timedelta(0) - - def tzname(self, dt): - if self._isdst(dt): return self.data.dstname - else: return self.data.stdname - - def _isdst(self, dt): - dat=self.data - dston = pickNthWeekday(dt.year, dat.dstmonth, dat.dstdayofweek, - dat.dsthour, dat.dstminute, dat.dstweeknumber) - dstoff = pickNthWeekday(dt.year, dat.stdmonth, dat.stddayofweek, - dat.stdhour, dat.stdminute, dat.stdweeknumber) - if dston < dstoff: - if dston <= dt.replace(tzinfo=None) < dstoff: return True - else: return False - else: - if dstoff <= dt.replace(tzinfo=None) < dston: return False - else: return True - - def __repr__(self): - return "" % self.data.display - -def pickNthWeekday(year, month, dayofweek, hour, minute, whichweek): - """dayofweek == 0 means Sunday, whichweek > 4 means last instance""" - first = datetime.datetime(year=year, month=month, hour=hour, minute=minute, - day=1) - weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7 + 1)) - for n in range(whichweek - 1, -1, -1): - dt=weekdayone + n * WEEKS - if dt.month == month: return dt - - -class win32tz_data(object): - """Read a registry key for a timezone, expose its contents.""" - - def __init__(self, path): - """Load path, or if path is empty, load local time.""" - if path: - keydict=valuesToDict(winreg.OpenKey(tzparent, path)) - self.display = keydict['Display'] - self.dstname = keydict['Dlt'] - self.stdname = keydict['Std'] - - #see http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm - tup = struct.unpack('=3l16h', keydict['TZI']) - self.stdoffset = -tup[0]-tup[1] #Bias + StandardBias * -1 - self.dstoffset = self.stdoffset - tup[2] # + DaylightBias * -1 - - offset=3 - self.stdmonth = tup[1 + offset] - self.stddayofweek = tup[2 + offset] #Sunday=0 - self.stdweeknumber = tup[3 + offset] #Last = 5 - self.stdhour = tup[4 + offset] - self.stdminute = tup[5 + offset] - - offset=11 - self.dstmonth = tup[1 + offset] - self.dstdayofweek = tup[2 + offset] #Sunday=0 - self.dstweeknumber = tup[3 + offset] #Last = 5 - self.dsthour = tup[4 + offset] - self.dstminute = tup[5 + offset] - - else: - keydict=valuesToDict(localkey) - - self.stdname = keydict['StandardName'] - self.dstname = keydict['DaylightName'] - - sourcekey=winreg.OpenKey(tzparent, self.stdname) - self.display = valuesToDict(sourcekey)['Display'] - - self.stdoffset = -keydict['Bias']-keydict['StandardBias'] - self.dstoffset = self.stdoffset - keydict['DaylightBias'] - - #see http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm - tup = struct.unpack('=8h', keydict['StandardStart']) - - offset=0 - self.stdmonth = tup[1 + offset] - self.stddayofweek = tup[2 + offset] #Sunday=0 - self.stdweeknumber = tup[3 + offset] #Last = 5 - self.stdhour = tup[4 + offset] - self.stdminute = tup[5 + offset] - - tup = struct.unpack('=8h', keydict['DaylightStart']) - self.dstmonth = tup[1 + offset] - self.dstdayofweek = tup[2 + offset] #Sunday=0 - self.dstweeknumber = tup[3 + offset] #Last = 5 - self.dsthour = tup[4 + offset] - self.dstminute = tup[5 + offset] - -def valuesToDict(key): - """Convert a registry key's values to a dictionary.""" - dict={} - size=winreg.QueryInfoKey(key)[1] - for i in range(size): - dict[winreg.EnumValue(key, i)[0]]=winreg.EnumValue(key, i)[1] - return dict - -def _test(): - import win32tz, doctest - doctest.testmod(win32tz, verbose=0) - -if __name__ == '__main__': - _test() \ No newline at end of file diff --git a/src/libs/python/bongo/nmap/CommandStream.py b/src/libs/python/bongo/nmap/CommandStream.py index 3be64a6..860a179 100755 --- a/src/libs/python/bongo/nmap/CommandStream.py +++ b/src/libs/python/bongo/nmap/CommandStream.py @@ -191,7 +191,7 @@ class ResponseIterator: def __next__(self): r = self.stream.GetResponse() if r.code == 1000: - if re.match("\d+", r.message): + if re.match(r"\d+", r.message): self.total = int(r.message) self.done = True elif r.code > 3000: diff --git a/src/libs/python/bongo/store/CommandStream.py b/src/libs/python/bongo/store/CommandStream.py index 12e6676..fcedf92 100644 --- a/src/libs/python/bongo/store/CommandStream.py +++ b/src/libs/python/bongo/store/CommandStream.py @@ -91,7 +91,9 @@ class CommandStream: log = logging.getLogger("Bongo.StoreConnection") def __init__(self, socket): - self.stream = socket.makefile() + # Python 2 returned a byte-like string here. Keep the protocol binary + # under Python 3 so declared payload lengths remain byte lengths. + self.stream = socket.makefile("rwb") def setEventCallback(self, callback): self.eventCallback = callback @@ -102,17 +104,21 @@ class CommandStream: def Read(self, length=-1): return self.stream.read(length) - def Write(self, str, nolog=False): + def Write(self, value, nolog=False): if not nolog: - self.log.debug("SEND: %s", str) - self.stream.write(str) - self.stream.write("\r\n") + self.log.debug("SEND: %s", value) + if not isinstance(value, bytes): + value = value.encode("utf-8") + self.stream.write(value) + self.stream.write(b"\r\n") self.stream.flush() - def WriteRaw(self, str, nolog=False): + def WriteRaw(self, value, nolog=False): if not nolog: - self.log.debug("SEND: %s", str) - self.stream.write(str) + self.log.debug("SEND: %s", value) + if not isinstance(value, bytes): + value = value.encode("utf-8") + self.stream.write(value) self.stream.flush() def GetResponse(self, nolog=False): @@ -136,11 +142,11 @@ class CommandStream: def readline(self): s = self.stream.readline() - if s[-2:] == "\r\n": + if s[-2:] == b"\r\n": s = s[:-2] - elif s[-1:] == "\n": + elif s[-1:] == b"\n": s = s[:-1] - return s + return s.decode("utf-8", "surrogateescape") class ResponseIterator: def __init__(self, stream): @@ -156,7 +162,7 @@ class ResponseIterator: def __next__(self): r = self.stream.GetResponse() if r.code == 1000: - if re.match("^\d+$", r.message): + if re.match(r"^\d+$", r.message): self.total = int(r.message) self.done = True elif r.code > 3000: @@ -201,4 +207,3 @@ class CollectionIterator(ResponseIterator): def __next__(self): collection = Collection(ResponseIterator.next(self)) return collection - diff --git a/src/libs/python/bongo/store/QueueClient.py b/src/libs/python/bongo/store/QueueClient.py index ac2298d..f9c25cf 100644 --- a/src/libs/python/bongo/store/QueueClient.py +++ b/src/libs/python/bongo/store/QueueClient.py @@ -73,8 +73,9 @@ class QueueClient: return r def StoreMessage(self, data): - self.stream.Write("QSTOR MESSAGE %d" % (len(data))) - self.stream.WriteRaw(data) + wire_data = data if isinstance(data, bytes) else data.encode("utf-8") + self.stream.Write("QSTOR MESSAGE %d" % (len(wire_data))) + self.stream.WriteRaw(wire_data) r = self.stream.GetResponse() if r.code != 1000: diff --git a/src/libs/python/bongo/store/StoreClient.py b/src/libs/python/bongo/store/StoreClient.py index c021608..d8f7720 100644 --- a/src/libs/python/bongo/store/StoreClient.py +++ b/src/libs/python/bongo/store/StoreClient.py @@ -95,7 +95,7 @@ class ACL : if entry == '' : continue - tmp = re.split('\s+', entry) + tmp = re.split(r'\s+', entry) self.entries.append((tmp[0], tmp[1], tmp[2])) self.changed = False @@ -408,7 +408,7 @@ class StoreClient: def CookieDelete(self, token): command = "COOKIE DELETE %s" % token - stream.log.debug("SEND: COOKIE DELETE ****************") + self.stream.log.debug("SEND: COOKIE DELETE ****************") self.stream.Write(command, nolog=1) r = self.stream.GetResponse() @@ -417,7 +417,7 @@ class StoreClient: raise CommandError(r) def CookieNew(self, timeout): - command = "COOKIE NEW %d" % (time.time() + timeout) + command = "COOKIE NEW %d" % timeout self.stream.Write(command) r = self.stream.GetResponse(nolog=True) @@ -596,7 +596,7 @@ class StoreClient: raise CommandError(r) def List(self, path, props=[], start=-1, end=-1, flags=None, mask=None): - path = path.replace(" ", "\ ") + path = path.replace(" ", "\\ ") command = "LIST %s" % path if start == -1 and end == -1: @@ -760,7 +760,7 @@ class StoreClient: self.connection.Close() def Read(self, doc): - doc = doc.replace(" ", "\ ") + doc = doc.replace(" ", "\\ ") self.stream.Write("READ %s" % (doc)) r = self.stream.GetResponse() if r.code != 2001: diff --git a/src/libs/python/libbongo/CMakeLists.txt b/src/libs/python/libbongo/CMakeLists.txt index 494c233..fb31ab2 100644 --- a/src/libs/python/libbongo/CMakeLists.txt +++ b/src/libs/python/libbongo/CMakeLists.txt @@ -19,8 +19,15 @@ python_add_module(bootstrap pybongo.c ) +python_add_module(_calendar + calendar-module.c + cal.c + json.c + pybongo.c +) + # remove 'lib' prefix from these libraries. -set_target_properties(libs bootstrap +set_target_properties(libs bootstrap _calendar PROPERTIES PREFIX "" ) @@ -35,8 +42,18 @@ target_link_libraries(libs ) target_link_libraries(bootstrap + bongomsgapi ${PYTHON_LIBRARIES} ) -install(TARGETS libs bootstrap DESTINATION ${PYTHON_SITELIB_PATH}/libbongo/) +target_link_libraries(_calendar + bongomsgapi + bongojson + bongoutil + bongoxpl + bongocal + ${PYTHON_LIBRARIES} +) + +install(TARGETS libs bootstrap _calendar DESTINATION ${PYTHON_SITELIB_PATH}/libbongo/) install(FILES __init__.py DESTINATION ${PYTHON_SITELIB_PATH}/libbongo/) diff --git a/src/libs/python/libbongo/bongoutil.c b/src/libs/python/libbongo/bongoutil.c index 426ca92..285a082 100755 --- a/src/libs/python/libbongo/bongoutil.c +++ b/src/libs/python/libbongo/bongoutil.c @@ -41,9 +41,11 @@ static PyObject * bongoutil_ModifiedUtf7ToUtf8(PyObject *self, PyObject *args) { char *utf7; - unsigned int utf7Len; + size_t utf7Len; char utf8[XPL_MAX_PATH]; - unsigned int utf8Len; + int utf8Len; + + (void)self; if (!PyArg_ParseTuple(args, "s", &utf7)) { return NULL; @@ -52,7 +54,7 @@ bongoutil_ModifiedUtf7ToUtf8(PyObject *self, PyObject *args) utf7Len = strlen(utf7); utf8Len = ModifiedUtf7ToUtf8(utf7, utf7Len, utf8, XPL_MAX_PATH); - if(utf8Len < 0) { + if (utf8Len < 0 || utf8Len >= XPL_MAX_PATH) { Py_INCREF(Py_None); return Py_None; } diff --git a/src/libs/python/libbongo/bootstrap.c b/src/libs/python/libbongo/bootstrap.c index 01219d0..875e7ec 100644 --- a/src/libs/python/libbongo/bootstrap.c +++ b/src/libs/python/libbongo/bootstrap.c @@ -30,7 +30,6 @@ #include #include "bootstrap.h" #include "msgapi-defs.h" -#include #include #ifdef __cplusplus @@ -38,6 +37,17 @@ extern "C" { #endif static PyMethodDef ModuleMethods[] = { {NULL} }; +static struct PyModuleDef Module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "bootstrap", + .m_doc = NULL, + .m_size = -1, + .m_methods = ModuleMethods, + .m_slots = NULL, + .m_traverse = NULL, + .m_clear = NULL, + .m_free = NULL +}; extern EnumItemDef MdbEnums[]; extern PyMethodDef MdbMethods[]; extern EnumItemDef MsgApiEnums[]; @@ -50,22 +60,19 @@ PyMethodDef MsgApiMethods[] = { }; PyMODINIT_FUNC -initbootstrap() +PyInit_bootstrap(void) { - char dbfdir[PATH_MAX]; - - /* Initialize the various bongo libraries */ - if (!MemoryManagerOpen((unsigned char*)"pybongo")) { - PyErr_SetString(PyExc_ImportError, - "bongo.libs error: MemoryManagerOpen() failed"); - return; - } + PyObject *module; /* Create the libs module */ - PyObject *module = Py_InitModule("bootstrap", ModuleMethods); + module = PyModule_Create(&Module); + if (!module) { + return NULL; + } /* Add the Bongo libs */ AddLibrary(module, "msgapi", MsgApiMethods, MsgApiEnums); + return module; } #ifdef __cplusplus diff --git a/src/libs/python/libbongo/cal.c b/src/libs/python/libbongo/cal.c index 2401f58..8a3f664 100644 --- a/src/libs/python/libbongo/cal.c +++ b/src/libs/python/libbongo/cal.c @@ -17,6 +17,8 @@ cal_Merge(PyObject *self, PyObject *args) JsonObject *a; JsonObject *b; JsonObject *ret; + + (void)self; if (!PyArg_ParseTuple(args, "OO", &a, &b)) { PyErr_SetString(PyExc_TypeError, "Merge() takes 2 arguments"); @@ -79,6 +81,7 @@ CalTimeFromPy(PyObject *dt) static PyObject * cal_PrimaryOccurrence(PyObject *self, PyObject *args) { + (void)self; JsonObject *json; const char *tzid; BongoCalObject *cal; @@ -109,6 +112,7 @@ cal_PrimaryOccurrence(PyObject *self, PyObject *args) static PyObject * cal_Collect(PyObject *self, PyObject *args) { + (void)self; JsonObject *json; BongoCalObject *cal; GArray *occs; @@ -162,6 +166,7 @@ cal_Collect(PyObject *self, PyObject *args) static PyObject * cal_JsonToIcal(PyObject *self, PyObject *args) { + (void)self; JsonObject *a; BongoCalObject *cal; icalcomponent *comp; @@ -192,6 +197,7 @@ cal_JsonToIcal(PyObject *self, PyObject *args) static PyObject * cal_IcalToJson(PyObject *self, PyObject *args) { + (void)self; BongoJsonObject *json; icalcomponent *comp; char *str; @@ -202,15 +208,26 @@ cal_IcalToJson(PyObject *self, PyObject *args) } comp = icalparser_parse_string(str); + if (comp == NULL) { + PyErr_SetString(PyExc_ValueError, "Invalid iCalendar data"); + return NULL; + } json = BongoCalIcalToJson(comp); icalcomponent_free(comp); + if (json == NULL) { + PyErr_SetString(PyExc_ValueError, + "Unsupported or invalid iCalendar component"); + return NULL; + } + return JsonObjectToPy(json, TRUE); } static PyObject * cal_BongoCalTimeToStringConcise(PyObject *self, PyObject *args) { + (void)self; BongoCalTime t; char *str; char buf[128]; @@ -257,6 +274,7 @@ GetTz(const char *tzid, BongoCalObject **cal, const char *json) static PyObject * cal_ConvertTzid(PyObject *self, PyObject *args) { + (void)self; BongoCalTime t; const char *json; const char *timestr; @@ -305,6 +323,7 @@ cal_ConvertTzid(PyObject *self, PyObject *args) int BongoCalPostInit(PyObject *module) { + (void)module; #ifdef PyDateTime_IMPORT PyDateTime_IMPORT; #endif @@ -313,13 +332,13 @@ BongoCalPostInit(PyObject *module) } PyMethodDef CalMethods[] = { - {"Merge", cal_Merge, METH_VARARGS | METH_STATIC}, - {"Collect", cal_Collect, METH_VARARGS | METH_STATIC}, - {"IcalToJson", cal_IcalToJson, METH_VARARGS | METH_STATIC}, - {"JsonToIcal", cal_JsonToIcal, METH_VARARGS | METH_STATIC}, - {"PrimaryOccurrence", cal_PrimaryOccurrence, METH_VARARGS | METH_STATIC}, - {"TimeToStringConcise", cal_BongoCalTimeToStringConcise, METH_VARARGS | METH_STATIC }, - {"ConvertTzid", cal_ConvertTzid, METH_VARARGS | METH_STATIC }, + {"Merge", cal_Merge, METH_VARARGS | METH_STATIC, NULL}, + {"Collect", cal_Collect, METH_VARARGS | METH_STATIC, NULL}, + {"IcalToJson", cal_IcalToJson, METH_VARARGS | METH_STATIC, NULL}, + {"JsonToIcal", cal_JsonToIcal, METH_VARARGS | METH_STATIC, NULL}, + {"PrimaryOccurrence", cal_PrimaryOccurrence, METH_VARARGS | METH_STATIC, NULL}, + {"TimeToStringConcise", cal_BongoCalTimeToStringConcise, METH_VARARGS | METH_STATIC, NULL}, + {"ConvertTzid", cal_ConvertTzid, METH_VARARGS | METH_STATIC, NULL}, {NULL, NULL, 0, NULL} }; diff --git a/src/libs/python/libbongo/calendar-module.c b/src/libs/python/libbongo/calendar-module.c new file mode 100644 index 0000000..af5a447 --- /dev/null +++ b/src/libs/python/libbongo/calendar-module.c @@ -0,0 +1,49 @@ +#include +#include +#include +#include + +#include "libs.h" + +extern PyMethodDef CalMethods[]; +extern PyMethodDef BongoJsonMethods[]; + +static PyMethodDef ModuleMethods[] = {{NULL}}; + +static struct PyModuleDef Module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "_calendar", + .m_doc = "Standalone Bongo iCalendar conversion bindings.", + .m_size = -1, + .m_methods = ModuleMethods, + .m_slots = NULL, + .m_traverse = NULL, + .m_clear = NULL, + .m_free = NULL +}; + +PyMODINIT_FUNC +PyInit__calendar(void) +{ + PyObject *module; + + XplInit(); + if (!BongoJsonPreInit()) { + return NULL; + } + if (!BongoCalInit(XPL_DEFAULT_DBF_DIR)) { + PyErr_SetString(PyExc_ImportError, + "could not initialize Bongo calendar timezones"); + return NULL; + } + + module = PyModule_Create(&Module); + if (module == NULL) { + return NULL; + } + AddLibrary(module, "cal", CalMethods, NULL); + AddLibrary(module, "bongojson", BongoJsonMethods, NULL); + BongoJsonPostInit(module); + BongoCalPostInit(module); + return module; +} diff --git a/src/libs/python/libbongo/json.c b/src/libs/python/libbongo/json.c index d2c6d82..366fa4f 100644 --- a/src/libs/python/libbongo/json.c +++ b/src/libs/python/libbongo/json.c @@ -17,51 +17,24 @@ static PyMethodDef JsonObject_methods[] = { }; static PyMappingMethods JsonObject_mapping = { - JsonObject_length, - JsonObject_subscript, - JsonObject_subscript_assign + .mp_length = JsonObject_length, + .mp_subscript = JsonObject_subscript, + .mp_ass_subscript = JsonObject_subscript_assign }; PyTypeObject JsonObjectType = { - PyObject_HEAD_INIT(NULL) - 0, /* ob_size */ - "libs.JsonObject", - sizeof(JsonObject), - 0, /*tp_itemsize*/ - (destructor)JsonObject_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - &JsonObject_mapping, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - JsonObject_str, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ - "Bongo Json Object", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - JsonObject_methods, /* tp_methods */ - JsonObject_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)JsonObject_init, /* tp_init */ - 0, /* tp_alloc */ - JsonObject_new, /* tp_new */ + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "libs.JsonObject", + .tp_basicsize = sizeof(JsonObject), + .tp_dealloc = (destructor)JsonObject_dealloc, + .tp_as_mapping = &JsonObject_mapping, + .tp_str = JsonObject_str, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = "Bongo Json Object", + .tp_methods = JsonObject_methods, + .tp_members = JsonObject_members, + .tp_init = (initproc)JsonObject_init, + .tp_new = JsonObject_new, }; @@ -75,16 +48,8 @@ static PyMethodDef JsonArray_methods[] = { }; static PySequenceMethods JsonArray_sequence = { - JsonArray_length, - JsonArray_concat, - JsonArray_repeat, - JsonArray_item, - JsonArray_slice, - JsonArray_assign, - JsonArray_assign_slice, - JsonArray_contains, - JsonArray_inplace_concat, - JsonArray_inplace_repeat, + .sq_length = JsonArray_length, + .sq_item = JsonArray_item, }; #if 0 @@ -103,45 +68,18 @@ static PySequenceMethods JsonArray_sequence = { PyTypeObject JsonArrayType = { - PyObject_HEAD_INIT(NULL) - 0, /* ob_size */ - "libs.JsonArray", - sizeof(JsonArray), - 0, /*tp_itemsize*/ - (destructor)JsonArray_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - &JsonArray_sequence, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - JsonArray_str, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ - "Bongo Json Array", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - JsonArray_methods, /* tp_methods */ - JsonArray_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)JsonArray_init, /* tp_init */ - 0, /* tp_alloc */ - JsonArray_new, /* tp_new */ + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "libs.JsonArray", + .tp_basicsize = sizeof(JsonArray), + .tp_dealloc = (destructor)JsonArray_dealloc, + .tp_as_sequence = &JsonArray_sequence, + .tp_str = JsonArray_str, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = "Bongo Json Array", + .tp_methods = JsonArray_methods, + .tp_members = JsonArray_members, + .tp_init = (initproc)JsonArray_init, + .tp_new = JsonArray_new, }; static void @@ -291,7 +229,8 @@ JsonNodeToPy(BongoJsonNode *node, BOOL own, BOOL native) ret = (PyObject*)PyObject_New(JsonArray, &JsonArrayType); ((JsonArray*)ret)->own = own; ((JsonArray*)ret)->array = BongoJsonNodeAsArray(node); - } + } + break; case BONGO_JSON_BOOL : if (BongoJsonNodeAsBool(node)) { ret = Py_True; @@ -327,13 +266,16 @@ JsonArray_dealloc(JsonArray *self) BongoJsonArrayFree(self->array); } - self->ob_type->tp_free((PyObject*)self); + Py_TYPE(self)->tp_free((PyObject*)self); } PyObject * JsonArray_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { JsonArray *self; + + (void)args; + (void)kwds; self = (JsonArray*)type->tp_alloc(type, 0); self->own = FALSE; @@ -348,6 +290,8 @@ JsonArray_new(PyTypeObject *type, PyObject *args, PyObject *kwds) int JsonArray_init(JsonArray *self, PyObject *args, PyObject *kwds) { + (void)args; + (void)kwds; self->own = TRUE; self->array = g_array_sized_new(FALSE, FALSE, sizeof(BongoJsonNode*), 16); return 0; @@ -400,20 +344,6 @@ JsonArray_length(PyObject *selfp) return (self->array->len > 0) ? (Py_ssize_t)self->array->len : 0; } -PyObject * -JsonArray_concat(PyObject *selfp, PyObject *b) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return NULL; -} - -PyObject * -JsonArray_repeat(PyObject *selfp, Py_ssize_t i) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return NULL; -} - PyObject * JsonArray_item(PyObject *selfp, Py_ssize_t i) { @@ -430,48 +360,6 @@ JsonArray_item(PyObject *selfp, Py_ssize_t i) return JsonNodeToPy(node, FALSE, FALSE); } -PyObject * -JsonArray_slice(PyObject *selfp, Py_ssize_t i1, Py_ssize_t i2) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return NULL; -} - -int -JsonArray_assign(PyObject *selfp, Py_ssize_t i, PyObject *obj) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return 0; -} - -int -JsonArray_assign_slice(PyObject *selfp, Py_ssize_t i1, Py_ssize_t i2, PyObject *obj) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return 0; -} - -int -JsonArray_contains(PyObject *selfp, PyObject *b) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return 0; -} - -PyObject * -JsonArray_inplace_concat(PyObject *selfp, PyObject *b) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return NULL; -} - -PyObject * -JsonArray_inplace_repeat(PyObject *selfp, Py_ssize_t i) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return NULL; -} - /*** JsonObject ***/ void @@ -483,7 +371,7 @@ JsonObject_dealloc(JsonObject *self) } #endif - self->ob_type->tp_free((PyObject*)self); + Py_TYPE(self)->tp_free((PyObject*)self); } @@ -492,6 +380,9 @@ JsonObject_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { JsonObject *self; + (void)args; + (void)kwds; + self = (JsonObject*)type->tp_alloc(type, 0); self->obj = NULL; self->own = FALSE; @@ -506,6 +397,8 @@ JsonObject_new(PyTypeObject *type, PyObject *args, PyObject *kwds) int JsonObject_init(JsonObject *self, PyObject *args, PyObject *kwds) { + (void)args; + (void)kwds; self->obj = BongoJsonObjectNew(); self->own = TRUE; self->valid = TRUE; @@ -531,6 +424,7 @@ JsonObject_str(PyObject *selfp) Py_ssize_t JsonObject_length(PyObject *self) { + (void)self; /* FIXME */ return 1; } @@ -618,6 +512,7 @@ BongoJsonPostInit(PyObject *module) static PyObject * bongojson_BongoJsonParseString(PyObject *self, PyObject *args) { + (void)self; char *jsonStr; BongoJsonNode *node; BongoJsonResult res; @@ -665,6 +560,7 @@ bongojson_BongoJsonParseString(PyObject *self, PyObject *args) static PyObject * bongojson_ToPy(PyObject *self, PyObject *args) { + (void)self; PyObject *obj; PyObject *ret; diff --git a/src/libs/python/libbongo/json.h b/src/libs/python/libbongo/json.h index aaaea0c..e2c9c24 100644 --- a/src/libs/python/libbongo/json.h +++ b/src/libs/python/libbongo/json.h @@ -7,11 +7,6 @@ #include #include -// attempt to support python 2.4-2.5 -#ifndef Py_ssize_t -#define Py_ssize_t int -#endif - #ifdef __cplusplus extern "C" { #endif @@ -65,15 +60,7 @@ extern PyTypeObject JsonArrayType; /* Sequence protocol */ Py_ssize_t JsonArray_length(PyObject *selfp); -PyObject *JsonArray_concat(PyObject *selfp, PyObject *b); -PyObject *JsonArray_repeat(PyObject *selfp, Py_ssize_t i); PyObject *JsonArray_item(PyObject *selfp, Py_ssize_t i); -PyObject *JsonArray_slice(PyObject *selfp, Py_ssize_t i1, Py_ssize_t i2); -int JsonArray_assign(PyObject *selfp, Py_ssize_t i, PyObject *obj); -int JsonArray_assign_slice(PyObject *selfp, Py_ssize_t i1, Py_ssize_t i2, PyObject *obj); -int JsonArray_contains(PyObject *selfp, PyObject *b); -PyObject *JsonArray_inplace_concat(PyObject *selfp, PyObject *b); -PyObject *JsonArray_inplace_repeat(PyObject *selfp, Py_ssize_t i); PyObject *JsonObjectToPy(BongoJsonObject *object, BOOL own); diff --git a/src/libs/python/libbongo/libs.c b/src/libs/python/libbongo/libs.c index 3b18513..fc8d286 100644 --- a/src/libs/python/libbongo/libs.c +++ b/src/libs/python/libbongo/libs.c @@ -36,6 +36,17 @@ extern "C" { #endif static PyMethodDef ModuleMethods[] = { {NULL} }; +static struct PyModuleDef Module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "libs", + .m_doc = NULL, + .m_size = -1, + .m_methods = ModuleMethods, + .m_slots = NULL, + .m_traverse = NULL, + .m_clear = NULL, + .m_free = NULL +}; /* Pull in the methods from the bindings */ #if 0 @@ -54,15 +65,15 @@ extern PyMethodDef BongoUtilMethods[]; extern PyMethodDef TestMethods[]; PyMODINIT_FUNC -initlibs() +PyInit_libs(void) { - char dbfdir[PATH_MAX]; + PyObject *module; /* Initialize the various bongo libraries */ if (!ConnStartup(DEFAULT_CONNECTION_TIMEOUT)) { PyErr_SetString(PyExc_ImportError, "bongo.libs error: ConnStartup() failed"); - return; + return NULL; } MsgInit(); @@ -70,29 +81,32 @@ initlibs() if (!BongoCalInit(MsgGetDir(MSGAPI_DIR_DBF, NULL, 0))) { PyErr_SetString(PyExc_ImportError, "bongo.libs error: BongoCalInit() failed"); - return; + return NULL; } if (!NMAPInitialize()) { PyErr_SetString(PyExc_ImportError, "bongo.libs error: NMAPInitialize() failed"); - return; + return NULL; } if (!StreamioInit()) { PyErr_SetString(PyExc_ImportError, "bongo.libs error: StreamioInit() failed"); - return; + return NULL; } if (!BongoJsonPreInit()) { PyErr_SetString(PyExc_ImportError, "bongo.libs error: BongoJsonPreInit() failed"); - return; + return NULL; } /* Create the libs module */ - PyObject *module = Py_InitModule("libs", ModuleMethods); + module = PyModule_Create(&Module); + if (!module) { + return NULL; + } /* Add the Bongo libs */ AddLibrary(module, "cal", CalMethods, NULL); @@ -105,6 +119,7 @@ initlibs() BongoJsonPostInit(module); BongoCalPostInit(module); + return module; } #ifdef __cplusplus diff --git a/src/libs/python/libbongo/msgapi-defs.c b/src/libs/python/libbongo/msgapi-defs.c index f61e037..a149ff7 100644 --- a/src/libs/python/libbongo/msgapi-defs.c +++ b/src/libs/python/libbongo/msgapi-defs.c @@ -1,8 +1,36 @@ #include #include "msgapi-defs.h" +PyObject * +msgapi_GetConfigProperty(PyObject *self, PyObject *args) +{ + const char *property; + unsigned char value[MSGSRV_CONFIG_MAX_PROP_CHARS + 1]; + + (void)self; + if (!PyArg_ParseTuple(args, "s", &property)) + return NULL; + if (!MsgGetConfigProperty(value, (unsigned char *)property)) { + PyErr_SetString(PyExc_KeyError, property); + return NULL; + } + return PyUnicode_FromString((const char *)value); +} + PyObject * msgapi_GetUnprivilegedUser(PyObject *self, PyObject *args) { + (void)self; + (void)args; return Py_BuildValue("z", MsgGetUnprivilegedUser()); } + +EnumItemDef MsgApiEnums[] = { + {"MAX_PROP_CHARS", MSGSRV_CONFIG_MAX_PROP_CHARS, NULL}, + {"MESSAGING_SERVER", 0, MSGSRV_CONFIG_PROP_MESSAGING_SERVER}, + {"WEB_ADMIN_SERVER", 0, MSGSRV_CONFIG_PROP_WEB_ADMIN_SERVER}, + {"DEFAULT_CONTEXT", 0, MSGSRV_CONFIG_PROP_DEFAULT_CONTEXT}, + {"BONGO_SERVICES", 0, MSGSRV_CONFIG_PROP_BONGO_SERVICES}, + {"CONFIG_FILENAME", 0, MSGSRV_CONFIG_FILENAME}, + {NULL, 0, NULL} +}; diff --git a/src/libs/python/libbongo/msgapi.c b/src/libs/python/libbongo/msgapi.c index b2f57c2..199573a 100644 --- a/src/libs/python/libbongo/msgapi.c +++ b/src/libs/python/libbongo/msgapi.c @@ -21,6 +21,7 @@ ****************************************************************************/ #include +#include #include #include @@ -41,6 +42,7 @@ Collect and import the calendars subscribed to by all users."); static PyObject * msgapi_CollectAllUsers(PyObject *self, PyObject *args) { + (void)self; if (!PyArg_ParseTuple(args, "")) { return NULL; } @@ -61,6 +63,8 @@ msgapi_CollectUser(PyObject *self, PyObject *args) { char *user; + (void)self; + if (!PyArg_ParseTuple(args, "s", &user)) { return NULL; } @@ -71,38 +75,6 @@ msgapi_CollectUser(PyObject *self, PyObject *args) return Py_None; } -PyDoc_STRVAR(GetUserFeature_doc, -"GetUserFeature(dn, featurename, ) -> array of strings\n\ -\n\ -Return a string containing the ip address of user u's document store."); - -static const char *features[] = { - "", - "imap", - "pop", - "addressbook", - "proxy", - "forward", - "autoreply", - "rules", - "finger", - "smtp_send_count_limit", - "smtp_send_size_limit", - "nmap_quota", - "nmap_store", - "webmail", - "modweb", - "mwmail_addressbook_personal", - "mwmail_addressbook_system", - "mwmail_addressbook_global", - "modweb_wap", - "calagent", - "calendar", - "antivirus", - "shared_folders", - NULL -}; - static void ThrowImportException(int code) { @@ -140,6 +112,7 @@ The calendar with that name must already exist."); static PyObject * msgapi_ImportIcsUrl(PyObject *self, PyObject *args) { + (void)self; char *user; char *calName; char *url; @@ -170,6 +143,7 @@ The calendar with that name must already exist."); static PyObject * msgapi_ImportIcs(PyObject *self, PyObject *args) { + (void)self; PyObject *pyfile = NULL; FILE *file = NULL; char *user; @@ -180,12 +154,22 @@ msgapi_ImportIcs(PyObject *self, PyObject *args) if (!PyArg_ParseTuple(args, "Ossz", &pyfile, &user, &calName, &color)) { return NULL; } - if(!PyFile_Check(pyfile)) + int fd = PyObject_AsFileDescriptor(pyfile); + if (fd < 0) return NULL; - file = PyFile_AsFile(pyfile); + fd = dup(fd); + if (fd < 0) { + return PyErr_SetFromErrno(PyExc_OSError); + } + file = fdopen(fd, "rb"); + if (!file) { + close(fd); + return PyErr_SetFromErrno(PyExc_OSError); + } ret = MsgImportIcs(file, user, calName, color, NULL); + fclose(file); if (ret < 0) { ThrowImportException(ret); return NULL; @@ -202,6 +186,7 @@ Import the specified calendar url."); static PyObject * msgapi_IcsImport(PyObject *self, PyObject *args) { + (void)self; char *user; char *name; char *url; @@ -233,6 +218,7 @@ Subscribe user to the specified calendar url."); static PyObject * msgapi_IcsSubscribe(PyObject *self, PyObject *args) { + (void)self; char *user; char *name; char *url; @@ -265,6 +251,7 @@ s is of the format \"NMAP <40c0cbb0hostname441444a4>\"."); static PyObject * msgapi_NmapChallenge(PyObject *self, PyObject *args) { + (void)self; unsigned char *salt; unsigned int len=33; unsigned char buf[len]; @@ -290,6 +277,9 @@ msgapi_GetBuildVersion(PyObject *self, PyObject *args) int version; BOOL custom; + (void)self; + (void)args; + MsgGetBuildVersion (&version, &custom); return Py_BuildValue("ib", version, custom); @@ -306,6 +296,9 @@ msgapi_GetAvailableVersion(PyObject *self, PyObject *args) { int version; + (void)self; + (void)args; + if (MsgGetAvailableVersion (&version)) { return Py_BuildValue("i", version); } else { diff --git a/src/libs/python/libbongo/pybongo.c b/src/libs/python/libbongo/pybongo.c index 8c69829..07ce05a 100644 --- a/src/libs/python/libbongo/pybongo.c +++ b/src/libs/python/libbongo/pybongo.c @@ -11,26 +11,22 @@ AddLibrary(PyObject *module, char *name, PyMethodDef *classMethods, PyMethodDef *def; EnumItemDef *item; - /* Create the new class object and add it to the module */ - PyObject *moduleDict = PyModule_GetDict(module); - PyObject *classDict = PyDict_New(); - PyObject *className = PyString_FromString(name); - PyObject *classObj = PyClass_New(NULL, classDict, className); + PyObject *library = PyModule_New(name); - PyDict_SetItemString(moduleDict, name, classObj); + if (!library) { + return; + } /* Add the class's methods to its dict */ for (def = classMethods; def->ml_name != NULL; def++) { - PyObject *func = PyCFunction_New(def, NULL); - PyObject *method; - if (def->ml_flags & METH_STATIC) { - method = PyStaticMethod_New(func); - } else { - method = PyMethod_New(func, NULL, classObj); + PyObject *func; + def->ml_flags &= ~METH_STATIC; + func = PyCFunction_NewEx(def, NULL, library); + if (!func || PyModule_AddObject(library, def->ml_name, func) < 0) { + Py_XDECREF(func); + Py_DECREF(library); + return; } - PyDict_SetItemString(classDict, def->ml_name, method); - Py_DECREF(func); - Py_DECREF(method); } if (enumItems != NULL) { @@ -41,14 +37,17 @@ AddLibrary(PyObject *module, char *name, PyMethodDef *classMethods, } else { value = PyInt_FromLong(item->value); } - PyDict_SetItemString(classDict, item->name, value); - Py_DECREF(value); + if (!value || PyModule_AddObject(library, item->name, value) < 0) { + Py_XDECREF(value); + Py_DECREF(library); + return; + } } } - Py_DECREF(classDict); - Py_DECREF(className); - Py_DECREF(classObj); + if (PyModule_AddObject(module, name, library) < 0) { + Py_DECREF(library); + } } #ifdef __cplusplus diff --git a/src/libs/python/libbongo/pybongo.h b/src/libs/python/libbongo/pybongo.h index 9860908..c8b0f78 100644 --- a/src/libs/python/libbongo/pybongo.h +++ b/src/libs/python/libbongo/pybongo.h @@ -3,6 +3,13 @@ #include +#define PyInt_Check PyLong_Check +#define PyInt_AsLong PyLong_AsLong +#define PyInt_FromLong PyLong_FromLong +#define PyString_Check PyUnicode_Check +#define PyString_AsString(object) ((char *)PyUnicode_AsUTF8(object)) +#define PyString_FromString PyUnicode_FromString + #ifdef __cplusplus extern "C" { #endif diff --git a/src/libs/python/libbongo/streamio.c b/src/libs/python/libbongo/streamio.c index 8417895..036563e 100644 --- a/src/libs/python/libbongo/streamio.c +++ b/src/libs/python/libbongo/streamio.c @@ -2,6 +2,16 @@ #include #include +#include "pybongo.h" + +static const char *BONGO_STREAM_CAPSULE = "bongo.BongoStream"; + +static void +BongoStreamCapsuleFree(PyObject *capsule) +{ + BongoStream *stream = PyCapsule_GetPointer(capsule, BONGO_STREAM_CAPSULE); + if (stream) BongoStreamFree(stream); +} #ifdef __cplusplus extern "C" { @@ -15,6 +25,7 @@ Add a codec to a stream."); static PyObject * streamio_BongoStreamAddCodec(PyObject *self, PyObject *args) { + (void)self; PyObject *pystream = NULL; BongoStream *stream = NULL; char *codec = NULL; @@ -25,7 +36,8 @@ streamio_BongoStreamAddCodec(PyObject *self, PyObject *args) if (!PyArg_ParseTuple(args, "OsO", &pystream, &codec, &pyencode)) { return NULL; } - stream = PyCObject_AsVoidPtr(pystream); + stream = PyCapsule_GetPointer(pystream, BONGO_STREAM_CAPSULE); + if (!stream) return NULL; if (!PyBool_Check(pyencode)) { PyErr_SetString(PyExc_TypeError, @@ -50,6 +62,7 @@ Return a pointer to a BongoStream."); static PyObject * streamio_BongoStreamCreate(PyObject *self, PyObject *args) { + (void)self; PyObject *pycodecs = NULL; PyObject *pyencode = NULL; BongoStream *stream = NULL; @@ -89,7 +102,7 @@ streamio_BongoStreamCreate(PyObject *self, PyObject *args) return Py_None; } - return PyCObject_FromVoidPtr(stream, (void *)BongoStreamFree); + return PyCapsule_New(stream, BONGO_STREAM_CAPSULE, BongoStreamCapsuleFree); } PyDoc_STRVAR(BongoStreamEos_doc, @@ -100,13 +113,15 @@ Close down a stream."); static PyObject * streamio_BongoStreamEos(PyObject *self, PyObject *args) { + (void)self; PyObject *pystream = NULL; BongoStream *stream = NULL; if (!PyArg_ParseTuple(args, "O", &pystream)) { return NULL; } - stream = PyCObject_AsVoidPtr(pystream); + stream = PyCapsule_GetPointer(pystream, BONGO_STREAM_CAPSULE); + if (!stream) return NULL; BongoStreamEos(stream); @@ -123,25 +138,27 @@ Return the number of bytes read."); static PyObject * streamio_BongoStreamGet(PyObject *self, PyObject *args) { + (void)self; PyObject *pystream = NULL; BongoStream *stream = NULL; PyObject *pybuf = NULL; - char *buf; - int bufLen; + Py_buffer view; int read = 0; if (!PyArg_ParseTuple(args, "OO", &pystream, &pybuf)) { return NULL; } - stream = PyCObject_AsVoidPtr(pystream); + stream = PyCapsule_GetPointer(pystream, BONGO_STREAM_CAPSULE); + if (!stream) return NULL; - if (PyObject_AsCharBuffer(pybuf, (const char**)&buf, &bufLen)) { - PyErr_SetString(PyExc_TypeError, "BongoStreamGet() buffer must be a string"); + if (PyObject_GetBuffer(pybuf, &view, PyBUF_WRITABLE) < 0) { + PyErr_SetString(PyExc_TypeError, "BongoStreamGet() buffer must be writable"); return NULL; } - read = BongoStreamGet(stream, buf, bufLen); + read = BongoStreamGet(stream, view.buf, view.len); + PyBuffer_Release(&view); return Py_BuildValue("i", read); } @@ -154,24 +171,26 @@ Put as many bytes possible from buffer into the stream."); static PyObject * streamio_BongoStreamPut(PyObject *self, PyObject *args) { + (void)self; PyObject *pystream = NULL; BongoStream *stream = NULL; PyObject *pybuf = NULL; - const char *buf; - int bufLen; + Py_buffer view; if (!PyArg_ParseTuple(args, "OO", &pystream, &pybuf)) { return NULL; } - stream = PyCObject_AsVoidPtr(pystream); + stream = PyCapsule_GetPointer(pystream, BONGO_STREAM_CAPSULE); + if (!stream) return NULL; - if (PyObject_AsCharBuffer(pybuf, &buf, &bufLen)) { - PyErr_SetString(PyExc_TypeError, "BongoStreamPut() buffer must be a string"); + if (PyObject_GetBuffer(pybuf, &view, PyBUF_SIMPLE) < 0) { + PyErr_SetString(PyExc_TypeError, "BongoStreamPut() argument must support the buffer protocol"); return NULL; } - BongoStreamPut(stream, buf, bufLen); + BongoStreamPut(stream, view.buf, view.len); + PyBuffer_Release(&view); Py_INCREF(Py_None); return Py_None; diff --git a/src/libs/python/libbongo/tests.c b/src/libs/python/libbongo/tests.c index 0d3ab78..31b959c 100644 --- a/src/libs/python/libbongo/tests.c +++ b/src/libs/python/libbongo/tests.c @@ -14,6 +14,8 @@ extern "C" { static PyObject * test_GetAsciiString(PyObject *one, PyObject *two) { + (void)one; + (void)two; char *hello_english = "Hello"; PyObject *ret; @@ -25,6 +27,8 @@ test_GetAsciiString(PyObject *one, PyObject *two) static PyObject * test_GetUnicodeString(PyObject *one, PyObject *two) { + (void)one; + (void)two; char *hello_finnish = "hyvää päivää"; PyObject *ret; @@ -35,8 +39,8 @@ test_GetUnicodeString(PyObject *one, PyObject *two) } PyMethodDef TestMethods[] = { - {"GetAsciiString", test_GetAsciiString, METH_VARARGS | METH_STATIC}, - {"GetUnicodeString", test_GetUnicodeString, METH_VARARGS | METH_STATIC}, + {"GetAsciiString", test_GetAsciiString, METH_VARARGS | METH_STATIC, NULL}, + {"GetUnicodeString", test_GetUnicodeString, METH_VARARGS | METH_STATIC, NULL}, {NULL, NULL, 0, NULL} }; diff --git a/src/libs/sasl/CMakeLists.txt b/src/libs/sasl/CMakeLists.txt new file mode 100644 index 0000000..7898558 --- /dev/null +++ b/src/libs/sasl/CMakeLists.txt @@ -0,0 +1,10 @@ +add_library(bongosasl sasl.c) +target_include_directories(bongosasl PRIVATE ${SASL2_INCLUDE_DIRS}) +target_link_libraries(bongosasl PRIVATE ${SASL2_LIBRARIES}) +install(TARGETS bongosasl DESTINATION ${LIB_INSTALL_DIR}) + +if(BUILD_TESTING) + add_executable(bongo-sasl-test tests/sasl-test.c) + target_link_libraries(bongo-sasl-test PRIVATE bongosasl) + add_test(NAME sasl-plain COMMAND bongo-sasl-test) +endif() diff --git a/src/libs/sasl/sasl.c b/src/libs/sasl/sasl.c new file mode 100644 index 0000000..84639f9 --- /dev/null +++ b/src/libs/sasl/sasl.c @@ -0,0 +1,177 @@ +#include +#include +#include +#include +#include +#include + +struct BongoSaslSession { + sasl_conn_t *connection; + BongoSaslVerify verify; + void *context; + sasl_callback_t callbacks[2]; + char mechanisms[256]; +}; + +static int +check_password(sasl_conn_t *connection, void *context, const char *user, + const char *password, unsigned password_length, + struct propctx *properties) +{ + BongoSaslSession *session = context; + (void)connection; + (void)properties; + if (!session || !session->verify || !user || !password) return SASL_BADPARAM; + return session->verify(session->context, user, password, password_length) + ? SASL_OK : SASL_BADAUTH; +} + +int BongoSaslInitialize(void) +{ + return sasl_server_init(NULL, "bongo") == SASL_OK; +} + +void BongoSaslShutdown(void) { sasl_server_done(); } + +int +BongoSaslSessionCreate(BongoSaslSession **output, const char *service, + const char *hostname, const char *local_address, + const char *remote_address, int tls_active, + BongoSaslVerify verify, void *context) +{ + BongoSaslSession *session; + sasl_server_userdb_checkpass_t *typed_callback = check_password; + sasl_ssf_t external_ssf = tls_active ? 256 : 0; + if (!output || !service || !verify) return 0; + *output = NULL; + session = calloc(1, sizeof(*session)); + if (!session) return 0; + session->verify = verify; + session->context = context; + session->callbacks[0].id = SASL_CB_SERVER_USERDB_CHECKPASS; + _Static_assert(sizeof(session->callbacks[0].proc) == sizeof(typed_callback), + "Cyrus SASL callback pointer size mismatch"); + memcpy(&session->callbacks[0].proc, &typed_callback, + sizeof(session->callbacks[0].proc)); + session->callbacks[0].context = session; + session->callbacks[1].id = SASL_CB_LIST_END; + if (sasl_server_new(service, hostname, NULL, local_address, remote_address, + session->callbacks, 0, &session->connection) != SASL_OK || + sasl_setprop(session->connection, SASL_SSF_EXTERNAL, &external_ssf) != SASL_OK) { + BongoSaslSessionDestroy(&session); + return 0; + } + *output = session; + return 1; +} + +void +BongoSaslSessionDestroy(BongoSaslSession **session) +{ + if (!session || !*session) return; + if ((*session)->connection) sasl_dispose(&(*session)->connection); + free(*session); + *session = NULL; +} + +int +BongoSaslMechanisms(BongoSaslSession *session, const char **mechanisms) +{ + const char *available; + char *copy, *token, *save = NULL; + size_t used = 0; + unsigned length; + int count; + if (!session || !mechanisms) return 0; + if (sasl_listmech(session->connection, NULL, "", " ", "", &available, + &length, &count) != SASL_OK || !length || count <= 0 || + !(copy = strdup(available))) return 0; + session->mechanisms[0] = '\0'; + for (token = strtok_r(copy, " ", &save); token; token = strtok_r(NULL, " ", &save)) { + size_t token_length = strlen(token); + /* The current Bongo callback can verify password mechanisms only. + * SCRAM/OAUTHBEARER/EXTERNAL are advertised once their credential + * callbacks and app-password policy are wired end to end. */ + if (strcmp(token, "PLAIN") && strcmp(token, "LOGIN")) continue; + if (used + token_length + (used ? 1 : 0) >= sizeof(session->mechanisms)) break; + if (used) session->mechanisms[used++] = ' '; + memcpy(session->mechanisms + used, token, token_length + 1); + used += token_length; + } + free(copy); + *mechanisms = session->mechanisms; + return used != 0; +} + +static int +exchange_result(int result, const char *sasl_output, unsigned sasl_length, + const void **output, size_t *output_length) +{ + if (output) *output = sasl_output; + if (output_length) *output_length = sasl_length; + if (result == SASL_OK) return BONGO_SASL_COMPLETE; + if (result == SASL_CONTINUE) return BONGO_SASL_CONTINUE; + return BONGO_SASL_FAIL; +} + +int +BongoSaslStart(BongoSaslSession *session, const char *mechanism, + const void *input, size_t input_length, + const void **output, size_t *output_length) +{ + const char *sasl_output = NULL; + unsigned sasl_length = 0; + int result; + if (!session || !mechanism || input_length > (size_t)UINT_MAX) return BONGO_SASL_FAIL; + result = sasl_server_start(session->connection, mechanism, input, + (unsigned)input_length, &sasl_output, &sasl_length); + return exchange_result(result, sasl_output, sasl_length, output, output_length); +} + +int +BongoSaslStep(BongoSaslSession *session, const void *input, size_t input_length, + const void **output, size_t *output_length) +{ + const char *sasl_output = NULL; + unsigned sasl_length = 0; + int result; + if (!session || input_length > (size_t)UINT_MAX) return BONGO_SASL_FAIL; + result = sasl_server_step(session->connection, input, (unsigned)input_length, + &sasl_output, &sasl_length); + return exchange_result(result, sasl_output, sasl_length, output, output_length); +} + +const char * +BongoSaslUsername(BongoSaslSession *session) +{ + const void *username = NULL; + if (!session || sasl_getprop(session->connection, SASL_USERNAME, &username) != SASL_OK) + return NULL; + return username; +} + +int +BongoSaslDecode64(const char *input, size_t input_length, void *output, + size_t output_capacity, size_t *output_length) +{ + unsigned decoded = 0; + if (!input || !output || !output_length || input_length > UINT_MAX || + output_capacity > UINT_MAX) return 0; + if (sasl_decode64(input, (unsigned)input_length, output, + (unsigned)output_capacity, &decoded) != SASL_OK) return 0; + *output_length = decoded; + return 1; +} + +int +BongoSaslEncode64(const void *input, size_t input_length, char *output, + size_t output_capacity, size_t *output_length) +{ + unsigned encoded = 0; + if (!input || !output || !output_length || input_length > UINT_MAX || + output_capacity > UINT_MAX) return 0; + if (sasl_encode64(input, (unsigned)input_length, output, + (unsigned)output_capacity, &encoded) != SASL_OK) return 0; + *output_length = encoded; + return 1; +} diff --git a/src/libs/sasl/tests/sasl-test.c b/src/libs/sasl/tests/sasl-test.c new file mode 100644 index 0000000..d47305e --- /dev/null +++ b/src/libs/sasl/tests/sasl-test.c @@ -0,0 +1,43 @@ +#include +#include + +static int verify(void *context, const char *user, const char *password, + size_t password_length) +{ + (void)context; + return !strcmp(user, "alice@example.test") && password_length == 6 && + !memcmp(password, "secret", 6); +} + +int main(void) +{ + static const char plain[] = "\0alice@example.test\0secret"; + BongoSaslSession *session = NULL; + const char *mechanisms = NULL; + const void *output = NULL; + size_t output_length = 0; + char encoded[128]; + char decoded[128]; + size_t encoded_length; + size_t decoded_length; + int result = 1; + if (!BongoSaslInitialize()) return 1; + if (!BongoSaslSessionCreate(&session, "sieve", "mail.example.test", + "127.0.0.1;4190", "127.0.0.1;12345", 1, + verify, NULL)) goto finish; + if (!BongoSaslMechanisms(session, &mechanisms) || !strstr(mechanisms, "PLAIN")) goto finish; + if (BongoSaslStart(session, "PLAIN", plain, sizeof(plain) - 1, + &output, &output_length) != BONGO_SASL_COMPLETE) goto finish; + if (!BongoSaslUsername(session) || strcmp(BongoSaslUsername(session), + "alice@example.test")) goto finish; + if (!BongoSaslEncode64(plain, sizeof(plain) - 1, encoded, sizeof(encoded), + &encoded_length) || + !BongoSaslDecode64(encoded, encoded_length, decoded, sizeof(decoded), + &decoded_length) || decoded_length != sizeof(plain) - 1 || + memcmp(decoded, plain, decoded_length)) goto finish; + result = 0; +finish: + BongoSaslSessionDestroy(&session); + BongoSaslShutdown(); + return result; +} diff --git a/src/libs/sieve/CMakeLists.txt b/src/libs/sieve/CMakeLists.txt new file mode 100644 index 0000000..366b179 --- /dev/null +++ b/src/libs/sieve/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libbongosieve sieve.c store.c managesieve.c) +set_target_properties(libbongosieve PROPERTIES OUTPUT_NAME bongosieve) +target_include_directories(libbongosieve PRIVATE ${MAILUTILS_INCLUDE_DIR}) +target_include_directories(libbongosieve PRIVATE ${SQLITE_INCLUDE_DIRS}) +target_link_libraries(libbongosieve PRIVATE ${MAILUTILS_LIBRARIES} ${SQLITE_LIBRARIES}) + +install(TARGETS libbongosieve DESTINATION ${LIB_INSTALL_DIR}) + +if(BUILD_TESTING) + add_executable(bongo-sieve-test tests/sieve-test.c tests/store-test.c + tests/managesieve-test.c) + target_link_libraries(bongo-sieve-test PRIVATE libbongosieve) + add_test(NAME sieve-validation COMMAND bongo-sieve-test) +endif() diff --git a/src/libs/sieve/managesieve.c b/src/libs/sieve/managesieve.c new file mode 100644 index 0000000..577e995 --- /dev/null +++ b/src/libs/sieve/managesieve.c @@ -0,0 +1,134 @@ +#include +#include +#include +#include +#include + +struct command_name { const char *name; BongoManageSieveCommand command; }; +static const struct command_name commands[] = { + {"AUTHENTICATE", BONGO_MANAGESIEVE_AUTHENTICATE}, + {"CAPABILITY", BONGO_MANAGESIEVE_CAPABILITY}, + {"CHECKSCRIPT", BONGO_MANAGESIEVE_CHECKSCRIPT}, + {"DELETESCRIPT", BONGO_MANAGESIEVE_DELETESCRIPT}, + {"GETSCRIPT", BONGO_MANAGESIEVE_GETSCRIPT}, + {"HAVESPACE", BONGO_MANAGESIEVE_HAVESPACE}, + {"LISTSCRIPTS", BONGO_MANAGESIEVE_LISTSCRIPTS}, + {"LOGOUT", BONGO_MANAGESIEVE_LOGOUT}, + {"NOOP", BONGO_MANAGESIEVE_NOOP}, + {"PUTSCRIPT", BONGO_MANAGESIEVE_PUTSCRIPT}, + {"RENAMESCRIPT", BONGO_MANAGESIEVE_RENAMESCRIPT}, + {"SETACTIVE", BONGO_MANAGESIEVE_SETACTIVE}, + {"STARTTLS", BONGO_MANAGESIEVE_STARTTLS} +}; + +static int +token(const char **cursor, const char *end, char *output, size_t capacity, + size_t *literal_length, int *nonsynchronizing) +{ + size_t used = 0; + while (*cursor < end && (**cursor == ' ' || **cursor == '\t')) (*cursor)++; + if (*cursor >= end) return 0; + if (**cursor == '"') { + (*cursor)++; + while (*cursor < end && **cursor != '"') { + char value = *(*cursor)++; + if (value == '\\') { + if (*cursor >= end || (**cursor != '\\' && **cursor != '"')) return -1; + value = *(*cursor)++; + } + if ((unsigned char)value < 0x20 || used + 1 >= capacity) return -1; + output[used++] = value; + } + if (*cursor >= end || *(*cursor)++ != '"') return -1; + } else if (**cursor == '{') { + char *number_end; + unsigned long number; + const char *start = ++*cursor; + if (start >= end || !isdigit((unsigned char)*start)) return -1; + number = strtoul(start, &number_end, 10); + if (number_end == start || number > 1024 * 1024 || number_end >= end) return -1; + *cursor = number_end; + if (**cursor == '+') { *nonsynchronizing = 1; (*cursor)++; } + if (*cursor >= end || *(*cursor)++ != '}') return -1; + *literal_length = (size_t)number; + output[0] = '\0'; + return 2; + } else { + while (*cursor < end && **cursor != ' ' && **cursor != '\t') { + unsigned char value = (unsigned char)*(*cursor)++; + if (value < 0x21 || value > 0x7e || value == '{' || value == '}' || + value == '"' || value == '\\' || used + 1 >= capacity) return -1; + output[used++] = (char)value; + } + } + output[used] = '\0'; + return 1; +} + +int +BongoManageSieveParse(const char *line, size_t length, BongoManageSieveRequest *request) +{ + const char *cursor; + const char *end; + char command[32]; + size_t index; + int result; + if (!line || !request || !length || length > 8192) return 0; + memset(request, 0, sizeof(*request)); + while (length && (line[length - 1] == '\r' || line[length - 1] == '\n')) length--; + cursor = line; + end = line + length; + result = token(&cursor, end, command, sizeof(command), &request->literal_length, + &request->literal_nonsynchronizing); + if (result != 1) return 0; + for (index = 0; index < sizeof(commands) / sizeof(commands[0]); index++) { + if (!strcasecmp(command, commands[index].name)) { + request->command = commands[index].command; + break; + } + } + if (request->command == BONGO_MANAGESIEVE_INVALID) return 0; + while (cursor < end) { + while (cursor < end && (*cursor == ' ' || *cursor == '\t')) cursor++; + if (cursor == end) break; + if (request->argument_count >= 3) return 0; + result = token(&cursor, end, request->arguments[request->argument_count], + sizeof(request->arguments[0]), &request->literal_length, + &request->literal_nonsynchronizing); + if (result < 1) return 0; + request->argument_count++; + if (result == 2 && cursor != end) return 0; + } + switch (request->command) { + case BONGO_MANAGESIEVE_CAPABILITY: + case BONGO_MANAGESIEVE_LISTSCRIPTS: + case BONGO_MANAGESIEVE_LOGOUT: + case BONGO_MANAGESIEVE_STARTTLS: + return request->argument_count == 0; + case BONGO_MANAGESIEVE_NOOP: + return request->argument_count <= 1 && request->literal_length == 0; + case BONGO_MANAGESIEVE_GETSCRIPT: + case BONGO_MANAGESIEVE_DELETESCRIPT: + case BONGO_MANAGESIEVE_SETACTIVE: + return request->argument_count == 1 && request->literal_length == 0; + case BONGO_MANAGESIEVE_RENAMESCRIPT: + return request->argument_count == 2 && request->literal_length == 0; + case BONGO_MANAGESIEVE_HAVESPACE: { + char *number_end; + unsigned long amount; + if (request->argument_count != 2 || request->literal_length) return 0; + amount = strtoul(request->arguments[1], &number_end, 10); + return *request->arguments[1] && !*number_end && amount <= 1024 * 1024; + } + case BONGO_MANAGESIEVE_PUTSCRIPT: + return request->argument_count == 2 && request->literal_length > 0; + case BONGO_MANAGESIEVE_CHECKSCRIPT: + return request->argument_count == 1 && request->literal_length > 0; + case BONGO_MANAGESIEVE_AUTHENTICATE: + return request->argument_count >= 1 && request->argument_count <= 2 && + request->literal_length == 0; + case BONGO_MANAGESIEVE_INVALID: + default: + return 0; + } +} diff --git a/src/libs/sieve/sieve.c b/src/libs/sieve/sieve.c new file mode 100644 index 0000000..168e579 --- /dev/null +++ b/src/libs/sieve/sieve.c @@ -0,0 +1,307 @@ +#include +#include +#include +#include +#include + +static int install_bongo_actions(mu_sieve_machine_t machine); + +static int +safe_requirement(const char *name, size_t length) +{ + static const char *allowed[] = { + "fileinto", "redirect", "reject", "envelope", "body", "variables", + "vacation", "vacation-seconds" + }; + size_t index; + for (index = 0; index < sizeof(allowed) / sizeof(allowed[0]); index++) { + if (strlen(allowed[index]) == length && + !memcmp(name, allowed[index], length)) return 1; + } + return 0; +} + +/* Mailutils supports loadable Sieve modules, including pipe/include-style + * facilities which must never be available to untrusted mailbox users. */ +static int +safe_requirements(const char *script, size_t length) +{ + size_t cursor = 0; + int in_require = 0; + int has_days = 0, has_seconds = 0; + while (cursor < length) { + if (script[cursor] == '#') { + while (cursor < length && script[cursor] != '\n') cursor++; + } else if (cursor + 1 < length && script[cursor] == '/' && script[cursor + 1] == '*') { + cursor += 2; + while (cursor + 1 < length && !(script[cursor] == '*' && script[cursor + 1] == '/')) cursor++; + if (cursor + 1 >= length) return 0; + cursor += 2; + } else if (script[cursor] == '"') { + size_t start = ++cursor; + while (cursor < length && script[cursor] != '"') { + if (script[cursor] == '\\' && ++cursor >= length) return 0; + cursor++; + } + if (cursor >= length) return 0; + if (in_require && !safe_requirement(script + start, cursor - start)) return 0; + cursor++; + } else if (script[cursor] == ':' && cursor + 5 < length) { + cursor++; + if (cursor + 4 <= length && !strncasecmp(script + cursor, "days", 4) && + (cursor + 4 == length || !isalnum((unsigned char)script[cursor + 4]))) { + has_days = 1; + } else if (cursor + 7 <= length && !strncasecmp(script + cursor, "seconds", 7) && + (cursor + 7 == length || !isalnum((unsigned char)script[cursor + 7]))) { + has_seconds = 1; + } + } else if ((script[cursor] >= 'A' && script[cursor] <= 'Z') || + (script[cursor] >= 'a' && script[cursor] <= 'z') || script[cursor] == '_') { + size_t start = cursor++; + while (cursor < length && ((script[cursor] >= 'A' && script[cursor] <= 'Z') || + (script[cursor] >= 'a' && script[cursor] <= 'z') || + (script[cursor] >= '0' && script[cursor] <= '9') || script[cursor] == '_')) cursor++; + if (cursor - start == 7 && !strncasecmp(script + start, "require", 7)) in_require = 1; + } else { + if (in_require && script[cursor] == ';') in_require = 0; + cursor++; + } + } + return !in_require && !(has_days && has_seconds); +} + +static char * +normalize_script(const char *script, size_t length, size_t *normalized_length) +{ + char *normalized; + size_t input, output = 0; + if (!script || !length || length > 1024 * 1024 || + !safe_requirements(script, length)) return NULL; + normalized = malloc(length + 1); + if (!normalized) return NULL; + for (input = 0; input < length; input++) { + if (script[input] == '\0') { free(normalized); return NULL; } + if (script[input] == '\r') { + if (input + 1 >= length || script[input + 1] != '\n') { + free(normalized); + return NULL; + } + continue; + } + normalized[output++] = script[input]; + } + normalized[output] = '\0'; + *normalized_length = output; + return normalized; +} + +int +BongoSieveValidate(const char *script, size_t length) +{ + mu_sieve_machine_t machine = NULL; + struct mu_locus_point origin = MU_LOCUS_POINT_INITIALIZER; + char *normalized; + size_t output = 0; + int result; + + normalized = normalize_script(script, length, &output); + if (!normalized) return 0; + if (mu_sieve_machine_create(&machine) != 0 || !machine) { + free(normalized); + return 0; + } + if (!install_bongo_actions(machine)) { + mu_sieve_machine_destroy(&machine); + free(normalized); + return 0; + } + mu_sieve_set_dry_run(machine, 1); + origin.mu_file = "bongo-upload.sieve"; + origin.mu_line = 1; + origin.mu_col = 1; + result = mu_sieve_compile_text(machine, normalized, output, &origin); + mu_sieve_machine_destroy(&machine); + free(normalized); + return result == 0; +} + +struct evaluation_context { + BongoSieveActionCallback callback; + void *data; + int valid; + BongoSieveResult actions[256]; + size_t action_count; +}; + +static int +queue_action(struct evaluation_context *context, const BongoSieveResult *result) +{ + if (!context || !context->valid || context->action_count >= 256) return 0; + context->actions[context->action_count++] = *result; + return 1; +} + +static int +report_action(mu_sieve_machine_t machine, BongoSieveAction action, + int has_argument) +{ + struct evaluation_context *context = mu_sieve_get_data(machine); + BongoSieveResult result = { .action = action, .days = 7 }; + if (!context || !context->valid) return 1; + if (has_argument) mu_sieve_get_arg(machine, 0, SVT_STRING, &result.argument); + if ((has_argument && (!result.argument || !*result.argument)) || + !queue_action(context, &result)) { + context->valid = 0; + return 1; + } + return 0; +} + +static int action_keep(mu_sieve_machine_t m) { return report_action(m, BONGO_SIEVE_KEEP, 0); } +static int action_discard(mu_sieve_machine_t m) { return report_action(m, BONGO_SIEVE_DISCARD, 0); } +static int action_fileinto(mu_sieve_machine_t m) { return report_action(m, BONGO_SIEVE_FILEINTO, 1); } +static int action_redirect(mu_sieve_machine_t m) { return report_action(m, BONGO_SIEVE_REDIRECT, 1); } +static int action_reject(mu_sieve_machine_t m) { return report_action(m, BONGO_SIEVE_REJECT, 1); } + +static int +vacation_tag_checker(mu_sieve_machine_t machine) +{ + size_t arguments = 0, tags = 0, index; + int days = 0, seconds = 0; + mu_sieve_get_argc(machine, &arguments, &tags); + (void)arguments; + for (index = 0; index < tags; index++) { + mu_sieve_value_t *tag = mu_sieve_get_tag_n(machine, index); + if (tag && tag->tag) { + if (!strcmp(tag->tag, "days")) days = 1; + else if (!strcmp(tag->tag, "seconds")) seconds = 1; + } + } + return days && seconds; +} + +static int +action_vacation(mu_sieve_machine_t machine) +{ + struct evaluation_context *context = mu_sieve_get_data(machine); + BongoSieveResult result = { .action = BONGO_SIEVE_VACATION, .days = 7 }; + size_t days = 7; + size_t seconds = 0; + mu_sieve_value_t *addresses; + size_t index; + if (!context || !context->valid) return 1; + mu_sieve_get_arg(machine, 0, SVT_STRING, &result.argument); + if (mu_sieve_get_tag(machine, "days", SVT_NUMBER, &days)) result.days = (unsigned long)days; + mu_sieve_get_tag(machine, "subject", SVT_STRING, &result.subject); + mu_sieve_get_tag(machine, "from", SVT_STRING, &result.from); + mu_sieve_get_tag(machine, "handle", SVT_STRING, &result.handle); + addresses = mu_sieve_get_tag_untyped(machine, "addresses"); + if (addresses && addresses->type == SVT_STRING_LIST) { + result.address_count = addresses->v.list.count; + if (result.address_count > 32) result.address_count = 32; + for (index = 0; index < result.address_count; index++) { + result.addresses[index] = mu_sieve_string(machine, &addresses->v.list, index); + } + } + if (mu_sieve_get_tag(machine, "seconds", SVT_NUMBER, &seconds)) { + result.seconds = (unsigned long)seconds; + result.has_seconds = 1; + } + result.mime = mu_sieve_get_tag(machine, "mime", SVT_VOID, NULL) != 0; + if (!result.argument || !*result.argument || result.days > 365 || + result.seconds > 366UL * 86400UL || + (result.has_seconds && mu_sieve_get_tag_untyped(machine, "days")) || + !queue_action(context, &result)) { + context->valid = 0; + return 1; + } + return 0; +} + +static int +install_bongo_actions(mu_sieve_machine_t machine) +{ + static mu_sieve_data_type string_argument[] = { SVT_STRING, SVT_VOID }; + static mu_sieve_data_type no_arguments[] = { SVT_VOID }; + static mu_sieve_tag_def_t vacation_tags[] = { + { "days", SVT_NUMBER }, { "subject", SVT_STRING }, + { "from", SVT_STRING }, { "addresses", SVT_STRING_LIST }, + { "mime", SVT_VOID }, { "handle", SVT_STRING }, + { "seconds", SVT_NUMBER }, { NULL, SVT_VOID } + }; + static mu_sieve_tag_group_t vacation_groups[] = { + { vacation_tags, vacation_tag_checker }, { NULL, NULL } + }; + mu_sieve_register_action(machine, "keep", action_keep, + no_arguments, NULL, 1); + mu_sieve_register_action(machine, "discard", action_discard, + no_arguments, NULL, 1); + mu_sieve_register_action(machine, "fileinto", action_fileinto, + string_argument, NULL, 0); + mu_sieve_register_action(machine, "redirect", action_redirect, + string_argument, NULL, 0); + mu_sieve_register_action(machine, "reject", action_reject, + string_argument, NULL, 0); + mu_sieve_register_action(machine, "vacation", action_vacation, + string_argument, vacation_groups, 1); + /* RFC 6131 uses this name only as a require/capability identifier. */ + mu_sieve_register_action(machine, "vacation-seconds", action_vacation, + string_argument, vacation_groups, 0); + return mu_sieve_registry_lookup(machine, "fileinto", mu_sieve_record_action) && + mu_sieve_registry_lookup(machine, "redirect", mu_sieve_record_action) && + mu_sieve_registry_lookup(machine, "reject", mu_sieve_record_action) && + mu_sieve_registry_lookup(machine, "vacation", mu_sieve_record_action) && + mu_sieve_registry_lookup(machine, "vacation-seconds", mu_sieve_record_action); +} + +int +BongoSieveEvaluate(const char *script, size_t script_length, + const char *message, size_t message_length, + BongoSieveActionCallback callback, void *data) +{ + mu_sieve_machine_t machine = NULL; + mu_message_t mail = NULL; + mu_stream_t stream = NULL; + struct mu_locus_point origin = MU_LOCUS_POINT_INITIALIZER; + struct evaluation_context context = { .callback = callback, .data = data, .valid = 1 }; + char *normalized; + size_t normalized_length = 0; + int result = 0; + if (!message || !message_length || !callback) return 0; + normalized = normalize_script(script, script_length, &normalized_length); + if (!normalized) return 0; + origin.mu_file = "bongo-active.sieve"; + origin.mu_line = 1; + origin.mu_col = 1; + if (mu_sieve_machine_create(&machine) != 0 || !machine || + mu_static_memory_stream_create(&stream, message, message_length) != 0 || + mu_message_create(&mail, NULL) != 0 || + mu_message_set_stream(mail, stream, NULL) != 0) goto finish; + mu_sieve_set_data(machine, &context); + if (!install_bongo_actions(machine) || + mu_sieve_compile_text(machine, normalized, normalized_length, &origin) != 0) goto finish; + if (mu_sieve_message(machine, mail) == 0 && context.valid) { + size_t index; + int reject_count = 0, has_delivery = 0, has_vacation = 0; + for (index = 0; index < context.action_count; index++) { + BongoSieveAction action = context.actions[index].action; + if (action == BONGO_SIEVE_REJECT) reject_count++; + if (action == BONGO_SIEVE_VACATION) has_vacation = 1; + if (action == BONGO_SIEVE_KEEP || action == BONGO_SIEVE_FILEINTO || + action == BONGO_SIEVE_REDIRECT) has_delivery = 1; + } + if (reject_count > 1 || (reject_count && (has_delivery || has_vacation))) { + context.valid = 0; + } + for (index = 0; context.valid && index < context.action_count; index++) { + if (!callback(&context.actions[index], data)) context.valid = 0; + } + if (context.valid) result = 1; + } +finish: + if (mail) mu_message_destroy(&mail, NULL); + if (stream) mu_stream_unref(stream); + if (machine) mu_sieve_machine_destroy(&machine); + free(normalized); + return result; +} diff --git a/src/libs/sieve/store.c b/src/libs/sieve/store.c new file mode 100644 index 0000000..b2ce1c1 --- /dev/null +++ b/src/libs/sieve/store.c @@ -0,0 +1,260 @@ +#include +#include +#include +#include + +struct BongoSieveStore { + sqlite3 *database; +}; + +static int +valid_identity(const char *value, size_t maximum, int script_name) +{ + size_t length; + const unsigned char *cursor; + if (!value || !(length = strlen(value)) || length > maximum) return 0; + for (cursor = (const unsigned char *)value; *cursor; cursor++) { + if (*cursor < 0x20 || *cursor == 0x7f || (script_name && (*cursor == '/' || *cursor == '\\'))) { + return 0; + } + } + return 1; +} + +int +BongoSieveStoreOpen(BongoSieveStore **output, const char *path) +{ + BongoSieveStore *store; + static const char schema[] = + "PRAGMA journal_mode=WAL;" + "PRAGMA foreign_keys=ON;" + "CREATE TABLE IF NOT EXISTS sieve_scripts (" + " user TEXT NOT NULL, name TEXT NOT NULL, script BLOB NOT NULL," + " active INTEGER NOT NULL DEFAULT 0 CHECK(active IN (0,1))," + " revision INTEGER NOT NULL DEFAULT 1, updated_at INTEGER NOT NULL," + " PRIMARY KEY(user,name));" + "CREATE UNIQUE INDEX IF NOT EXISTS sieve_one_active " + " ON sieve_scripts(user) WHERE active=1;" + "CREATE TABLE IF NOT EXISTS sieve_vacation_replies (" + " user TEXT NOT NULL, sender TEXT NOT NULL, handle TEXT NOT NULL," + " replied_at INTEGER NOT NULL, PRIMARY KEY(user,sender,handle));"; + if (!output || !path || !*path) return 0; + *output = NULL; + store = calloc(1, sizeof(*store)); + if (!store) return 0; + if (sqlite3_open_v2(path, &store->database, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL) != SQLITE_OK || + sqlite3_busy_timeout(store->database, 5000) != SQLITE_OK || + sqlite3_exec(store->database, schema, NULL, NULL, NULL) != SQLITE_OK) { + BongoSieveStoreClose(&store); + return 0; + } + *output = store; + return 1; +} + +void +BongoSieveStoreClose(BongoSieveStore **store) +{ + if (!store || !*store) return; + if ((*store)->database) sqlite3_close((*store)->database); + free(*store); + *store = NULL; +} + +int +BongoSieveStorePut(BongoSieveStore *store, const char *user, + const char *name, const char *script, size_t length) +{ + sqlite3_stmt *statement = NULL; + int result = 0; + static const char sql[] = + "INSERT INTO sieve_scripts(user,name,script,updated_at) " + "VALUES(?,?,?,unixepoch()) ON CONFLICT(user,name) DO UPDATE SET " + "script=excluded.script, revision=revision+1, updated_at=unixepoch()"; + if (!store || !valid_identity(user, 320, 0) || + !valid_identity(name, 128, 1) || !BongoSieveValidate(script, length)) return 0; + if (sqlite3_prepare_v2(store->database, sql, -1, &statement, NULL) == SQLITE_OK && + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 2, name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_blob(statement, 3, script, (int)length, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_DONE) result = 1; + sqlite3_finalize(statement); + return result; +} + +int +BongoSieveStoreGet(BongoSieveStore *store, const char *user, + const char *name, char **script, size_t *length, int *active) +{ + sqlite3_stmt *statement = NULL; + const void *data; + int bytes; + int result = 0; + const char *sql = name ? + "SELECT script,active FROM sieve_scripts WHERE user=? AND name=?" : + "SELECT script,active FROM sieve_scripts WHERE user=? AND active=1"; + if (!store || !script || !length || !valid_identity(user, 320, 0) || + (name && !valid_identity(name, 128, 1))) return 0; + *script = NULL; + *length = 0; + if (sqlite3_prepare_v2(store->database, sql, -1, &statement, NULL) != SQLITE_OK || + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) != SQLITE_OK || + (name && sqlite3_bind_text(statement, 2, name, -1, SQLITE_TRANSIENT) != SQLITE_OK) || + sqlite3_step(statement) != SQLITE_ROW) goto finish; + data = sqlite3_column_blob(statement, 0); + bytes = sqlite3_column_bytes(statement, 0); + *script = malloc((size_t)bytes + 1); + if (!*script) goto finish; + memcpy(*script, data, (size_t)bytes); + (*script)[bytes] = '\0'; + *length = (size_t)bytes; + if (active) *active = sqlite3_column_int(statement, 1); + result = 1; +finish: + sqlite3_finalize(statement); + return result; +} + +int +BongoSieveStoreSetActive(BongoSieveStore *store, const char *user, const char *name) +{ + sqlite3_stmt *statement = NULL; + int result = 0; + if (!store || !valid_identity(user, 320, 0) || + (name && *name && !valid_identity(name, 128, 1))) return 0; + if (sqlite3_exec(store->database, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) return 0; + if (sqlite3_prepare_v2(store->database, + "UPDATE sieve_scripts SET active=0 WHERE user=?", -1, + &statement, NULL) == SQLITE_OK && + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_DONE) result = 1; + sqlite3_finalize(statement); + statement = NULL; + if (result && name && *name) { + result = 0; + if (sqlite3_prepare_v2(store->database, + "UPDATE sieve_scripts SET active=1 WHERE user=? AND name=?", + -1, &statement, NULL) == SQLITE_OK && + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 2, name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_DONE && + sqlite3_changes(store->database) == 1) result = 1; + sqlite3_finalize(statement); + } + sqlite3_exec(store->database, result ? "COMMIT" : "ROLLBACK", NULL, NULL, NULL); + return result; +} + +int +BongoSieveStoreDelete(BongoSieveStore *store, const char *user, const char *name) +{ + sqlite3_stmt *statement = NULL; + int result = 0; + if (!store || !valid_identity(user, 320, 0) || !valid_identity(name, 128, 1)) return 0; + if (sqlite3_prepare_v2(store->database, + "DELETE FROM sieve_scripts WHERE user=? AND name=? AND active=0", + -1, &statement, NULL) == SQLITE_OK && + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 2, name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(store->database) == 1) result = 1; + sqlite3_finalize(statement); + return result; +} + +int +BongoSieveStoreRename(BongoSieveStore *store, const char *user, + const char *old_name, const char *new_name) +{ + sqlite3_stmt *statement = NULL; + int result = 0; + if (!store || !valid_identity(user, 320, 0) || + !valid_identity(old_name, 128, 1) || !valid_identity(new_name, 128, 1) || + strcmp(old_name, new_name) == 0) return 0; + if (sqlite3_prepare_v2(store->database, + "UPDATE sieve_scripts SET name=?, revision=revision+1, " + "updated_at=unixepoch() WHERE user=? AND name=? AND NOT EXISTS " + "(SELECT 1 FROM sieve_scripts WHERE user=? AND name=?)", + -1, &statement, NULL) == SQLITE_OK && + sqlite3_bind_text(statement, 1, new_name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 2, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 3, old_name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 4, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 5, new_name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(store->database) == 1) { + result = 1; + } + sqlite3_finalize(statement); + return result; +} + +int +BongoSieveVacationClaim(BongoSieveStore *store, const char *user, + const char *sender, const char *handle, + unsigned long interval_seconds) +{ + sqlite3_stmt *statement = NULL; + int result = 0; + static const char sql[] = + "INSERT INTO sieve_vacation_replies(user,sender,handle,replied_at) " + "VALUES(?,?,?,unixepoch()) ON CONFLICT(user,sender,handle) DO UPDATE SET " + "replied_at=unixepoch() WHERE replied_at <= unixepoch()-?"; + if (!store || !valid_identity(user, 320, 0) || + !valid_identity(sender, 320, 0) || !valid_identity(handle, 128, 1) || + interval_seconds > 366UL * 86400UL) return 0; + if (sqlite3_prepare_v2(store->database, sql, -1, &statement, NULL) == SQLITE_OK && + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 2, sender, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 3, handle, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_int64(statement, 4, (sqlite3_int64)interval_seconds) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(store->database) == 1) { + result = 1; + } + sqlite3_finalize(statement); + return result; +} + +void +BongoSieveVacationRelease(BongoSieveStore *store, const char *user, + const char *sender, const char *handle) +{ + sqlite3_stmt *statement = NULL; + if (!store || !user || !sender || !handle) return; + if (sqlite3_prepare_v2(store->database, + "DELETE FROM sieve_vacation_replies WHERE user=? AND sender=? AND handle=?", + -1, &statement, NULL) == SQLITE_OK) { + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 2, sender, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 3, handle, -1, SQLITE_TRANSIENT); + sqlite3_step(statement); + } + sqlite3_finalize(statement); +} + +int +BongoSieveStoreList(BongoSieveStore *store, const char *user, + BongoSieveListCallback callback, void *data) +{ + sqlite3_stmt *statement = NULL; + int result = 0; + if (!store || !callback || !valid_identity(user, 320, 0)) return 0; + if (sqlite3_prepare_v2(store->database, + "SELECT name,active FROM sieve_scripts WHERE user=? ORDER BY name", + -1, &statement, NULL) != SQLITE_OK || + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) != SQLITE_OK) { + sqlite3_finalize(statement); + return 0; + } + for (;;) { + int step = sqlite3_step(statement); + if (step == SQLITE_DONE) { + result = 1; + break; + } + if (step != SQLITE_ROW || !callback( + (const char *)sqlite3_column_text(statement, 0), + sqlite3_column_int(statement, 1), data)) break; + } + sqlite3_finalize(statement); + return result; +} diff --git a/src/libs/sieve/tests/managesieve-test.c b/src/libs/sieve/tests/managesieve-test.c new file mode 100644 index 0000000..02534ee --- /dev/null +++ b/src/libs/sieve/tests/managesieve-test.c @@ -0,0 +1,27 @@ +#include +#include + +int BongoManageSieveProtocolTest(void) +{ + BongoManageSieveRequest request; + const char put[] = "PUTSCRIPT \"Family rules\" {123+}\r\n"; + const char bad[] = "PUTSCRIPT \"x\\ny\" {9999999}\r\n"; + const char capability_extra[] = "CAPABILITY extra\r\n"; + const char put_quoted[] = "PUTSCRIPT \"main\" \"not-a-literal\"\r\n"; + const char havespace[] = "HAVESPACE \"main\" 1048576\r\n"; + const char havespace_bad[] = "HAVESPACE \"main\" -1\r\n"; + if (!BongoManageSieveParse(put, strlen(put), &request) || + request.command != BONGO_MANAGESIEVE_PUTSCRIPT || + request.argument_count != 2 || strcmp(request.arguments[0], "Family rules") || + request.literal_length != 123 || !request.literal_nonsynchronizing) return 20; + if (BongoManageSieveParse(bad, strlen(bad), &request)) return 21; + if (!BongoManageSieveParse("CAPABILITY\r\n", 12, &request) || + request.command != BONGO_MANAGESIEVE_CAPABILITY || request.argument_count) return 22; + if (BongoManageSieveParse(capability_extra, strlen(capability_extra), + &request)) return 23; + if (BongoManageSieveParse(put_quoted, strlen(put_quoted), &request)) return 24; + if (!BongoManageSieveParse(havespace, strlen(havespace), &request)) return 25; + if (BongoManageSieveParse(havespace_bad, strlen(havespace_bad), + &request)) return 26; + return 0; +} diff --git a/src/libs/sieve/tests/sieve-test.c b/src/libs/sieve/tests/sieve-test.c new file mode 100644 index 0000000..f9c3242 --- /dev/null +++ b/src/libs/sieve/tests/sieve-test.c @@ -0,0 +1,128 @@ +#include +#include + +int BongoSieveStoreTest(void); +int BongoManageSieveProtocolTest(void); + +struct action_result { + BongoSieveAction action; + char argument[64]; + char subject[64]; + unsigned long days; + unsigned long seconds; + int has_seconds; + int count; +}; +static int capture_action(const BongoSieveResult *action, void *data) +{ + struct action_result *result = data; + result->action = action->action; + result->count++; + if (action->argument) { + strncpy(result->argument, action->argument, sizeof(result->argument) - 1); + result->argument[sizeof(result->argument) - 1] = '\0'; + } + if (action->subject) { + strncpy(result->subject, action->subject, sizeof(result->subject) - 1); + result->subject[sizeof(result->subject) - 1] = '\0'; + } + result->days = action->days; + result->seconds = action->seconds; + result->has_seconds = action->has_seconds; + return 1; +} + +int +main(void) +{ + static const char valid[] = + "require [\"fileinto\"];\n" + "if address :domain :is \"from\" \"family.example\" {\n" + " fileinto \"Family\";\n" + " stop;\n" + "}\n"; + static const char invalid[] = "if header { discard;\n"; + static const char crlf[] = "if true { keep; }\r\n"; + static const char bare_cr[] = "if true { keep; }\r"; + static const char dangerous[] = "require [\"pipe\"]; pipe \"/bin/true\";\n"; + + if (!BongoSieveValidate(valid, strlen(valid))) { + return 1; + } + if (BongoSieveValidate(invalid, strlen(invalid))) { + return 2; + } + if (BongoSieveValidate(valid, 1024 * 1024 + 1)) { + return 3; + } + if (!BongoSieveValidate(crlf, strlen(crlf))) { + return 4; + } + if (BongoSieveValidate(bare_cr, strlen(bare_cr))) { + return 5; + } + if (BongoSieveValidate(dangerous, strlen(dangerous))) return 7; + { + static const char message[] = + "From: sender@family.example\r\nTo: user@example.test\r\n" + "Subject: family note\r\n\r\nHello\r\n"; + struct action_result action = { 0 }; + if (!BongoSieveEvaluate(valid, strlen(valid), message, strlen(message), + capture_action, &action) || + action.count != 1 || action.action != BONGO_SIEVE_FILEINTO || + strcmp(action.argument, "Family")) return 6; + } + { + static const char vacation[] = + "require \"vacation\";\n" + "vacation :days 3 :subject \"Away\" \"Back soon\";\n"; + static const char message[] = + "From: friend@example.test\r\nTo: user@example.test\r\n\r\nHello\r\n"; + struct action_result action = { 0 }; + if (!BongoSieveValidate(vacation, strlen(vacation)) || + !BongoSieveEvaluate(vacation, strlen(vacation), message, strlen(message), + capture_action, &action) || + action.count != 1 || action.action != BONGO_SIEVE_VACATION || + action.days != 3 || strcmp(action.subject, "Away") || + strcmp(action.argument, "Back soon")) return 8; + } + { + static const char reject[] = + "require \"reject\"; reject \"Policy refusal\";\n"; + static const char double_reject[] = + "require \"reject\"; reject \"one\"; reject \"two\";\n"; + static const char reject_keep[] = + "require \"reject\"; reject \"no\"; keep;\n"; + static const char message[] = + "From: sender@example.test\r\nTo: user@example.test\r\n\r\nHello\r\n"; + struct action_result action = { 0 }; + if (!BongoSieveValidate(reject, strlen(reject)) || + !BongoSieveEvaluate(reject, strlen(reject), message, strlen(message), + capture_action, &action) || + action.count != 1 || action.action != BONGO_SIEVE_REJECT || + BongoSieveEvaluate(double_reject, strlen(double_reject), message, + strlen(message), capture_action, &action) || + BongoSieveEvaluate(reject_keep, strlen(reject_keep), message, + strlen(message), capture_action, &action)) return 10; + } + { + static const char vacation[] = + "require \"vacation-seconds\";\n" + "vacation :seconds 7200 \"Short break\";\n"; + static const char invalid[] = + "require \"vacation-seconds\"; vacation :days 1 :seconds 1 \"x\";\n"; + static const char message[] = + "From: friend@example.test\r\nTo: user@example.test\r\n\r\nHello\r\n"; + struct action_result action = { 0 }; + if (!BongoSieveValidate(vacation, strlen(vacation)) || + BongoSieveValidate(invalid, strlen(invalid)) || + !BongoSieveEvaluate(vacation, strlen(vacation), message, strlen(message), + capture_action, &action) || + action.action != BONGO_SIEVE_VACATION || !action.has_seconds || + action.seconds != 7200) return 9; + } + { + int result = BongoSieveStoreTest(); + return result ? result : BongoManageSieveProtocolTest(); + } +} diff --git a/src/libs/sieve/tests/store-test.c b/src/libs/sieve/tests/store-test.c new file mode 100644 index 0000000..4b2665a --- /dev/null +++ b/src/libs/sieve/tests/store-test.c @@ -0,0 +1,66 @@ +#include +#include +#include +#include + +static int store_test(void) +{ + char path[] = "/tmp/bongo-sieve-store-XXXXXX"; + const char script[] = "if true { keep; }\r\n"; + BongoSieveStore *store = NULL; + char *loaded = NULL; + size_t length = 0; + int active = 0; + int fd = mkstemp(path); + if (fd < 0) return 10; + close(fd); + if (!BongoSieveStoreOpen(&store, path) || + !BongoSieveStorePut(store, "alice@example.test", "main", script, strlen(script)) || + !BongoSieveStoreSetActive(store, "alice@example.test", "main") || + !BongoSieveStoreGet(store, "alice@example.test", NULL, &loaded, &length, &active) || + length != strlen(script) || memcmp(loaded, script, length) || !active || + BongoSieveStoreSetActive(store, "alice@example.test", "missing") || + BongoSieveStoreDelete(store, "alice@example.test", "main")) { + free(loaded); + BongoSieveStoreClose(&store); + unlink(path); + return 11; + } + free(loaded); + BongoSieveStoreSetActive(store, "alice@example.test", ""); + if (!BongoSieveStoreDelete(store, "alice@example.test", "main")) { + BongoSieveStoreClose(&store); + unlink(path); + return 12; + } + if (!BongoSieveStorePut(store, "alice@example.test", "old", script, strlen(script)) || + !BongoSieveStoreRename(store, "alice@example.test", "old", "renamed") || + BongoSieveStoreGet(store, "alice@example.test", "old", &loaded, &length, &active) || + !BongoSieveStoreGet(store, "alice@example.test", "renamed", &loaded, &length, &active)) { + free(loaded); + BongoSieveStoreClose(&store); + unlink(path); + return 13; + } + free(loaded); + if (!BongoSieveVacationClaim(store, "alice@example.test", "bob@example.test", + "holiday", 86400) || + BongoSieveVacationClaim(store, "alice@example.test", "bob@example.test", + "holiday", 86400)) { + BongoSieveStoreClose(&store); + unlink(path); + return 14; + } + BongoSieveVacationRelease(store, "alice@example.test", "bob@example.test", "holiday"); + if (!BongoSieveVacationClaim(store, "alice@example.test", "bob@example.test", + "holiday", 86400)) { + BongoSieveStoreClose(&store); + unlink(path); + return 15; + } + BongoSieveStoreClose(&store); + unlink(path); + return 0; +} + +int BongoSieveStoreTest(void) { return store_test(); } diff --git a/src/libs/streamio/bongostream.c b/src/libs/streamio/bongostream.c index 98c00fd..0d2f92d 100644 --- a/src/libs/streamio/bongostream.c +++ b/src/libs/streamio/bongostream.c @@ -52,6 +52,7 @@ Examine(const char *data, int size) static int StreamToMemory(StreamStruct *codec, StreamStruct *next) { + UNUSED_PARAMETER(next); #if DEBUG_BONGOSTREAM printf("appending %lu bytes\n", codec->Len); Examine(codec->Start, codec->Len); @@ -77,7 +78,7 @@ BongoStreamInsertCodec(BongoStream *stream, const char *name, BOOL encode, Strea printf("looking for %s\n", name); #endif - codec = FindCodec((char *)name, encode); + codec = FindCodec(name, encode); if (codec) { diff --git a/src/libs/streamio/codec/big5.h b/src/libs/streamio/codec/big5.h index a441692..d9a6c54 100644 --- a/src/libs/streamio/codec/big5.h +++ b/src/libs/streamio/codec/big5.h @@ -4188,7 +4188,7 @@ BIG5_Encode(StreamStruct *Codec, StreamStruct *NextCodec) UTF8DecodeChar(In, wc, Len, Codec->Len); - if (wc >= 0x0000 && wc < 0x0100) { + if (wc < 0x0100) { summary = &big5_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x0200 && wc < 0x0460) { summary = &big5_uni2indx_page02[(wc>>4)-0x020]; diff --git a/src/libs/streamio/codec/euc_kr.h b/src/libs/streamio/codec/euc_kr.h index 27f97b9..97778e2 100644 --- a/src/libs/streamio/codec/euc_kr.h +++ b/src/libs/streamio/codec/euc_kr.h @@ -120,7 +120,7 @@ EUC_KR_Encode(StreamStruct *Codec, StreamStruct *NextCodec) UTF8DecodeChar(In, wc, Len, Codec->Len); - if (wc >= 0x0000 && wc < 0x0460) { + if (wc < 0x0460) { summary = &KSC5601_CP00[(wc>>4)]; } else if (wc >= 0x2000 && wc < 0x2670) { summary = &KSC5601_CP20[(wc>>4)-0x200]; diff --git a/src/libs/streamio/codec/euc_tw.h b/src/libs/streamio/codec/euc_tw.h index ee6a319..881b69c 100644 --- a/src/libs/streamio/codec/euc_tw.h +++ b/src/libs/streamio/codec/euc_tw.h @@ -179,7 +179,7 @@ EUC_TW_Encode(StreamStruct *Codec, StreamStruct *NextCodec) UTF8DecodeChar(In, wc, Len, Codec->Len); - if (wc >= 0x0000 && wc < 0x0100) { + if (wc < 0x0100) { summary = &cns11643_inv_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x0200 && wc < 0x03d0) { summary = &cns11643_inv_uni2indx_page02[(wc>>4)-0x020]; diff --git a/src/libs/streamio/codec/gb2312.h b/src/libs/streamio/codec/gb2312.h index 7db04b2..f87bd0a 100644 --- a/src/libs/streamio/codec/gb2312.h +++ b/src/libs/streamio/codec/gb2312.h @@ -2601,7 +2601,7 @@ GB2312_Encode(StreamStruct *Codec, StreamStruct *NextCodec) UTF8DecodeChar(In, wc, Len, Codec->Len); - if (wc >= 0x0000 && wc < 0x0460) { + if (wc < 0x0460) { summary = &gb2312_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x2000 && wc < 0x2650) { summary = &gb2312_uni2indx_page20[(wc>>4)-0x200]; diff --git a/src/libs/streamio/codec/html.h b/src/libs/streamio/codec/html.h index 3b5e2a3..8d38867 100644 --- a/src/libs/streamio/codec/html.h +++ b/src/libs/streamio/codec/html.h @@ -103,7 +103,7 @@ #define CODEC_HTML_SKIP_META_CHARSET_PRE_VALUE 42 #define CODEC_HTML_SKIP_META_CHARSET_VALUE 43 -unsigned char *StateString[] = { +static const char *const StateString[] = { "CODEC_HTML_SEND_CONTENT", "CODEC_HTML_SEND_TAG_EXAMINE", "CODEC_HTML_SEND_TAG", @@ -151,19 +151,19 @@ unsigned char *StateString[] = { }; typedef struct { - unsigned char *name; + const char *name; unsigned long state; } HtmlAttributeStruct; typedef struct { - unsigned char *name; + const char *name; unsigned long nameLen; unsigned long state; } HtmlAttributeInfoStruct; HtmlAttributeInfoStruct *HtmlAttributeInfo = NULL; -HtmlAttributeStruct HtmlAttribute[] = { +static const HtmlAttributeStruct HtmlAttribute[] = { {"href ", CODEC_HTML_SEND_TAG_URL_NAME}, {"href\t", CODEC_HTML_SEND_TAG_URL_NAME}, {"href=", CODEC_HTML_SEND_TAG_URL_NAME}, @@ -231,14 +231,14 @@ HtmlAttributeStruct HtmlAttribute[] = { }; typedef struct { - unsigned char *name; - unsigned char *endSupressTag; + const char *name; + const char *endSupressTag; unsigned long sendState; /* next state if the current mode is SEND */ unsigned long supressState; /* next state if the current mode is SUPPRESS */ unsigned long endSupressState; /* next state if the current mode is SUPPRESS and end tag is being processed */ } HtmlTagsStruct; -HtmlTagsStruct HtmlTag[] = { +static const HtmlTagsStruct HtmlTag[] = { {"", CODEC_HTML_SUPRESS_TAG, CODEC_HTML_SUPRESS_TAG, CODEC_HTML_SKIP_TAG}, @@ -264,7 +264,7 @@ HtmlTagsStruct HtmlTag[] = { }; typedef struct { - unsigned char *name; + const char *name; unsigned long nameLen; long endId; unsigned long sendState; @@ -402,6 +402,7 @@ HtmlDecodeFlushOutStream(unsigned long Space, unsigned char **Out, unsigned char __inline static void HtmlDecodeFlushNext(StreamStruct *Codec, StreamStruct *NextCodec, unsigned long Len, unsigned char *Out, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); if (NextCodec->Len > 0) { if (Codec->EOS) { if (Len == Codec->Len) { @@ -436,6 +437,7 @@ HtmlDecodeFlushNext(StreamStruct *Codec, StreamStruct *NextCodec, unsigned long __inline static void CodecCopyChar(unsigned char **In, unsigned char **Out, unsigned long *Len, StreamStruct *NextCodec, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); CopyCharDebugLog(debugHandle, In); (*Out)[0] = (*In)[0]; (*Out)++; @@ -447,6 +449,7 @@ CodecCopyChar(unsigned char **In, unsigned char **Out, unsigned long *Len, Strea __inline static void CodecSkipChar(unsigned char **In, unsigned long *Len, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); SkipCharDebugLog(debugHandle, In); (*In)++; (*Len)++; @@ -455,6 +458,7 @@ CodecSkipChar(unsigned char **In, unsigned long *Len, FILE *debugHandle) __inline static void CodecInsertChar(unsigned char newChar, unsigned char **Out, StreamStruct *NextCodec, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); InsertCharDebugLog(debugHandle, newChar); (*Out)[0] = newChar; (*Out)++; @@ -464,6 +468,7 @@ CodecInsertChar(unsigned char newChar, unsigned char **Out, StreamStruct *NextCo __inline static void CodecCopyChars(unsigned long size, unsigned char **In, unsigned char **Out, unsigned long *Len, StreamStruct *NextCodec, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); CopyCharsDebugLog(debugHandle, size, In); memcpy(*Out, *In, size); (*Out) += size; @@ -475,14 +480,16 @@ CodecCopyChars(unsigned long size, unsigned char **In, unsigned char **Out, unsi __inline static void CodecSkipChars(unsigned long size, unsigned char **In, unsigned long *Len, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); SkipCharsDebugLog(debugHandle, size, In); (*In) += size; (*Len) += size; } __inline static void -CodecInsertChars(unsigned long size, unsigned char *buffer, unsigned char **Out, StreamStruct *NextCodec, FILE *debugHandle) +CodecInsertChars(unsigned long size, const char *buffer, unsigned char **Out, StreamStruct *NextCodec, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); InsertCharsDebugLog(debugHandle, size, buffer); memcpy(*Out, buffer, size); (*Out) += size; @@ -498,9 +505,9 @@ SendUrlMailto(unsigned char **In, unsigned char **Out, unsigned char *OutEnd, un /* we need to replace the mailto url with our own */ CodecSkipChars(sizeof("mailto:") - 1, In, Len, debugHandle); mailToUrl = (unsigned char *)Codec->URL + strlen(Codec->URL) + 1; - mailToUrlLen = strlen(mailToUrl); + mailToUrlLen = strlen((const char *)mailToUrl); HtmlDecodeFlushOutStream(mailToUrlLen, Out, OutEnd, *Len, Codec, NextCodec); - CodecInsertChars(mailToUrlLen, mailToUrl, Out, NextCodec, debugHandle); + CodecInsertChars(mailToUrlLen, (const char *)mailToUrl, Out, NextCodec, debugHandle); } __inline static void @@ -568,6 +575,11 @@ HtmlExamineUrlUntil(unsigned char endChar, unsigned char **In, unsigned char **O { unsigned char *ptr; + UNUSED_PARAMETER(Out); + UNUSED_PARAMETER(Len); + UNUSED_PARAMETER(NextCodec); + UNUSED_PARAMETER(debugHandle); + if (tolower(**In) != 'h') { if (tolower(**In) != 'm') { if (tolower(**In) != 'j') { @@ -576,7 +588,7 @@ HtmlExamineUrlUntil(unsigned char endChar, unsigned char **In, unsigned char **O } if (*End > (*In + sizeof("javascript:") - 1)) { - if (XplStrNCaseCmp(*In, "javascript:", sizeof("javascript:") - 1) == 0) { + if (XplStrNCaseCmp((const char *)*In, "javascript:", sizeof("javascript:") - 1) == 0) { Codec->State = javascriptState; return(1); } @@ -613,7 +625,7 @@ HtmlExamineUrlUntil(unsigned char endChar, unsigned char **In, unsigned char **O if (Codec->URL) { if (*End > (*In + sizeof("mailto") - 1)) { - if (XplStrNCaseCmp(*In, "mailto:", sizeof("mailto:") - 1) == 0) { + if (XplStrNCaseCmp((const char *)*In, "mailto:", sizeof("mailto:") - 1) == 0) { /* we need to replace the mailto url with our own */ Codec->State = mailtoState; return(1); @@ -653,8 +665,13 @@ HtmlExamineUrlUntil(unsigned char endChar, unsigned char **In, unsigned char **O return(1); } - if (*End > (*In + sizeof("http:") - 1)) { - if (XplStrNCaseCmp(*In, "http:", sizeof("http:") - 1) == 0) { + if (*End > (*In + sizeof("https:") - 1)) { + if (XplStrNCaseCmp((const char *)*In, "https:", sizeof("https:") - 1) == 0) { + Codec->State = httpState; + return(1); + } + + if (XplStrNCaseCmp((const char *)*In, "http:", sizeof("http:") - 1) == 0) { Codec->State = httpState; return(1); } @@ -747,7 +764,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if (closeTagChar || Codec->EOS || ((Codec->Len - Len) > MaxTagLen)) { /* look up tag */ - tagId = BongoKeywordBegins(HtmlTagsIndex, In); + tagId = BongoKeywordBegins(HtmlTagsIndex, (const char *)In); if (tagId == -1) { Codec->State = CODEC_HTML_SEND_TAG; @@ -851,7 +868,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if (equalSignChar || Codec->EOS || ((Codec->Len - Len) > MaxAttributeLen)) { /* look up attribute */ - attributeId = BongoKeywordBegins(HtmlAttributeIndex, In); + attributeId = BongoKeywordBegins(HtmlAttributeIndex, (const char *)In); if (attributeId == -1) { Codec->State = CODEC_HTML_SEND_TAG_ATTRIBUTE_NAME; continue; @@ -1398,7 +1415,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if (closeTagChar || Codec->EOS || ((Codec->Len - Len) > MaxTagLen)) { /* look up tag */ - tagId = BongoKeywordBegins(HtmlTagsIndex, In); + tagId = BongoKeywordBegins(HtmlTagsIndex, (const char *)In); if (tagId == -1) { Codec->State = CODEC_HTML_SUPRESS_TAG; continue; @@ -1487,7 +1504,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) case CODEC_HTML_SUPRESS_META_CHARSET: { /* determine if the curent stream content is 'charset' */ if (End > (In + sizeof("charset=\"\"") - 1)) { - if (XplStrNCaseCmp(In, "charset", sizeof("charset") - 1) == 0) { + if (XplStrNCaseCmp((const char *)In, "charset", sizeof("charset") - 1) == 0) { if (In[sizeof("charset") - 1] == '=') { if (In[sizeof("charset=") - 1] != '>') { if (In[sizeof("charset=") - 1] != '\'') { @@ -1620,7 +1637,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) tmpChar = *endValue; *endValue = '\0'; - Func = FindCodec(In, FALSE); + Func = FindCodec((const char *)In, FALSE); *endValue = tmpChar; if (Func) { /* the existence of Codec->Charset is ensured in CODEC_HTML_SUPRESS_META */ @@ -1679,7 +1696,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) case CODEC_HTML_SKIP_META_CHARSET: { /* determine if the curent stream content is 'charset' */ if (End > (In + sizeof("charset=\"\"") - 1)) { - if (XplStrNCaseCmp(In, "charset", sizeof("charset") - 1) == 0) { + if (XplStrNCaseCmp((const char *)In, "charset", sizeof("charset") - 1) == 0) { if (In[sizeof("charset") - 1] == '=') { if (In[sizeof("charset=") - 1] != '>') { if (In[sizeof("charset=") - 1] != '\'') { @@ -1812,7 +1829,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) tmpChar = *endValue; *endValue = '\0'; - Func = FindCodecDecoder(In); + Func = FindCodecDecoder((const char *)In); *endValue = tmpChar; if (Func) { /* the existence of Codec->Charset is ensured in CODEC_HTML_SKIP_META */ @@ -1913,7 +1930,8 @@ HTML_Encode(StreamStruct *Codec, StreamStruct *NextCodec) Go=FALSE; continue; } - /* Fall-through */ + BlockEnd++; + continue; } default: { @@ -1992,7 +2010,7 @@ HTML_Encode(StreamStruct *Codec, StreamStruct *NextCodec) i=strlen(Codec->URL); ptr=(unsigned char *)(Codec->URL)+i+1; - i=strlen(ptr); + i=strlen((const char *)ptr); if (Codec->StreamData2) { Len=strlen(Codec->StreamData2); } @@ -2307,11 +2325,11 @@ HTML_Text_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if (closeTagChar || Codec->EOS || ((Codec->Len - Len) > strlen("Len - Len) > strlen("State = CODEC_HTML_TEXT_SUPRESS_META; continue; } else if ((toupper(*(In + 1)) == 'B') && - ((XplStrNCaseCmp(In, "", strlen("")) == 0) || (XplStrNCaseCmp(In, "", strlen("")) == 0) || (XplStrNCaseCmp((const char *)In, "State = CODEC_HTML_TEXT_SKIP_TAG; continue; } @@ -2390,7 +2408,7 @@ HTML_Text_Decode(StreamStruct *Codec, StreamStruct *NextCodec) case CODEC_HTML_TEXT_SUPRESS_META_CHARSET: { /* determine if the curent stream content is 'charset' */ if (End > (In + sizeof("charset=\"\"") - 1)) { - if (XplStrNCaseCmp(In, "charset", sizeof("charset") - 1) == 0) { + if (XplStrNCaseCmp((const char *)In, "charset", sizeof("charset") - 1) == 0) { if (In[sizeof("charset") - 1] == '=') { if (In[sizeof("charset=") - 1] != '>') { if (In[sizeof("charset=") - 1] != '\'') { @@ -2523,7 +2541,7 @@ HTML_Text_Decode(StreamStruct *Codec, StreamStruct *NextCodec) tmpChar = *endValue; *endValue = '\0'; - Func = FindCodec(In, FALSE); + Func = FindCodec((const char *)In, FALSE); *endValue = tmpChar; if (Func) { /* the existence of Codec->Charset is ensured in CODEC_HTML_TEXT_SUPRESS_META */ @@ -2589,7 +2607,7 @@ HTML_Text_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if ((Codec->Len - Len) > strlen("", strlen("")) == 0)) { + (XplStrNCaseCmp((const char *)In, "", strlen("")) == 0)) { Codec->State = CODEC_HTML_TEXT_SUPRESS_TAG; continue; } diff --git a/src/libs/streamio/codec/hz.h b/src/libs/streamio/codec/hz.h index eeb8753..df662b7 100644 --- a/src/libs/streamio/codec/hz.h +++ b/src/libs/streamio/codec/hz.h @@ -153,7 +153,7 @@ HZ_Encode(StreamStruct *Codec, StreamStruct *NextCodec) UTF8DecodeChar(In, wc, Len, Codec->Len); - if (wc >= 0x0000 && wc < 0x0460) { + if (wc < 0x0460) { summary = &gb2312_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x2000 && wc < 0x2650) { summary = &gb2312_uni2indx_page20[(wc>>4)-0x200]; diff --git a/src/libs/streamio/codec/iso2022_cn.h b/src/libs/streamio/codec/iso2022_cn.h index b32c41d..3a87a94 100644 --- a/src/libs/streamio/codec/iso2022_cn.h +++ b/src/libs/streamio/codec/iso2022_cn.h @@ -152,6 +152,7 @@ ISO2022_CN_Decode(StreamStruct *Codec, StreamStruct *NextCodec) } continue; } + __attribute__((fallthrough)); } default: { @@ -409,7 +410,7 @@ ISO2022_CN_Encode(StreamStruct *Codec, StreamStruct *NextCodec) UTF8DecodeChar(In, wc, Len, Codec->Len); /* Are we GB2312? */ - if (wc >= 0x0000 && wc < 0x0460) { + if (wc < 0x0460) { summary = &gb2312_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x2000 && wc < 0x2650) { summary = &gb2312_uni2indx_page20[(wc>>4)-0x200]; @@ -472,7 +473,7 @@ ISO2022_CN_Encode(StreamStruct *Codec, StreamStruct *NextCodec) /* We're not GB2312; check CNS11643 */ FlushOutStream(4); - if (wc >= 0x0000 && wc < 0x0100) { + if (wc < 0x0100) { summary = &cns11643_inv_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x0200 && wc < 0x03d0) { summary = &cns11643_inv_uni2indx_page02[(wc>>4)-0x020]; diff --git a/src/libs/streamio/codec/iso2022_jp.h b/src/libs/streamio/codec/iso2022_jp.h index 5b6a46d..567855b 100644 --- a/src/libs/streamio/codec/iso2022_jp.h +++ b/src/libs/streamio/codec/iso2022_jp.h @@ -303,7 +303,7 @@ ISO2022_JP_Encode(StreamStruct *Codec, StreamStruct *NextCodec) } else { /* Wasn't JIS 0201, try 0208 */ - if (wc >= 0x0000 && wc < 0x0100) { + if (wc < 0x0100) { summary = &jisx0208_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x0300 && wc < 0x0460) { summary = &jisx0208_uni2indx_page03[(wc>>4)-0x030]; diff --git a/src/libs/streamio/codec/iso2022_kr.h b/src/libs/streamio/codec/iso2022_kr.h index b4e3053..7bfe053 100644 --- a/src/libs/streamio/codec/iso2022_kr.h +++ b/src/libs/streamio/codec/iso2022_kr.h @@ -266,7 +266,7 @@ ISO2022_KR_Encode(StreamStruct *Codec, StreamStruct *NextCodec) /* Decode the UTF char */ UTF8DecodeChar(In, wc, Len, Codec->Len); - if (wc >= 0x0000 && wc < 0x0460) { + if (wc < 0x0460) { summary = &KSC5601_CP00[(wc>>4)]; } else if (wc >= 0x2000 && wc < 0x2670) { summary = &KSC5601_CP20[(wc>>4)-0x200]; diff --git a/src/libs/streamio/codec/q.h b/src/libs/streamio/codec/q.h index 87e9d98..27a81f5 100644 --- a/src/libs/streamio/codec/q.h +++ b/src/libs/streamio/codec/q.h @@ -127,7 +127,7 @@ Q_Decode(StreamStruct *Codec, StreamStruct *NextCodec) return(Len); } -static unsigned char *QHEX = {"0123456789ABCDEF"}; +static const char QHEX[] = "0123456789ABCDEF"; #define MAX_CHARS_PER_LINE 75 /* FIXME: this code is copied straight from the "quoted-printable" encoder. @@ -215,4 +215,3 @@ Q_Encode(StreamStruct *Codec, StreamStruct *NextCodec) return(Len); } - diff --git a/src/libs/streamio/codec/qp.h b/src/libs/streamio/codec/qp.h index 6f112e6..675736a 100644 --- a/src/libs/streamio/codec/qp.h +++ b/src/libs/streamio/codec/qp.h @@ -96,7 +96,7 @@ QP_Decode(StreamStruct *Codec, StreamStruct *NextCodec) return(Len); } -static unsigned char *HEX = {"0123456789ABCDEF"}; +static const char HEX[] = "0123456789ABCDEF"; #define MAX_CHARS_PER_LINE 75 static int @@ -179,4 +179,3 @@ QP_Encode(StreamStruct *Codec, StreamStruct *NextCodec) return(Len); } - diff --git a/src/libs/streamio/codec/rfc1522.h b/src/libs/streamio/codec/rfc1522.h index d8a0193..98b3995 100644 --- a/src/libs/streamio/codec/rfc1522.h +++ b/src/libs/streamio/codec/rfc1522.h @@ -791,7 +791,7 @@ RFC1522_Q_Decode(StreamStruct *Codec, StreamStruct *NextCodec) return(Len); } -static unsigned char *HEX_RFC1522 = {"0123456789ABCDEF"}; +static const char HEX_RFC1522[] = "0123456789ABCDEF"; #define MAX_CHARS_PER_LINE 75 static int @@ -874,7 +874,7 @@ NonEncodedWord: /* This is not an encoded word; pass it through */ /* This code is designed to return to pass through mode as quickly */ /* as possible if the current stream content is not an encoded word */ - unsigned char *Charset; + char *Charset; unsigned char *Encoding; unsigned char *String; unsigned char *End; @@ -890,7 +890,7 @@ NonEncodedWord: /* This is not an encoded word; pass it through */ endPtr = Codec->Start + Codec->Len; ptr = In + 2; - Charset = ptr; + Charset = (char *)ptr; /* Look for next '?' */ while (ptr < endPtr) { @@ -1120,7 +1120,7 @@ RFC1522_Encode(StreamStruct *Codec, StreamStruct *NextCodec) } /* Get the stream to translate the output into the users charset */ - if ((CharsetCodec=GetStream((unsigned char *)Codec->Charset, TRUE))==NULL) { + if ((CharsetCodec=GetStream(Codec->Charset, TRUE))==NULL) { In++; Len++; continue; @@ -1139,7 +1139,7 @@ RFC1522_Encode(StreamStruct *Codec, StreamStruct *NextCodec) FlushOutStream(30); /* Encoding indicator */ - Count=snprintf(Out, OutEnd - Out, "=?%s?%c?", (char *)Codec->Charset, Count>10 ? 'B' : 'Q'); + Count=snprintf((char *)Out, OutEnd - Out, "=?%s?%c?", (const char *)Codec->Charset, Count>10 ? 'B' : 'Q'); Out+=Count; NextCodec->Len+=Count; @@ -1280,7 +1280,7 @@ RFC2047_Name_Encode(StreamStruct *Codec, StreamStruct *NextCodec) FlushOutStream(6); /* Get the stream to translate the output into the users charset */ - if ((CharsetCodec=GetStream((unsigned char *)Codec->Charset, TRUE))==NULL) { + if ((CharsetCodec=GetStream(Codec->Charset, TRUE))==NULL) { In++; Len++; continue; @@ -1298,7 +1298,7 @@ RFC2047_Name_Encode(StreamStruct *Codec, StreamStruct *NextCodec) FlushOutStream(30); /* Encoding indicator */ - Count = snprintf(Out, OutEnd - Out, "=?%s?%c?", (char *)Codec->Charset, nonQtextChars > 10 ? 'B' : 'Q'); + Count = snprintf((char *)Out, OutEnd - Out, "=?%s?%c?", (const char *)Codec->Charset, nonQtextChars > 10 ? 'B' : 'Q'); Out+=Count; NextCodec->Len+=Count; @@ -1342,4 +1342,3 @@ RFC2047_Name_Encode(StreamStruct *Codec, StreamStruct *NextCodec) /* We have to flush the buffer */ return(Codec->Len); } - diff --git a/src/libs/streamio/codec/rfc2231.h b/src/libs/streamio/codec/rfc2231.h index 60cdb92..de26afe 100644 --- a/src/libs/streamio/codec/rfc2231.h +++ b/src/libs/streamio/codec/rfc2231.h @@ -904,7 +904,7 @@ Filename_Encode(StreamStruct *Codec, StreamStruct *NextCodec) unsigned char *OutEnd = NextCodec->End; if (Codec->StreamData) { - count = strlen((unsigned char *)(Codec->StreamData)); + count = strlen((const char *)Codec->StreamData); processed = 0; do { if ((count - processed) < BUFSIZE) { @@ -1091,7 +1091,7 @@ RFC2231_NMAP_Decode(StreamStruct *Codec, StreamStruct *NextCodec) StreamStruct *rfc2047Codec; StreamStruct *transportCodec; StreamStruct *charsetCodec; - unsigned char *charset; + char *charset; unsigned char *lang; unsigned char *encodedWord; unsigned char *nonEncodedWord; @@ -1196,8 +1196,8 @@ RFC2231_NMAP_Decode(StreamStruct *Codec, StreamStruct *NextCodec) /* possibly rfc2231 */ encodedWord = NULL; - charset = In + 2; - lang = charset + 1; + charset = (char *)In + 2; + lang = (unsigned char *)charset + 1; while (lang < endPtr) { if (*lang != '\'') { lang++; @@ -1503,15 +1503,13 @@ RFC2231_Value_Encode(StreamStruct *Codec, StreamStruct *NextCodec) unsigned long attrChars; /* number of attribute-chars as defined by rfc2231 */ BOOL needTransportEncoding; /* the presence of 8 bit chars force this to true */ - unsigned long bytesInWord; /* number of bytes of value string to be sent on this line */ - unsigned long lineCount; /* number of lines already created */ Len = 0; if (Codec->EOS) { /* find the length of the parameter name */ - nameLen = strlen((unsigned char *)(Codec->StreamData)); + nameLen = strlen((const char *)Codec->StreamData); /* Initialize stream variables */ In = Codec->Start; @@ -1790,8 +1788,6 @@ ErrorPath: *Out = '='; Out++; NextCodec->Len++; - bytesInWord = Codec->Len; - FlushOutStream(2); *Out = '"'; Out++; diff --git a/src/libs/streamio/codec/rfc822.h b/src/libs/streamio/codec/rfc822.h index 3613d34..e73ae0e 100644 --- a/src/libs/streamio/codec/rfc822.h +++ b/src/libs/streamio/codec/rfc822.h @@ -147,7 +147,7 @@ RFC822_Header_Value_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if (Codec->StreamData) { *ptr = '\0'; - if (XplStrCaseCmp(Codec->StreamData, In) == 0) { + if (XplStrCaseCmp((const char *)Codec->StreamData, (const char *)In) == 0) { Codec->State = RFC822_HEADER_VALUE_SEND; } else { Codec->State = RFC822_HEADER_VALUE_SKIP; diff --git a/src/libs/streamio/codec/search.h b/src/libs/streamio/codec/search.h index e0ada12..933df55 100644 --- a/src/libs/streamio/codec/search.h +++ b/src/libs/streamio/codec/search.h @@ -33,7 +33,7 @@ WordContains(unsigned char *word, unsigned long wordLen, unsigned char *substrin continue; } - if (XplStrNCaseCmp(word + i, substring, substringLen) != 0) { + if (XplStrNCaseCmp((const char *)word + i, (const char *)substring, substringLen) != 0) { continue; } return(TRUE); @@ -55,18 +55,16 @@ Search_Subword_Decode(StreamStruct *Codec, StreamStruct *NextCodec) unsigned long Len = 0; unsigned long substringLen; unsigned char *substring; - unsigned char *word = NULL; unsigned long wordLen; unsigned long remainingLen; substring = Codec->StreamData; - substringLen = strlen(substring); + substringLen = strlen((const char *)substring); if (substringLen > 0) { for(;;) { if (Len < Codec->Len) { if (isspace(*In)) { - word = NULL; In++; Len++; continue; @@ -161,7 +159,7 @@ Search_Substring_Decode(StreamStruct *Codec, StreamStruct *NextCodec) unsigned long count; substring = Codec->StreamData; - substringLen = strlen(substring); + substringLen = strlen((const char *)substring); if (substringLen > 0) { for(;;) { @@ -177,7 +175,7 @@ Search_Substring_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if (substringLen <= remainingLen) { /* we have enough data in the buffer to verify a match */ - if (XplStrNCaseCmp(cur, substring, substringLen) != 0) { + if (XplStrNCaseCmp((const char *)cur, (const char *)substring, substringLen) != 0) { /* not a match, keep going! */ cur++; continue; @@ -227,7 +225,7 @@ Search_Substring_Decode(StreamStruct *Codec, StreamStruct *NextCodec) continue; } - if (XplStrNCaseCmp(cur, substring, remainingLen) != 0) { + if (XplStrNCaseCmp((const char *)cur, (const char *)substring, remainingLen) != 0) { cur++; continue; } diff --git a/src/libs/streamio/codec/sjis.h b/src/libs/streamio/codec/sjis.h index ad6eab9..c83574b 100644 --- a/src/libs/streamio/codec/sjis.h +++ b/src/libs/streamio/codec/sjis.h @@ -171,7 +171,7 @@ SJIS_Encode(StreamStruct *Codec, StreamStruct *NextCodec) } else { /* Wasn't JIS 0201, try 0208 */ - if (wc >= 0x0000 && wc < 0x0100) { + if (wc < 0x0100) { summary = &jisx0208_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x0300 && wc < 0x0460) { summary = &jisx0208_uni2indx_page03[(wc>>4)-0x030]; diff --git a/src/libs/streamio/streamio.c b/src/libs/streamio/streamio.c index d56c7ea..a86db0f 100644 --- a/src/libs/streamio/streamio.c +++ b/src/libs/streamio/streamio.c @@ -519,7 +519,7 @@ StreamDescStruct StreamList[] = { {"33", SJIS_Decode, SJIS_Encode, FALSE, NULL, }, {"34", SJIS_Decode, SJIS_Encode, FALSE, NULL, }, -{NULL, NULL, NULL, FALSE, } +{NULL, NULL, NULL, FALSE, NULL, } }; #if 0 @@ -541,7 +541,7 @@ SigHandler(int Signal) #endif StreamStruct -*GetStreamEncoder(unsigned char *Charset) +*GetStreamEncoder(const char *Charset) { StreamStruct *Stream; long streamId; @@ -564,7 +564,7 @@ StreamStruct } StreamStruct -*GetStreamDecoder(unsigned char *Charset) +*GetStreamDecoder(const char *Charset) { StreamStruct *Stream; long streamId; @@ -587,7 +587,7 @@ StreamStruct } StreamStruct -*GetStream(unsigned char *Charset, BOOL Encoder) +*GetStream(const char *Charset, BOOL Encoder) { StreamDescStruct *StreamListPtr = StreamList; StreamStruct *Stream; @@ -635,7 +635,7 @@ ReleaseStream(StreamStruct *Stream) StreamCodecFunc -FindCodec(unsigned char *Charset, BOOL Encoder) +FindCodec(const char *Charset, BOOL Encoder) { StreamDescStruct *StreamListPtr = StreamList; unsigned long i = 0; @@ -655,7 +655,7 @@ FindCodec(unsigned char *Charset, BOOL Encoder) } StreamCodecFunc -FindCodecEncoder(unsigned char *Charset) +FindCodecEncoder(const char *Charset) { long codecId; @@ -668,7 +668,7 @@ FindCodecEncoder(unsigned char *Charset) } StreamCodecFunc -FindCodecDecoder(unsigned char *Charset) +FindCodecDecoder(const char *Charset) { long codecId; @@ -737,4 +737,3 @@ StreamioShutdown(void) CodecSearchShutdown(); return(TRUE); } - diff --git a/src/libs/streamio/streamiop.h b/src/libs/streamio/streamiop.h index 75fdcac..976a894 100644 --- a/src/libs/streamio/streamiop.h +++ b/src/libs/streamio/streamiop.h @@ -61,6 +61,7 @@ __inline static BOOL FlushOutStreamEx(StreamStruct *Codec, StreamStruct *NextCodec, unsigned char **Out, unsigned char *OutEnd, unsigned long Space) { + UNUSED_PARAMETER(Codec); if ((*Out + Space) < OutEnd) { return(TRUE); } @@ -247,6 +248,6 @@ typedef unsigned long ucs4_t; typedef unsigned long state_t; typedef struct _conv_t *conv_t; -StreamStruct *GetStream(unsigned char *Charset, BOOL Encoder); +StreamStruct *GetStream(const char *Charset, BOOL Encoder); BOOL ReleaseStream(StreamStruct *Stream); -StreamCodecFunc FindCodec(unsigned char *Charset, BOOL Encoder); +StreamCodecFunc FindCodec(const char *Charset, BOOL Encoder); diff --git a/src/libs/util/bongoagent.c b/src/libs/util/bongoagent.c index 71da430..e8af8b0 100644 --- a/src/libs/util/bongoagent.c +++ b/src/libs/util/bongoagent.c @@ -345,13 +345,10 @@ QueueHandleConnection (void *datap) { ListenCallbackData *data = datap; void *client; - int ccode; char line[CONN_BUFSIZE]; client = MemPrivatePoolGetEntryDirect(data->clientPool, __FILE__, __LINE__); - memset(client, 0, data->clientSize); - if (client == NULL) { XplConsolePrintf("%s: New worker failed to startup; out of memory.\r\n", data->agent->name); @@ -360,8 +357,10 @@ QueueHandleConnection (void *datap) return; } + memset(client, 0, data->clientSize); + if (ConnNegotiate(data->conn, data->agent->sslContext)) { - ccode = data->handler(client, data->conn); + data->handler(client, data->conn); } NMAPSendCommand(data->conn, "QDONE\r\n", 7); @@ -379,19 +378,18 @@ HandleConnection (void *datap) { ListenCallbackData *data = datap; void *client; - int ccode; client = MemPrivatePoolGetEntryDirect(data->clientPool, __FILE__, __LINE__); - memset(client, 0, data->clientSize); - if (client == NULL) { XplConsolePrintf("%s: New worker failed to startup; out of memory.\r\n", data->agent->name); return; } + memset(client, 0, data->clientSize); + if (ConnNegotiate(data->conn, data->agent->sslContext)) { - ccode = data->handler(client, data->conn); + data->handler(client, data->conn); } ConnClose(data->conn); @@ -602,7 +600,7 @@ BongoAgentShutdown(BongoAgent *agent) MemFree((char *)agent->name); } - ConnCloseAll(1); + ConnCloseAll(); LogClose(); @@ -856,4 +854,3 @@ BongoAgentStartMonitor(BongoAgent *agent) XplBeginThread(&agent->monitorID, Monitor, 10240, agent, ccode); } - diff --git a/src/libs/util/bongokeyword.c b/src/libs/util/bongokeyword.c index 3ff0769..040afd6 100644 --- a/src/libs/util/bongokeyword.c +++ b/src/libs/util/bongokeyword.c @@ -117,7 +117,7 @@ KeywordCompareCaseInSensitive(const void *param1, const void *param2) } BongoKeywordIndex * -BongoKeywordIndexCreate(char **strings, unsigned long count, BOOL caseInsensitive) +BongoKeywordIndexCreate(const char *const *strings, unsigned long count, BOOL caseInsensitive) { unsigned long i; BongoKeywordIndex *index; @@ -161,12 +161,12 @@ BongoKeywordIndexCreate(char **strings, unsigned long count, BOOL caseInsensitiv } __inline static long -FindMatch(BongoKeywordIndex *keywordIndex, char *searchString, BOOL beginsWith) +FindMatch(BongoKeywordIndex *keywordIndex, const char *searchString, BOOL beginsWith) { BongoKeyword *keyword; unsigned long row = 0; unsigned long nextRow; - unsigned char *string; + const unsigned char *string; BongoKeywordTable *table; table = keywordIndex->table; @@ -250,13 +250,13 @@ FindMatch(BongoKeywordIndex *keywordIndex, char *searchString, BOOL beginsWith) } long -BongoKeywordFind(BongoKeywordIndex *keywordIndex, unsigned char *searchString) +BongoKeywordFind(BongoKeywordIndex *keywordIndex, const char *searchString) { return(FindMatch(keywordIndex, searchString, FALSE)); } long -BongoKeywordBegins(BongoKeywordIndex *keywordIndex, unsigned char *searchString) +BongoKeywordBegins(BongoKeywordIndex *keywordIndex, const char *searchString) { return(FindMatch(keywordIndex, searchString, TRUE)); } diff --git a/src/libs/util/bongoutil.c b/src/libs/util/bongoutil.c index 5beb669..bf1c02f 100644 --- a/src/libs/util/bongoutil.c +++ b/src/libs/util/bongoutil.c @@ -24,7 +24,7 @@ #include #include -const unsigned char *Base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; +const char Base64Chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; BOOL QuickNCmp(char *str1, char *str2, int len) @@ -424,23 +424,19 @@ ProtocolCommandTreeRotateRight(ProtocolCommandTree *tree, ProtocolCommand *comma } ProtocolCommand * -ProtocolCommandTreeSearch(ProtocolCommandTree *tree, const unsigned char *command) +ProtocolCommandTreeSearch(ProtocolCommandTree *tree, const char *command) { - const unsigned char *k1; - const unsigned char *k2; + const char *k1; + const char *k2; ProtocolCommand *x; - ProtocolCommand *y; if (tree && command) { - y = tree->sentinel; x = tree->root; k1 = x->name; k2 = command; while (x != tree->sentinel) { - y = x; - - if (*k1 < toupper(*k2)) { + if (*k1 < toupper((unsigned char)*k2)) { x = x->left; k1 = x->name; @@ -449,7 +445,7 @@ ProtocolCommandTreeSearch(ProtocolCommandTree *tree, const unsigned char *comman continue; } - if (*k1 > toupper(*k2)) { + if (*k1 > toupper((unsigned char)*k2)) { x = x->right; k1 = x->name; @@ -475,8 +471,8 @@ ProtocolCommandTreeSearch(ProtocolCommandTree *tree, const unsigned char *comman static void ProtocolCommandTreeInsert(ProtocolCommandTree *tree, ProtocolCommand *command) { - const unsigned char *k1; - const unsigned char *k2; + const char *k1; + const char *k2; BOOL l = FALSE; ProtocolCommand *x; ProtocolCommand *y; @@ -639,13 +635,13 @@ LoadProtocolCommandTree(ProtocolCommandTree *tree, ProtocolCommand *commands) return; } -unsigned char * -DecodeBase64(unsigned char *EncodedString) +char * +DecodeBase64(char *EncodedString) { int i; int j; int length = strlen(EncodedString); - unsigned char *buffer = MemStrdup(EncodedString); + unsigned char *buffer = (unsigned char *)MemStrdup(EncodedString); unsigned char *ptr = buffer; unsigned char table[257]; unsigned char *ptr2; @@ -654,7 +650,7 @@ DecodeBase64(unsigned char *EncodedString) table[256] = '\0'; for (i = 0; i < 65; i++) { - table[Base64Chars[i]] = i; + table[(unsigned char)Base64Chars[i]] = i; } for (i=0; i= NMAP_CRED_STORE_SIZE) { - srcPtr = Credential; - dstPtr = Hash; - dstEnd = Hash + NMAP_HASH_SIZE; + srcPtr = (const unsigned char *)Credential; + dstPtr = (unsigned char *)Hash; + dstEnd = (unsigned char *)Hash + NMAP_HASH_SIZE; XplHashNew(&ctx, XPLHASH_MD5); XplHashWrite(&ctx, srcPtr, NMAP_CRED_CHUNK_SIZE); @@ -844,7 +840,7 @@ HashCredential(unsigned char *Credential, unsigned char *Hash) do { XplHashNew(&ctx, XPLHASH_MD5); XplHashWrite(&ctx, srcPtr, NMAP_CRED_CHUNK_SIZE); - XplHashWrite(&ctx, Hash, dstPtr - Hash); + XplHashWrite(&ctx, Hash, dstPtr - (unsigned char *)Hash); XplHashFinalBytes(&ctx, digest, XPLHASH_MD5BYTES_LENGTH); srcPtr += NMAP_CRED_CHUNK_SIZE; diff --git a/src/libs/util/pools.c b/src/libs/util/pools.c index 9fd2ca0..333be4f 100644 --- a/src/libs/util/pools.c +++ b/src/libs/util/pools.c @@ -11,7 +11,7 @@ struct MemPoolPrivateRef { }; EXPORT void * -MemPrivatePoolAlloc(unsigned char *name, size_t AllocationSize, unsigned int MinAllocCount, unsigned int MaxAllocCount, BOOL Dynamic, BOOL Temporary, PoolEntryCB allocCB, PoolEntryCB freeCB, void *clientData) +MemPrivatePoolAlloc(const char *name, size_t AllocationSize, unsigned int MinAllocCount, unsigned int MaxAllocCount, BOOL Dynamic, BOOL Temporary, PoolEntryCB allocCB, PoolEntryCB freeCB, void *clientData) { struct MemPoolPrivateRef *ref; UNUSED_PARAMETER_REFACTOR(name); diff --git a/src/libs/util/stringbuilder.c b/src/libs/util/stringbuilder.c index 5a26668..40ce2aa 100644 --- a/src/libs/util/stringbuilder.c +++ b/src/libs/util/stringbuilder.c @@ -202,13 +202,11 @@ void BongoStringBuilderAppendF(BongoStringBuilder *sb, const char *format, ...) { char buf[1024]; - int i; va_list ap; va_start(ap, format); - i = XplVsnprintf(buf, 1024, format, ap); + XplVsnprintf(buf, 1024, format, ap); va_end(ap); BongoStringBuilderAppend(sb, buf); } - diff --git a/src/libs/util/utf7mod.c b/src/libs/util/utf7mod.c index cddd9ce..f03fa20 100644 --- a/src/libs/util/utf7mod.c +++ b/src/libs/util/utf7mod.c @@ -620,7 +620,7 @@ ModifiedUtf7ToUtf8(char *utf7Start, unsigned long utf7Len, char *utf8Buffer, uns do { /* find end of 7 bit string */ - ptr = strchr(utf7In, '&'); + ptr = memchr(utf7In, '&', utf7InEnd - utf7In); if (!ptr) { if (Copy7BitString(&utf7In, utf7InEnd - utf7In, &utf8Out, utf8OutEnd)) { return((int)(utf8Out - (unsigned char *)utf8Buffer)); @@ -645,7 +645,7 @@ ModifiedUtf7ToUtf8(char *utf7Start, unsigned long utf7Len, char *utf8Buffer, uns } /* have encoded string; find end */ - ptr = strchr(utf7In, '-'); + ptr = memchr(utf7In, '-', utf7InEnd - utf7In); if (ptr) { ccode = ModifiedBase64ToUtf8(&utf7In, ptr, &utf8Out, utf8OutEnd); if (ccode == UTF7_STATUS_SUCCESS) { @@ -699,7 +699,7 @@ GetUtf8FromModifiedUtf7(char *utf7Start, unsigned long utf7Len, char **result) do { /* find end of 7 bit string */ - ptr = strchr(utf7In, '&'); + ptr = memchr(utf7In, '&', utf7InEnd - utf7In); if (!ptr) { Copy7BitStringNoCheck(&utf7In, utf7InEnd - utf7In, &utf8Out); resultLen = (int)(utf8Out - (unsigned char *)utf8OutBuffer); @@ -730,7 +730,7 @@ GetUtf8FromModifiedUtf7(char *utf7Start, unsigned long utf7Len, char **result) } /* have encoded string; find end */ - ptr = strchr(utf7In, '-'); + ptr = memchr(utf7In, '-', utf7InEnd - utf7In); if (ptr) { ccode = ModifiedBase64ToUtf8NoCheck(&utf7In, ptr, &utf8Out); if (ccode == UTF7_STATUS_SUCCESS) { @@ -787,7 +787,7 @@ Utf8ToModifiedUtf7(char *utf8Start, unsigned long utf8Len, char *utf7Buffer, uns BOOL PassThroughMode; unsigned char octet[6]; - long octetCount = 0; + unsigned long octetCount = 0; utf8In = (unsigned char *)utf8Start; utf8InEnd = utf8In + utf8Len; @@ -909,7 +909,7 @@ GetModifiedUtf7FromUtf8(char *utf8Start, unsigned long utf8Len, char **result) unicode utf16low; BOOL PassThroughMode = TRUE; unsigned char octet[6]; - long octetCount = 0; + unsigned long octetCount = 0; /* Set up in and out buffers */ utf8In = (unsigned char *)utf8Start; diff --git a/src/libs/xpl/dir.c b/src/libs/xpl/dir.c index f5f53cd..47ea717 100644 --- a/src/libs/xpl/dir.c +++ b/src/libs/xpl/dir.c @@ -41,7 +41,11 @@ XplDir return NULL; } - strcpy(newDir->Path, dirname); + if (snprintf(newDir->Path, sizeof(newDir->Path), "%s", dirname) >= (int)sizeof(newDir->Path)) { + closedir(newDir->dirp); + MemFree(newDir); + return NULL; + } newDir->d_size = 0; newDir->d_attr = 0; @@ -61,7 +65,10 @@ XplDir if (dirp->direntp) { dirp->d_name = dirp->direntp->d_name; - sprintf(path, "%s/%s", dirp->Path, dirp->direntp->d_name); + if (snprintf(path, sizeof(path), "%s/%s", dirp->Path, + dirp->direntp->d_name) >= (int)sizeof(path)) { + return NULL; + } if (stat(path, &(dirp->stats)) == 0) { dirp->d_attr = dirp->stats.st_mode; dirp->d_cdatetime = dirp->stats.st_mtime; @@ -103,13 +110,16 @@ XplRemoveDir(const char *dir) if (!strcmp(".", dh->direntp->d_name) || !strcmp("..", dh->direntp->d_name)) continue; - snprintf(path, XPL_MAX_PATH, "%s/%s", dh->Path, dh->direntp->d_name); + if (snprintf(path, sizeof(path), "%s/%s", dh->Path, + dh->direntp->d_name) >= (int)sizeof(path)) { + goto finish; + } if (stat(path, &(dh->stats)) != 0) goto finish; if (S_ISREG(dh->stats.st_mode)) { - unlink(dh->direntp->d_name); + if (unlink(path)) goto finish; } else if (S_ISDIR(dh->stats.st_mode)) { if (XplRemoveDir(path)) goto finish; if (rmdir(path)) goto finish; diff --git a/src/libs/xpl/dns.c b/src/libs/xpl/dns.c index 58f9549..121eefd 100644 --- a/src/libs/xpl/dns.c +++ b/src/libs/xpl/dns.c @@ -44,22 +44,24 @@ _XplDns_ParseName(const unsigned char *response, const int response_len, const u break; } if ((*ptr & 0xc0) == 0xc0) { - int result; + int nested_result; int newptr; // this is a pointer to a previous label newptr = (*ptr++ & 0x3f) * 256; newptr += *ptr++; if (newptr == (ptr - response - 2)) return -4; // cyclical pointer; bad. - result = _XplDns_ParseName(response, response_len, (response + newptr), buffer, buffer_len); - if (result <= 0) return result; + nested_result = _XplDns_ParseName(response, response_len, (response + newptr), buffer, buffer_len); + if (nested_result <= 0) return nested_result; break; } else { // append the next label if we have room in the buffer if ((*ptr + 1) < len) { // FIXME - cast below should be removed somehow. - strncat(buffer, (char *)ptr + 1, *ptr); - strncat(buffer, ".", 1); + size_t used = strlen(buffer); + memcpy(buffer + used, ptr + 1, *ptr); + buffer[used + *ptr] = '.'; + buffer[used + *ptr + 1] = '\0'; len -= *ptr + 1; ptr += *ptr + 1; } else { diff --git a/src/libs/xpl/hash.c b/src/libs/xpl/hash.c index 3629291..e656575 100644 --- a/src/libs/xpl/hash.c +++ b/src/libs/xpl/hash.c @@ -13,7 +13,9 @@ #include #include #include +#if GCRYPT_VERSION_NUMBER < 0x010600 GCRY_THREAD_OPTION_PTHREAD_IMPL; +#endif /** * Initialise the XPL library. Should be called once in each program that @@ -37,9 +39,12 @@ XplInit(void) void XplHashInit(void) { - //const char *gcry_version; + /* Since libgcrypt 1.6 the library uses the platform thread support + * internally and GCRYCTL_SET_THREAD_CBS is obsolete. */ + gcry_check_version(NULL); +#if GCRYPT_VERSION_NUMBER < 0x010600 gcry_control (GCRYCTL_SET_THREAD_CBS, &gcry_threads_pthread); - //gcry_version = gcry_check_version(NULL); +#endif gcry_control (GCRYCTL_DISABLE_SECMEM, 0); gcry_control (GCRYCTL_SET_RANDOM_SEED_FILE, XPL_DEFAULT_RANDSEED_PATH); gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0); @@ -106,25 +111,26 @@ XplHashFinalBytes(xpl_hash_context *context, unsigned char *buffer, size_t lengt void XplHashFinal(xpl_hash_context *context, xpl_hash_stringcase strcase, char *buffer, size_t length) { - char format[5]; - unsigned char *digest; + const unsigned char *digest; char *p; unsigned int i; - memcpy(format, "%02X\0", 5); - if (strcase == XPLHASH_LOWERCASE) - format[3] = 'x'; + if (!buffer || length == 0) { + gcry_md_close(context->gcrypt_context); + return; + } + digest = gcry_md_read(context->gcrypt_context, 0); + + for (i = 0, p = buffer; digest && i < context->buffer_size && + (size_t)(p - buffer) + 2 < length; i++, p += 2) { + if (strcase == XPLHASH_LOWERCASE) + snprintf(p, length - (size_t)(p - buffer), "%02x", digest[i]); + else + snprintf(p, length - (size_t)(p - buffer), "%02X", digest[i]); + } + *p = '\0'; - digest = MemMalloc(context->buffer_size); - memcpy(digest, gcry_md_read(context->gcrypt_context, 0), context->buffer_size); gcry_md_close(context->gcrypt_context); - - for (i = 0, p = buffer; i < context->buffer_size && p < buffer+length-1; i++, p += 2) { - sprintf((char *)p, format, digest[i]); - } - buffer[length-1] = 0; - - MemFree(digest); } /** diff --git a/src/www/CMakeLists.txt b/src/www/CMakeLists.txt new file mode 100644 index 0000000..80809af --- /dev/null +++ b/src/www/CMakeLists.txt @@ -0,0 +1,46 @@ +configure_file(bongo-web.py.in bongo-web @ONLY) + +find_program(MSGFMT_EXECUTABLE msgfmt REQUIRED) +set(BONGO_WEB_LOCALES de fr pt_BR) +foreach(locale IN LISTS BONGO_WEB_LOCALES) + set(mo_file ${CMAKE_CURRENT_BINARY_DIR}/locale/${locale}/LC_MESSAGES/bongo-web.mo) + add_custom_command(OUTPUT ${mo_file} + COMMAND ${CMAKE_COMMAND} -E make_directory + ${CMAKE_CURRENT_BINARY_DIR}/locale/${locale}/LC_MESSAGES + COMMAND ${MSGFMT_EXECUTABLE} --check -o ${mo_file} + ${CMAKE_SOURCE_DIR}/po/bongo-web/${locale}.po + DEPENDS ${CMAKE_SOURCE_DIR}/po/bongo-web/${locale}.po VERBATIM) + list(APPEND BONGO_WEB_CATALOGS ${mo_file}) + install(FILES ${mo_file} + DESTINATION ${LOCALEDIR}/${locale}/LC_MESSAGES) +endforeach() + +add_custom_target(bongo-web-check ALL + COMMAND ${Python3_EXECUTABLE} -m py_compile + ${CMAKE_CURRENT_BINARY_DIR}/bongo-web + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/bongo-web ${BONGO_WEB_CATALOGS} + VERBATIM) + +install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/bongo-web + DESTINATION ${SBIN_INSTALL_DIR}) + +install(FILES bongo-web.json + DESTINATION ${SYSCONF_INSTALL_DIR}/bongo) + +install(DIRECTORY bongo_web + DESTINATION ${PYTHON_SITEPACKAGES_PATH} + FILES_MATCHING + PATTERN "__pycache__" EXCLUDE + PATTERN "*.py") + +install(DIRECTORY static/ + DESTINATION ${DATA_INSTALL_DIR}/web) + +if(BUILD_TESTING) + add_test(NAME web-dav-discovery + COMMAND ${CMAKE_COMMAND} -E env + "PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}:${CMAKE_SOURCE_DIR}/src/libs/python" + "PYTHONPYCACHEPREFIX=${CMAKE_CURRENT_BINARY_DIR}/pycache" + ${Python3_EXECUTABLE} -m unittest discover + -s ${CMAKE_CURRENT_SOURCE_DIR}/tests -v) +endif() diff --git a/src/www/bongo-web.json b/src/www/bongo-web.json new file mode 100644 index 0000000..632d75b --- /dev/null +++ b/src/www/bongo-web.json @@ -0,0 +1,14 @@ +{ + "listen_address": "127.0.0.1", + "port": 8080, + "public_base_url": "", + "external_https": false, + "allow_basic_auth": true, + "trusted_proxies": ["127.0.0.1", "::1"], + "maximum_request_size": 10485760, + "session_database": "/var/lib/bongo/web/sessions.db", + "task_database": "/var/lib/bongo/web/tasks.db", + "session_lifetime": 3600, + "login_attempts": 5, + "login_window": 300 +} diff --git a/src/www/bongo-web.py.in b/src/www/bongo-web.py.in new file mode 100644 index 0000000..32738c9 --- /dev/null +++ b/src/www/bongo-web.py.in @@ -0,0 +1,903 @@ +#!/usr/bin/env python3 +"""Bongo's HTTP entry point for webmail, administration and DAV. + +The service binds to loopback by default and is intended to run behind a +TLS-terminating reverse proxy. Privileged operations are never performed by +this process. +""" + +from __future__ import annotations + +import argparse +import base64 +import binascii +import json +import gettext +import logging +import os +import posixpath +import re +import ssl +from http import HTTPStatus +from http.cookies import SimpleCookie +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import parse_qs, urlsplit + +from bongo_web.auth import LoginLimiter, authenticate, revoke, valid_csrf +from bongo_web.calendar_store import (CalendarNotFound, CalendarPathError, + PreconditionFailed, delete_event, + list_events, parse_calendar_path, read_event, + write_event) +from bongo_web.card_store import (CardNotFound, CardPathError, + CardPreconditionFailed, delete_card, + list_cards, parse_card_path, read_card, + write_card) +from bongo_web.dav import (card_multistatus, event_multistatus, multistatus, + options_headers, parse_card_report, parse_report, + requested_properties) +from bongo_web.session import (COOKIE_NAME, SessionStore, + expired_session_cookie, session_cookie) +from bongo_web.tasks import TaskStore +from bongo_web.mail_store import list_conversations, list_folders, read_conversation +from bongo_web.compose import send_message + + +DEFAULT_CONFIG = "@SYSCONF_INSTALL_DIR@/bongo/bongo-web.json" +STATIC_DIR = "@DATA_INSTALL_DIR@/web" +LOCALE_DIR = "@LOCALEDIR@" +SERVER_NAME = "Bongo" +STATIC_FILES = { + "/assets/app.css": ("app.css", "text/css; charset=utf-8"), + "/assets/app.js": ("app.js", "text/javascript; charset=utf-8"), + "/assets/Bongo-logo.png": ("Bongo-logo.png", "image/png"), + "/assets/dragonfly.png": ("dragonfly.png", "image/png"), + "/assets/contact-new.png": ("contact-new.png", "image/png"), + "/assets/favicon.ico": ("favicon.ico", "image/x-icon"), + "/assets/mail-inbox-22.png": ("mail-inbox-22.png", "image/png"), + "/assets/calendar-16.png": ("calendar-16.png", "image/png"), + "/assets/agent-control.png": ("agent-control.png", "image/png"), + "/assets/login-bg-top.png": ("login-bg-top.png", "image/png"), + "/assets/login-bg-middle.png": ("login-bg-middle.png", "image/png"), + "/assets/login-bg-bottom.png": ("login-bg-bottom.png", "image/png"), + "/assets/login-logo.png": ("login-logo.png", "image/png"), + "/assets/bg-page.png": ("bg-page.png", "image/png"), + "/assets/bg-selected.png": ("bg-selected.png", "image/png"), + "/assets/top-gradient.png": ("top-gradient.png", "image/png"), +} +UI_MESSAGES = ("Email address", "Password", "Sign in", "Sign out", "Mail", + "Calendar", "Contacts", "Administration", + "Mail, calendar and contacts", "Login failed.", + "Too many login attempts. Please try again later.", + "Calendar access is available through CalDAV.", + "Contact access is available through CardDAV.", + "The secure webmail connection is being enabled.", + "Administrative functions require a server-side role check.", + "Folder", "Refresh", "(no subject)", "Back", + "Load external images", "[external image blocked]", + "Compose new mail", "From", "To", "Cc", "Bcc", "Subject", + "Message", "Send", "Cancel", "Message sent.", + "The message could not be sent.", "Invalid message fields.", + "Tasks", "No open tasks.", "Ignore", "Disable", "Done", + "Settings", "Personal settings", "Open tasks", "Filters", + "External mail accounts", + "Sender identities", "Two-factor authentication", + "App passwords", "Language and display") +SUPPORTED_LANGUAGES = ("en", "de", "fr", "pt_BR") +ASSET_PATH = re.compile(r"^/assets/(css|img)/([A-Za-z0-9._-]+)$") +ASSET_TYPES = {".css": "text/css; charset=utf-8", ".gif": "image/gif", + ".ico": "image/x-icon", ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", ".png": "image/png"} + + +def load_config(path: str) -> dict: + with open(path, "r", encoding="utf-8") as stream: + config = json.load(stream) + if not isinstance(config, dict): + raise ValueError("web configuration must be a JSON object") + return config + + +class BongoWebServer(ThreadingHTTPServer): + daemon_threads = True + allow_reuse_address = True + + def __init__(self, address, handler, config): + super().__init__(address, handler) + self.config = config + self.sessions = SessionStore( + config.get("session_database", "/var/lib/bongo/web/sessions.db"), + config.get("session_lifetime", 3600)) + self.login_limiter = LoginLimiter(config.get("login_attempts", 5), + config.get("login_window", 300)) + self.tasks = TaskStore(config.get("task_database", + "/var/lib/bongo/web/tasks.db")) + + +class BongoWebHandler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + server_version = SERVER_NAME + sys_version = "" + + def version_string(self): + return SERVER_NAME + + def log_message(self, message, *args): + logging.info("%s %s", self.client_address[0], message % args) + + def send_response(self, code, message=None): + super().send_response(code, message) + self.send_header("X-Content-Type-Options", "nosniff") + self.send_header("X-Frame-Options", "DENY") + self.send_header("Referrer-Policy", "no-referrer") + self.send_header("Permissions-Policy", "camera=(), microphone=(), geolocation=()") + self.send_header("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data: http: https:; connect-src 'self'; frame-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'") + self.send_header("Cache-Control", "no-store") + + def _path(self): + raw_path = urlsplit(self.path).path + collection_path = raw_path.endswith("/") + normalized = posixpath.normpath("/" + raw_path.lstrip("/")) + if collection_path and normalized != "/": + normalized += "/" + return normalized + + def _empty(self, status, headers=None): + self.send_response(status) + for name, value in (headers or {}).items(): + self.send_header(name, value) + self.send_header("Content-Length", "0") + self.end_headers() + + def _json(self, status, value, headers=None): + payload = json.dumps(value, ensure_ascii=False).encode("utf-8") + b"\n" + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + for name, value in (headers or {}).items(): + self.send_header(name, value) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def _static(self, filename, content_type): + try: + with open(os.path.join(STATIC_DIR, filename), "rb") as stream: + payload = stream.read() + except OSError: + self._empty(HTTPStatus.NOT_FOUND) + return + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def _original_asset(self, path): + match = ASSET_PATH.fullmatch(path) + if match is None: + return False + extension = os.path.splitext(match.group(2))[1].casefold() + content_type = ASSET_TYPES.get(extension) + if content_type is None: + return False + self._static(os.path.join(match.group(1), match.group(2)), content_type) + return True + + def _translations(self): + query = parse_qs(urlsplit(self.path).query) + requested = query.get("lang", [""])[-1].replace("-", "_") + if not requested: + requested = self.headers.get("Accept-Language", "en").split(",", 1)[0] + requested = requested.split(";", 1)[0].strip().replace("-", "_") + language = next((item for item in SUPPORTED_LANGUAGES + if requested == item or requested.split("_", 1)[0] == item), + "en") + catalog = gettext.translation("bongo-web", LOCALE_DIR, + languages=[language], fallback=True) + self._json(HTTPStatus.OK, + {"language": language, + "messages": {message: catalog.gettext(message) + for message in UI_MESSAGES}}) + + def _mail_api(self, path): + session = self._session() + if session is None: + self._json(HTTPStatus.UNAUTHORIZED, + {"error": "authentication required"}, {"Vary": "Cookie"}) + return + try: + if path == "/api/mail/folders": + result = {"folders": list_folders(session["user"], + session["store_cookie"])} + elif path == "/api/mail/conversations": + query = parse_qs(urlsplit(self.path).query) + folder = query.get("folder", ["/mail/INBOX"])[-1] + offset = int(query.get("offset", ["0"])[-1]) + limit = int(query.get("limit", ["30"])[-1]) + result = list_conversations(session["user"], + session["store_cookie"], folder, + offset, limit) + elif path == "/api/mail/conversation": + query = parse_qs(urlsplit(self.path).query) + conversation_id = query.get("id", [""])[-1] + result = read_conversation(session["user"], + session["store_cookie"], + conversation_id) + for message in result.get("messages", ()): + mailing_list = message.get("mailing_list") + if mailing_list: + self.server.tasks.upsert( + session["user"], "mailing_list", mailing_list["id"], + "Mailing list detected", + {"list_id": mailing_list["id"], + "list_post": mailing_list.get("post", ""), + "conversation_id": conversation_id}) + else: + self._empty(HTTPStatus.NOT_FOUND) + return + except FileNotFoundError: + self._empty(HTTPStatus.NOT_FOUND) + return + except (TypeError, ValueError): + self._empty(HTTPStatus.BAD_REQUEST) + return + except RuntimeError: + logging.exception("Could not serve webmail request") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + self._json(HTTPStatus.OK, result, {"Vary": "Cookie"}) + + def _session_token(self): + try: + cookies = SimpleCookie(self.headers.get("Cookie", "")) + except Exception: + return None + morsel = cookies.get(COOKIE_NAME) + return morsel.value if morsel else None + + def _session(self): + return self.server.sessions.get(self._session_token()) + + def _request_data(self): + length = self.headers.get("Content-Length") + if length is None: + raise ValueError("length required") + try: + size = int(length, 10) + except ValueError as error: + raise ValueError("invalid length") from error + maximum = int(self.server.config.get("maximum_request_size", 10485760)) + if size < 0 or size > maximum: + raise OverflowError("request too large") + body = self.rfile.read(size) + content_type = self.headers.get_content_type() + if content_type == "application/json": + value = json.loads(body.decode("utf-8")) + if not isinstance(value, dict): + raise ValueError("JSON object required") + return value + if content_type == "application/x-www-form-urlencoded": + values = parse_qs(body.decode("utf-8"), keep_blank_values=True) + return {key: items[-1] for key, items in values.items()} + raise TypeError("unsupported media type") + + def _request_body(self, maximum=65536): + length = self.headers.get("Content-Length", "0") + try: + size = int(length, 10) + except ValueError as error: + raise ValueError("invalid length") from error + if size < 0 or size > maximum: + raise OverflowError("request too large") + return self.rfile.read(size) + + def _dav_session(self): + session = self._session() + if session is not None: + session["transient"] = False + return session + authorization = self.headers.get("Authorization", "") + basic_allowed = (self.server.config.get("allow_basic_auth", True) + and self.server.config.get("external_https", False)) + if basic_allowed and authorization.startswith("Basic "): + try: + decoded = base64.b64decode(authorization[6:], validate=True) + user, password = decoded.decode("utf-8").split(":", 1) + except (binascii.Error, UnicodeError, ValueError): + user = password = "" + lifetime = min(int(self.server.config.get("session_lifetime", 3600)), + 300) + store_cookie = authenticate(user.strip(), password, lifetime) + password = None + if store_cookie is not None: + return {"user": user.strip(), "store_cookie": store_cookie, + "transient": True} + self._json(HTTPStatus.UNAUTHORIZED, + {"error": "authentication required"}, + {"Vary": "Cookie", + "WWW-Authenticate": 'Basic realm="Bongo", charset="UTF-8"'}) + return None + + @staticmethod + def _release_dav_session(session): + if session and session.get("transient"): + revoke(session["user"], session["store_cookie"]) + + def _dav_write_allowed(self, session): + return (session.get("transient") + or valid_csrf(session, self.headers.get("X-Bongo-CSRF", ""))) + + def _dav_redirect(self, service): + base = self.server.config.get("public_base_url", "").rstrip("/") + location = f"{base}/{service}/" if base else f"/{service}/" + self._empty(HTTPStatus.TEMPORARY_REDIRECT, {"Location": location}) + + def do_GET(self): + path = self._path() + if path == "/health": + self._json(HTTPStatus.OK, {"status": "ok"}) + elif path == "/api/session": + session = self._session() + if session is None: + self._json(HTTPStatus.UNAUTHORIZED, {"error": "authentication required"}, + {"Vary": "Cookie"}) + else: + self._json(HTTPStatus.OK, + {"user": session["user"], + "csrf_token": session["csrf_token"]}, + {"Vary": "Cookie"}) + elif path == "/api/i18n": + self._translations() + elif path.startswith("/api/mail/"): + self._mail_api(path) + elif path == "/api/tasks": + session = self._session() + if session is None: + self._json(HTTPStatus.UNAUTHORIZED, + {"error": "authentication required"}) + else: + query = parse_qs(urlsplit(self.path).query) + include_closed = query.get("include_closed", ["0"])[-1] == "1" + self._json(HTTPStatus.OK, + {"tasks": self.server.tasks.list( + session["user"], include_closed=include_closed)}, + {"Vary": "Cookie"}) + elif path == "/.well-known/caldav": + self._dav_redirect("calendars") + elif path == "/.well-known/carddav": + self._dav_redirect("addressbooks") + elif path.startswith("/calendars/") and path.endswith(".ics"): + self._calendar_get(path) + elif path.startswith("/addressbooks/") and path.endswith(".vcf"): + self._card_get(path) + elif path in ("/", "/mail", "/admin", "/settings"): + self._static("index.html", "text/html; charset=utf-8") + elif path in STATIC_FILES: + self._static(*STATIC_FILES[path]) + elif path.startswith("/assets/") and self._original_asset(path): + pass + elif path in ("/calendars", "/addressbooks"): + self._dav_redirect(path[1:]) + else: + self._empty(HTTPStatus.NOT_FOUND) + + def _calendar_get(self, path): + session = self._dav_session() + if session is None: + return + try: + try: + resource = parse_calendar_path(path, session["user"]) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CalendarNotFound, CalendarPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + if resource["kind"] != "event": + self._empty(HTTPStatus.NOT_FOUND) + return + try: + payload, etag = read_event(session["user"], + session["store_cookie"], + resource["event"]) + except CalendarNotFound: + self._empty(HTTPStatus.NOT_FOUND) + return + if self.headers.get("If-None-Match") == etag: + self._empty(HTTPStatus.NOT_MODIFIED, {"ETag": etag}) + return + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "text/calendar; charset=utf-8") + self.send_header("ETag", etag) + self.send_header("Content-Length", str(len(payload))) + self.send_header("Vary", "Cookie, Authorization") + self.end_headers() + self.wfile.write(payload) + finally: + self._release_dav_session(session) + + def _card_get(self, path): + session = self._dav_session() + if session is None: + return + try: + try: + resource = parse_card_path(path, session["user"]) + if resource["kind"] != "card": + raise CardPathError("not a card") + payload, etag = read_card(session["user"], + session["store_cookie"], + resource["card"]) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CardNotFound, CardPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + except RuntimeError: + logging.exception("Could not read CardDAV contact") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + if self.headers.get("If-None-Match") == etag: + self._empty(HTTPStatus.NOT_MODIFIED, {"ETag": etag}) + return + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "text/vcard; charset=utf-8") + self.send_header("ETag", etag) + self.send_header("Content-Length", str(len(payload))) + self.send_header("Vary", "Cookie, Authorization") + self.end_headers() + self.wfile.write(payload) + finally: + self._release_dav_session(session) + + def do_HEAD(self): + path = self._path() + if path == "/health": + self._empty(HTTPStatus.OK) + else: + self._empty(HTTPStatus.NOT_FOUND) + + def do_POST(self): + path = self._path() + if path == "/api/tasks/state": + self._task_state() + return + if path == "/api/mail/send": + self._mail_send() + return + if path != "/api/session": + self._empty(HTTPStatus.NOT_FOUND) + return + try: + data = self._request_data() + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except TypeError: + self._empty(HTTPStatus.UNSUPPORTED_MEDIA_TYPE) + return + except (UnicodeError, ValueError, json.JSONDecodeError): + self._empty(HTTPStatus.BAD_REQUEST) + return + user = str(data.get("user", "")).strip() + password = str(data.get("password", "")) + key = (self.client_address[0], user.casefold()) + if not self.server.login_limiter.allowed(key): + self._json(HTTPStatus.TOO_MANY_REQUESTS, + {"error": "authentication temporarily unavailable"}, + {"Retry-After": str(self.server.login_limiter.window)}) + return + lifetime = int(self.server.config.get("session_lifetime", 3600)) + store_cookie = authenticate(user, password, lifetime) + password = None + if store_cookie is None: + self.server.login_limiter.failed(key) + self._json(HTTPStatus.UNAUTHORIZED, {"error": "authentication failed"}) + return + self.server.login_limiter.succeeded(key) + token, csrf_token = self.server.sessions.create(user, store_cookie) + self._json(HTTPStatus.CREATED, + {"user": user, "csrf_token": csrf_token}, + {"Set-Cookie": session_cookie(token, lifetime), + "Vary": "Cookie"}) + + def _mail_send(self): + session = self._session() + if session is None: + self._json(HTTPStatus.UNAUTHORIZED, + {"error": "authentication required"}, {"Vary": "Cookie"}) + return + if not valid_csrf(session, self.headers.get("X-Bongo-CSRF", "")): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + try: + data = self._request_data() + send_message(session["user"], data) + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except TypeError: + self._empty(HTTPStatus.UNSUPPORTED_MEDIA_TYPE) + return + except (UnicodeError, ValueError, json.JSONDecodeError): + self._json(HTTPStatus.BAD_REQUEST, {"error": "invalid message"}) + return + except Exception: + logging.exception("Could not submit webmail message") + self._json(HTTPStatus.BAD_GATEWAY, {"error": "queue unavailable"}) + return + self._empty(HTTPStatus.NO_CONTENT, {"Vary": "Cookie"}) + + def _task_state(self): + session = self._session() + if session is None: + self._json(HTTPStatus.UNAUTHORIZED, + {"error": "authentication required"}) + return + if not valid_csrf(session, self.headers.get("X-Bongo-CSRF", "")): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + try: + data = self._request_data() + self.server.tasks.set_state(session["user"], data.get("id"), + str(data.get("state", ""))) + except FileNotFoundError: + self._empty(HTTPStatus.NOT_FOUND) + return + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (TypeError, ValueError, OverflowError, json.JSONDecodeError): + self._empty(HTTPStatus.BAD_REQUEST) + return + self._empty(HTTPStatus.NO_CONTENT, {"Vary": "Cookie"}) + + def do_DELETE(self): + path = self._path() + if path.startswith("/calendars/") and path.endswith(".ics"): + self._calendar_delete(path) + return + if path.startswith("/addressbooks/") and path.endswith(".vcf"): + self._card_delete(path) + return + if path != "/api/session": + self._empty(HTTPStatus.NOT_FOUND) + return + token = self._session_token() + session = self.server.sessions.get(token) + if session is None: + self._json(HTTPStatus.UNAUTHORIZED, {"error": "authentication required"}) + return + if not valid_csrf(session, self.headers.get("X-Bongo-CSRF", "")): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + self.server.sessions.delete(token) + revoke(session["user"], session["store_cookie"]) + self._empty(HTTPStatus.NO_CONTENT, + {"Set-Cookie": expired_session_cookie(), "Vary": "Cookie"}) + + def do_PUT(self): + path = self._path() + if path.startswith("/addressbooks/") and path.endswith(".vcf"): + self._card_put(path) + return + if not (path.startswith("/calendars/") and path.endswith(".ics")): + self._empty(HTTPStatus.NOT_FOUND) + return + session = self._dav_session() + if session is None: + return + try: + if not self._dav_write_allowed(session): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + try: + resource = parse_calendar_path(path, session["user"]) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CalendarNotFound, CalendarPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + if self.headers.get_content_type() != "text/calendar": + self._empty(HTTPStatus.UNSUPPORTED_MEDIA_TYPE) + return + try: + payload = self._request_body(min( + int(self.server.config.get("maximum_request_size", 10485760)), + 1048576)) + created, etag = write_event( + session["user"], session["store_cookie"], resource["event"], + payload, self.headers.get("If-Match"), + self.headers.get("If-None-Match")) + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except (UnicodeError, ValueError): + self._empty(HTTPStatus.BAD_REQUEST) + return + except PreconditionFailed: + self._empty(HTTPStatus.PRECONDITION_FAILED) + return + except RuntimeError: + logging.exception("Could not store CalDAV event") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + self._empty(HTTPStatus.CREATED if created else HTTPStatus.NO_CONTENT, + {"ETag": etag}) + finally: + self._release_dav_session(session) + + def _card_put(self, path): + session = self._dav_session() + if session is None: + return + try: + if not self._dav_write_allowed(session): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + try: + resource = parse_card_path(path, session["user"]) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CardNotFound, CardPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + if self.headers.get_content_type() not in ("text/vcard", + "text/x-vcard"): + self._empty(HTTPStatus.UNSUPPORTED_MEDIA_TYPE) + return + try: + payload = self._request_body(min( + int(self.server.config.get("maximum_request_size", 10485760)), + 1048576)) + created, etag = write_card( + session["user"], session["store_cookie"], resource["card"], + payload, self.headers.get("If-Match"), + self.headers.get("If-None-Match")) + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except (UnicodeError, ValueError): + self._empty(HTTPStatus.BAD_REQUEST) + return + except CardPreconditionFailed: + self._empty(HTTPStatus.PRECONDITION_FAILED) + return + except RuntimeError: + logging.exception("Could not store CardDAV contact") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + self._empty(HTTPStatus.CREATED if created else HTTPStatus.NO_CONTENT, + {"ETag": etag}) + finally: + self._release_dav_session(session) + + def _card_delete(self, path): + session = self._dav_session() + if session is None: + return + try: + if not self._dav_write_allowed(session): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + try: + resource = parse_card_path(path, session["user"]) + delete_card(session["user"], session["store_cookie"], + resource["card"], self.headers.get("If-Match")) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CardNotFound, CardPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + except CardPreconditionFailed: + self._empty(HTTPStatus.PRECONDITION_FAILED) + return + except RuntimeError: + logging.exception("Could not delete CardDAV contact") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + self._empty(HTTPStatus.NO_CONTENT) + finally: + self._release_dav_session(session) + + def _calendar_delete(self, path): + session = self._dav_session() + if session is None: + return + try: + if not self._dav_write_allowed(session): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + try: + resource = parse_calendar_path(path, session["user"]) + delete_event(session["user"], session["store_cookie"], + resource["event"], self.headers.get("If-Match")) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CalendarNotFound, CalendarPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + except PreconditionFailed: + self._empty(HTTPStatus.PRECONDITION_FAILED) + return + except RuntimeError: + logging.exception("Could not delete CalDAV event") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + self._empty(HTTPStatus.NO_CONTENT) + finally: + self._release_dav_session(session) + + def do_OPTIONS(self): + path = self._path() + if path.startswith(("/calendars/", "/addressbooks/", "/principals/")): + session = self._dav_session() + if session is None: + return + try: + self._empty(HTTPStatus.OK, options_headers()) + finally: + self._release_dav_session(session) + else: + self._empty(HTTPStatus.NO_CONTENT, + {"Allow": "OPTIONS, GET, HEAD, POST, DELETE"}) + + def do_PROPFIND(self): + session = self._dav_session() + if session is None: + return + try: + depth_value = self.headers.get("Depth", "0") + if depth_value not in ("0", "1"): + self._empty(HTTPStatus.FORBIDDEN) + return + try: + requested = requested_properties(self._request_body()) + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except (UnicodeError, ValueError): + self._empty(HTTPStatus.BAD_REQUEST) + return + payload = multistatus(self._path(), session["user"], + int(depth_value), requested) + if payload is None: + self._empty(HTTPStatus.NOT_FOUND) + return + self.send_response(HTTPStatus.MULTI_STATUS) + self.send_header("Content-Type", "application/xml; charset=utf-8") + self.send_header("Content-Length", str(len(payload))) + self.send_header("Vary", "Cookie") + self.end_headers() + self.wfile.write(payload) + finally: + self._release_dav_session(session) + + def do_REPORT(self): + session = self._dav_session() + if session is None: + return + try: + path = self._path() + if path.startswith("/addressbooks/"): + self._card_report(session, path) + return + try: + resource = parse_calendar_path(self._path(), session["user"]) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CalendarNotFound, CalendarPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + if resource["kind"] != "calendar": + self._empty(HTTPStatus.NOT_FOUND) + return + try: + report = parse_report(self._request_body()) + events = list_events(session["user"], session["store_cookie"], + report["start"], report["end"]) + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except (UnicodeError, ValueError): + self._empty(HTTPStatus.BAD_REQUEST) + return + except CalendarNotFound: + self._empty(HTTPStatus.NOT_FOUND) + return + except RuntimeError: + logging.exception("Could not query CalDAV events") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + requested_hrefs = report["hrefs"] if report["kind"] == "multiget" \ + else None + payload = event_multistatus(events, report["properties"], + requested_hrefs) + self.send_response(HTTPStatus.MULTI_STATUS) + self.send_header("Content-Type", "application/xml; charset=utf-8") + self.send_header("Content-Length", str(len(payload))) + self.send_header("Vary", "Cookie, Authorization") + self.end_headers() + self.wfile.write(payload) + finally: + self._release_dav_session(session) + + def _card_report(self, session, path): + try: + resource = parse_card_path(path, session["user"]) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CardNotFound, CardPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + if resource["kind"] != "addressbook": + self._empty(HTTPStatus.NOT_FOUND) + return + try: + report = parse_card_report(self._request_body()) + cards = list_cards(session["user"], session["store_cookie"]) + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except (UnicodeError, ValueError): + self._empty(HTTPStatus.BAD_REQUEST) + return + except RuntimeError: + logging.exception("Could not query CardDAV contacts") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + requested_hrefs = report["hrefs"] if report["kind"] == "multiget" \ + else None + payload = card_multistatus(cards, report["properties"], requested_hrefs) + self.send_response(HTTPStatus.MULTI_STATUS) + self.send_header("Content-Type", "application/xml; charset=utf-8") + self.send_header("Content-Length", str(len(payload))) + self.send_header("Vary", "Cookie, Authorization") + self.end_headers() + self.wfile.write(payload) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Bongo web and DAV service") + parser.add_argument("--config", default=DEFAULT_CONFIG) + parser.add_argument("--listen-address") + parser.add_argument("--port", type=int) + parser.add_argument("--tls-certificate") + parser.add_argument("--tls-key") + parser.add_argument("--verbose", action="store_true") + return parser.parse_args() + + +def main(): + args = parse_args() + logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, + format="%(asctime)s %(levelname)s %(message)s") + config = load_config(args.config) + address = args.listen_address or config.get("listen_address", "127.0.0.1") + port = args.port or int(config.get("port", 8080)) + server = BongoWebServer((address, port), BongoWebHandler, config) + if bool(args.tls_certificate) != bool(args.tls_key): + raise SystemExit("both --tls-certificate and --tls-key are required") + if args.tls_certificate: + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.minimum_version = ssl.TLSVersion.TLSv1_2 + context.load_cert_chain(args.tls_certificate, args.tls_key) + server.socket = context.wrap_socket(server.socket, server_side=True) + logging.info("Bongo web service listening on %s:%d", address, port) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + finally: + server.server_close() + + +if __name__ == "__main__": + main() diff --git a/src/www/bongo_web/__init__.py b/src/www/bongo_web/__init__.py new file mode 100644 index 0000000..018eef0 --- /dev/null +++ b/src/www/bongo_web/__init__.py @@ -0,0 +1 @@ +"""Security-sensitive support code for the Bongo web service.""" diff --git a/src/www/bongo_web/auth.py b/src/www/bongo_web/auth.py new file mode 100644 index 0000000..d196b12 --- /dev/null +++ b/src/www/bongo_web/auth.py @@ -0,0 +1,73 @@ +"""Authentication adapter between HTTP and the Bongo Store.""" + +import hmac +import logging +import threading +import time + + +class LoginLimiter: + def __init__(self, attempts=5, window=300): + self.attempts = int(attempts) + self.window = int(window) + self.failures = {} + self.lock = threading.Lock() + + def allowed(self, key): + now = time.monotonic() + with self.lock: + recent = [stamp for stamp in self.failures.get(key, ()) + if now - stamp < self.window] + self.failures[key] = recent + return len(recent) < self.attempts + + def failed(self, key): + with self.lock: + self.failures.setdefault(key, []).append(time.monotonic()) + + def succeeded(self, key): + with self.lock: + self.failures.pop(key, None) + + +def authenticate(user, password, lifetime): + if not user or not password or len(user) > 320 or len(password) > 4096: + return None + client = None + try: + from bongo.store.StoreClient import StoreClient + client = StoreClient(user, user, authPassword=password) + return client.CookieNew(int(lifetime)) + except Exception: + logging.info("Bongo Store authentication failed for user %r", user) + return None + finally: + if client is not None: + try: + client.Quit() + except Exception: + logging.warning("Could not close Store connection after login") + + +def revoke(user, store_cookie): + """Best-effort revocation of a server-side Bongo Store cookie.""" + if not user or not store_cookie: + return + client = None + try: + from bongo.store.StoreClient import StoreClient + client = StoreClient(user, user, authCookie=store_cookie) + client.CookieDelete(store_cookie) + except Exception: + logging.warning("Could not revoke Store cookie for user %r", user) + finally: + if client is not None: + try: + client.Quit() + except Exception: + logging.warning("Could not close Store connection after logout") + + +def valid_csrf(session, supplied): + expected = session.get("csrf_token", "") if session else "" + return bool(supplied and hmac.compare_digest(expected, supplied)) diff --git a/src/www/bongo_web/calendar_store.py b/src/www/bongo_web/calendar_store.py new file mode 100644 index 0000000..e0ece14 --- /dev/null +++ b/src/www/bongo_web/calendar_store.py @@ -0,0 +1,202 @@ +"""CalDAV-to-Bongo Store mapping.""" + +import hashlib +import re +from urllib.parse import quote, unquote + + +EVENT_NAME = re.compile(r"^[A-Za-z0-9._~-]{1,240}\.ics$") +EVENT_DOCUMENT_TYPE = 0x0003 + + +class CalendarPathError(ValueError): + pass + + +class CalendarNotFound(LookupError): + pass + + +class PreconditionFailed(RuntimeError): + pass + + +def parse_calendar_path(path, authenticated_user): + parts = path.strip("/").split("/") + if len(parts) not in (3, 4) or parts[0] != "calendars": + raise CalendarPathError("not a calendar resource") + user = unquote(parts[1]) + if user != authenticated_user: + raise PermissionError("calendar owner differs from authenticated user") + if unquote(parts[2]) != "personal": + raise CalendarNotFound("calendar does not exist") + if len(parts) == 3: + return {"kind": "calendar", "user": user, "calendar": "personal"} + event = unquote(parts[3]) + if not EVENT_NAME.fullmatch(event): + raise CalendarPathError("invalid event resource name") + return {"kind": "event", "user": user, "calendar": "personal", + "event": event, "store_path": "/events/" + event[:-4]} + + +def event_href(user, event): + return "/calendars/{}/personal/{}".format( + quote(user, safe="@.-_~"), quote(event, safe="._~-")) + + +def open_store(user, store_cookie): + from bongo.store.StoreClient import StoreClient + return StoreClient(user, user, authCookie=store_cookie) + + +def json_to_ical(document): + from libbongo._calendar import bongojson, cal + value = bongojson.ParseString(document.strip()) + if not value: + raise CalendarNotFound("invalid stored calendar object") + output = cal.JsonToIcal(value) + return output.encode("utf-8") if isinstance(output, str) else bytes(output) + + +def ical_to_json(payload): + if len(payload) > 1048576: + raise ValueError("calendar object is too large") + text = payload.decode("utf-8", "strict") + upper = text.upper() + if (upper.count("BEGIN:VCALENDAR") != 1 + or upper.count("END:VCALENDAR") != 1 + or upper.count("BEGIN:VEVENT") != 1 + or upper.count("END:VEVENT") != 1 + or "BEGIN:VTODO" in upper + or "\nUID:" not in upper.replace("\r\n", "\n")): + raise ValueError("exactly one VEVENT with UID is required") + from libbongo._calendar import cal + converted = cal.IcalToJson(text) + if converted is None: + raise ValueError("iCalendar conversion failed") + return str(converted) + + +def read_event(user, store_cookie, event): + client = None + try: + client = open_store(user, store_cookie) + document = client.Read("/events/" + event[:-4]) + payload = json_to_ical(document) + except CalendarNotFound: + raise + except Exception as error: + # Store command failures intentionally become a non-enumerating 404. + raise CalendarNotFound("event not found") from error + finally: + if client is not None: + try: + client.Quit() + except Exception: + pass + return payload, '"{}"'.format(hashlib.sha256(payload).hexdigest()) + + +def make_etag(payload): + return '"{}"'.format(hashlib.sha256(payload).hexdigest()) + + +def etag_matches(condition, current): + if not condition: + return False + return condition.strip() == "*" or current in { + item.strip() for item in condition.split(",") + } + + +def _close(client): + if client is not None: + try: + client.Quit() + except Exception: + pass + + +def write_event(user, store_cookie, event, payload, if_match=None, + if_none_match=None, client_factory=open_store, + to_json=ical_to_json, to_ical=json_to_ical): + """Create or replace one event and return ``(created, etag)``.""" + document = to_json(payload) + path = "/events/" + event[:-4] + client = None + try: + client = client_factory(user, store_cookie) + exists = client.Info(path) is not None + current_etag = None + if exists and (if_match or if_none_match): + current_etag = make_etag(to_ical(client.Read(path))) + if (if_none_match and exists + and etag_matches(if_none_match, current_etag)): + raise PreconditionFailed("event already exists") + if if_match and (not exists or not etag_matches(if_match, current_etag)): + raise PreconditionFailed("event changed") + if exists: + client.Replace(path, document) + else: + client.Write("/events", EVENT_DOCUMENT_TYPE, document, + filename=event[:-4], link="/calendars/personal") + return not exists, make_etag(to_ical(document)) + except (PreconditionFailed, ValueError): + raise + except Exception as error: + raise RuntimeError("could not store calendar event") from error + finally: + _close(client) + + +def delete_event(user, store_cookie, event, if_match=None, + client_factory=open_store, to_ical=json_to_ical): + path = "/events/" + event[:-4] + client = None + try: + client = client_factory(user, store_cookie) + if client.Info(path) is None: + raise CalendarNotFound("event not found") + if if_match: + current_etag = make_etag(to_ical(client.Read(path))) + if not etag_matches(if_match, current_etag): + raise PreconditionFailed("event changed") + client.Delete(path) + except (CalendarNotFound, PreconditionFailed): + raise + except Exception as error: + raise RuntimeError("could not delete calendar event") from error + finally: + _close(client) + + +def list_events(user, store_cookie, start=None, end=None, + client_factory=open_store, to_ical=json_to_ical): + client = None + try: + client = client_factory(user, store_cookie) + calendar = client.Info("/calendars/personal") + if calendar is None: + raise CalendarNotFound("personal calendar not found") + output = [] + for item in client.Events( + calendar.uid, ["nmap.document", "nmap.event.calendars"], + start=start, end=end): + document = item.props.get("nmap.document") + if document is None: + continue + payload = to_ical(document.strip()) + filename = item.filename + if not filename.endswith(".ics"): + filename += ".ics" + if not EVENT_NAME.fullmatch(filename): + continue + output.append({"href": event_href(user, filename), + "etag": make_etag(payload), "data": payload}) + return output + except CalendarNotFound: + raise + except Exception as error: + raise RuntimeError("could not list calendar events") from error + finally: + _close(client) diff --git a/src/www/bongo_web/card_store.py b/src/www/bongo_web/card_store.py new file mode 100644 index 0000000..5ea7e6e --- /dev/null +++ b/src/www/bongo_web/card_store.py @@ -0,0 +1,222 @@ +"""CardDAV-to-Bongo Store mapping and lossless vCard storage.""" + +import hashlib +import json +import re +from urllib.parse import quote, unquote + + +CARD_NAME = re.compile(r"^[A-Za-z0-9._~-]{1,240}\.vcf$") +CONTACT_DOCUMENT_TYPE = 0x0004 +RAW_VCARD_KEY = "bongo.carddav.vcard" + + +class CardPathError(ValueError): + pass + + +class CardNotFound(LookupError): + pass + + +class CardPreconditionFailed(RuntimeError): + pass + + +def parse_card_path(path, authenticated_user): + parts = path.strip("/").split("/") + if len(parts) not in (3, 4) or parts[0] != "addressbooks": + raise CardPathError("not an address-book resource") + user = unquote(parts[1]) + if user != authenticated_user: + raise PermissionError("address-book owner differs from authenticated user") + if unquote(parts[2]) != "personal": + raise CardNotFound("address book does not exist") + if len(parts) == 3: + return {"kind": "addressbook", "user": user, + "addressbook": "personal"} + card = unquote(parts[3]) + if not CARD_NAME.fullmatch(card): + raise CardPathError("invalid card resource name") + return {"kind": "card", "user": user, "addressbook": "personal", + "card": card, "store_path": "/addressbook/personal/" + card[:-4]} + + +def card_href(user, card): + return "/addressbooks/{}/personal/{}".format( + quote(user, safe="@.-_~"), quote(card, safe="._~-")) + + +def _vobject(): + try: + import vobject + except ImportError as error: + raise RuntimeError("the system vobject package is required") from error + return vobject + + +def vcard_to_json(payload): + if len(payload) > 1048576: + raise ValueError("vCard is too large") + text = payload.decode("utf-8", "strict") + upper = text.upper() + if (upper.count("BEGIN:VCARD") != 1 + or upper.count("END:VCARD") != 1 + or "\nUID" not in upper.replace("\r\n", "\n") + or "\nFN" not in upper.replace("\r\n", "\n")): + raise ValueError("exactly one vCard with UID and FN is required") + card = _vobject().readOne(text) + if card.name.upper() != "VCARD" or not card.validate(): + raise ValueError("invalid vCard") + data = {RAW_VCARD_KEY: card.serialize(), "fn": card.fn.value, + "uid": card.uid.value} + emails = [] + for line in card.contents.get("email", []): + entry = {"value": str(line.value)} + for key, values in line.params.items(): + entry[key.lower()] = list(values) + emails.append(entry) + if emails: + data["email"] = emails + return json.dumps(data, ensure_ascii=False, separators=(",", ":")) + + +def json_to_vcard(document, fallback_uid="contact"): + data = json.loads(document.strip()) + raw = data.get(RAW_VCARD_KEY) + if raw: + return raw.encode("utf-8") + vobject = _vobject() + card = vobject.vCard() + card.add("uid").value = str(data.get("uid", fallback_uid)) + card.add("fn").value = str(data.get("fn") or data.get("fullName") + or fallback_uid) + for entry in data.get("email", []): + value = entry.get("value") or entry.get("address") + if not value: + continue + line = card.add("email") + line.value = str(value) + for key, values in entry.items(): + if key not in ("value", "address"): + line.params[key.upper()] = (values if isinstance(values, list) + else [str(values)]) + return card.serialize().encode("utf-8") + + +def make_etag(payload): + return '"{}"'.format(hashlib.sha256(payload).hexdigest()) + + +def open_store(user, store_cookie): + from bongo.store.StoreClient import StoreClient + return StoreClient(user, user, authCookie=store_cookie) + + +def etag_matches(condition, current): + if not condition: + return False + return condition.strip() == "*" or current in { + item.strip() for item in condition.split(",") + } + + +def _close(client): + if client is not None: + try: + client.Quit() + except Exception: + pass + + +def read_card(user, store_cookie, card, client_factory=open_store, + converter=json_to_vcard): + client = None + try: + client = client_factory(user, store_cookie) + path = "/addressbook/personal/" + card[:-4] + if client.Info(path) is None: + raise CardNotFound("card not found") + payload = converter(client.Read(path), card[:-4]) + return payload, make_etag(payload) + except CardNotFound: + raise + except Exception as error: + raise RuntimeError("could not read card") from error + finally: + _close(client) + + +def write_card(user, store_cookie, card, payload, if_match=None, + if_none_match=None, client_factory=open_store, + to_json=vcard_to_json, to_vcard=json_to_vcard): + document = to_json(payload) + path = "/addressbook/personal/" + card[:-4] + client = None + try: + client = client_factory(user, store_cookie) + exists = client.Info(path) is not None + current_etag = None + if exists and (if_match or if_none_match): + current_etag = make_etag(to_vcard(client.Read(path), card[:-4])) + if if_none_match and exists and etag_matches(if_none_match, current_etag): + raise CardPreconditionFailed("card already exists") + if if_match and (not exists or not etag_matches(if_match, current_etag)): + raise CardPreconditionFailed("card changed") + if exists: + client.Replace(path, document) + else: + client.Write("/addressbook/personal", CONTACT_DOCUMENT_TYPE, + document, filename=card[:-4]) + return not exists, make_etag(to_vcard(document, card[:-4])) + except (CardPreconditionFailed, ValueError): + raise + except Exception as error: + raise RuntimeError("could not store card") from error + finally: + _close(client) + + +def delete_card(user, store_cookie, card, if_match=None, + client_factory=open_store, converter=json_to_vcard): + path = "/addressbook/personal/" + card[:-4] + client = None + try: + client = client_factory(user, store_cookie) + if client.Info(path) is None: + raise CardNotFound("card not found") + if if_match: + current = make_etag(converter(client.Read(path), card[:-4])) + if not etag_matches(if_match, current): + raise CardPreconditionFailed("card changed") + client.Delete(path) + except (CardNotFound, CardPreconditionFailed): + raise + except Exception as error: + raise RuntimeError("could not delete card") from error + finally: + _close(client) + + +def list_cards(user, store_cookie, client_factory=open_store, + converter=json_to_vcard): + client = None + try: + client = client_factory(user, store_cookie) + output = [] + for item in client.List("/addressbook/personal", ["nmap.document"]): + document = item.props.get("nmap.document") + if document is None: + continue + filename = item.filename.rsplit("/", 1)[-1] + card = filename if filename.endswith(".vcf") else filename + ".vcf" + if not CARD_NAME.fullmatch(card): + continue + payload = converter(document, filename) + output.append({"href": card_href(user, card), + "etag": make_etag(payload), "data": payload}) + return output + except Exception as error: + raise RuntimeError("could not list cards") from error + finally: + _close(client) diff --git a/src/www/bongo_web/compose.py b/src/www/bongo_web/compose.py new file mode 100644 index 0000000..b3b7cfe --- /dev/null +++ b/src/www/bongo_web/compose.py @@ -0,0 +1,107 @@ +"""Build and submit messages created by the webmail composer.""" + +from __future__ import annotations + +from email.headerregistry import Address +from email.message import EmailMessage +from email.policy import SMTP +from email.utils import getaddresses + + +MAX_RECIPIENTS = 100 +MAX_SUBJECT = 998 +MAX_BODY = 1024 * 1024 + + +def _address(value: str) -> str: + if not value or any(character in value for character in "\r\n\0"): + raise ValueError("invalid email address") + try: + parsed = Address(addr_spec=value) + except (TypeError, ValueError) as error: + raise ValueError("invalid email address") from error + if not parsed.username or not parsed.domain: + raise ValueError("invalid email address") + domain = parsed.domain.encode("idna").decode("ascii") + return str(Address(username=parsed.username, domain=domain)) + + +def _addresses(value) -> list[tuple[str, str]]: + if value is None: + return [] + if isinstance(value, list): + raw = [str(item) for item in value] + elif isinstance(value, str): + raw = [value] + else: + raise ValueError("address list must be text or a list") + if any(any(character in item for character in "\r\n\0") for item in raw): + raise ValueError("invalid email address") + parsed = getaddresses(raw) + if not parsed or any(not address for _name, address in parsed): + raise ValueError("invalid email address") + result = [] + for name, address in parsed: + result.append((name.strip(), _address(address))) + return result + + +def build_message(sender: str, data: dict) -> tuple[EmailMessage, list[str]]: + """Return a validated RFC message and its envelope recipients.""" + sender = _address(sender) + recipients = {header: _addresses(data.get(header.casefold())) + for header in ("To", "Cc", "Bcc")} + envelope = [address for values in recipients.values() + for _name, address in values] + if not envelope: + raise ValueError("at least one recipient is required") + if len(envelope) > MAX_RECIPIENTS: + raise ValueError("too many recipients") + + subject = str(data.get("subject", "")) + body = str(data.get("text", "")) + if any(character in subject for character in "\r\n\0"): + raise ValueError("invalid subject") + if len(subject) > MAX_SUBJECT or len(body.encode("utf-8")) > MAX_BODY: + raise ValueError("message is too large") + + message = EmailMessage(policy=SMTP) + message["From"] = sender + for header in ("To", "Cc"): + if recipients[header]: + message[header] = [Address(display_name=name, addr_spec=address) + for name, address in recipients[header]] + message["Subject"] = subject + message.set_content(body) + return message, envelope + + +def send_message(sender: str, data: dict, queue_factory=None) -> None: + """Submit a composed message to Bongo's native queue service.""" + message, recipients = build_message(sender, data) + if queue_factory is None: + from bongo.store.QueueClient import QueueClient + queue_factory = QueueClient + queue = queue_factory(sender) + created = False + try: + queue.Create() + created = True + queue.StoreFrom(_address(sender), _address(sender)) + for recipient in recipients: + queue.StoreTo(recipient, recipient) + # SMTP policy emits a seven/eight-bit clean wire representation with CRLF. + queue.StoreMessage(message.as_string(policy=SMTP)) + queue.Run() + except Exception: + if created: + try: + queue.Abort() + except Exception: + pass + raise + finally: + try: + queue.Quit() + except Exception: + pass diff --git a/src/www/bongo_web/dav.py b/src/www/bongo_web/dav.py new file mode 100644 index 0000000..9d365ba --- /dev/null +++ b/src/www/bongo_web/dav.py @@ -0,0 +1,281 @@ +"""Small, dependency-free DAV discovery helpers. + +This module deliberately implements only the discovery surface advertised by +the server. Calendar object reads and writes are added separately once their +Store mapping has been verified. +""" + +from datetime import datetime, timezone +from urllib.parse import quote +from xml.etree import ElementTree as ET + + +DAV = "DAV:" +CALDAV = "urn:ietf:params:xml:ns:caldav" +CARDDAV = "urn:ietf:params:xml:ns:carddav" +CS = "http://calendarserver.org/ns/" + +ET.register_namespace("D", DAV) +ET.register_namespace("C", CALDAV) +ET.register_namespace("A", CARDDAV) +ET.register_namespace("CS", CS) + + +def qname(namespace, name): + return f"{{{namespace}}}{name}" + + +def requested_properties(body): + """Return requested Clark names, or None for DAV:allprop/empty bodies.""" + if not body or not body.strip(): + return None + if len(body) > 65536: + raise ValueError("PROPFIND document is too large") + root = ET.fromstring(body) + if root.tag != qname(DAV, "propfind"): + raise ValueError("DAV:propfind required") + prop = root.find(qname(DAV, "prop")) + if prop is None: + return None + return [child.tag for child in prop] + + +def _parse_utc(value): + if not value: + return None + try: + return datetime.strptime(value, "%Y%m%dT%H%M%SZ").replace( + tzinfo=timezone.utc) + except ValueError as error: + raise ValueError("invalid CalDAV time-range") from error + + +def parse_report(body): + if not body or len(body) > 65536: + raise ValueError("invalid REPORT document") + root = ET.fromstring(body) + query = qname(CALDAV, "calendar-query") + multiget = qname(CALDAV, "calendar-multiget") + if root.tag not in (query, multiget): + raise ValueError("unsupported calendar REPORT") + prop = root.find(qname(DAV, "prop")) + properties = [child.tag for child in prop] if prop is not None else [] + result = {"kind": "query" if root.tag == query else "multiget", + "properties": properties, "start": None, "end": None, + "hrefs": []} + if root.tag == query: + time_range = root.find(f".//{qname(CALDAV, 'time-range')}") + if time_range is not None: + result["start"] = _parse_utc(time_range.get("start")) + result["end"] = _parse_utc(time_range.get("end")) + if bool(result["start"]) != bool(result["end"]): + raise ValueError("both time-range bounds are required") + else: + result["hrefs"] = [element.text for element in + root.findall(qname(DAV, "href")) if element.text] + if len(result["hrefs"]) > 1000: + raise ValueError("too many calendar resources requested") + return result + + +def parse_card_report(body): + if not body or len(body) > 65536: + raise ValueError("invalid REPORT document") + root = ET.fromstring(body) + query = qname(CARDDAV, "addressbook-query") + multiget = qname(CARDDAV, "addressbook-multiget") + if root.tag not in (query, multiget): + raise ValueError("unsupported address-book REPORT") + prop = root.find(qname(DAV, "prop")) + properties = [child.tag for child in prop] if prop is not None else [] + hrefs = [] + if root.tag == multiget: + hrefs = [element.text for element in root.findall(qname(DAV, "href")) + if element.text] + if len(hrefs) > 1000: + raise ValueError("too many address-book resources requested") + return {"kind": "query" if root.tag == query else "multiget", + "properties": properties, "hrefs": hrefs} + + +def event_multistatus(events, properties, requested_hrefs=None): + by_href = {event["href"]: event for event in events} + hrefs = requested_hrefs if requested_hrefs is not None else list(by_href) + root = ET.Element(qname(DAV, "multistatus")) + for href in hrefs: + response = ET.SubElement(root, qname(DAV, "response")) + ET.SubElement(response, qname(DAV, "href")).text = href + event = by_href.get(href) + if event is None: + ET.SubElement(response, qname(DAV, "status")).text = \ + "HTTP/1.1 404 Not Found" + continue + propstat = ET.SubElement(response, qname(DAV, "propstat")) + prop = ET.SubElement(propstat, qname(DAV, "prop")) + for name in properties: + if name == qname(DAV, "getetag"): + ET.SubElement(prop, name).text = event["etag"] + elif name == qname(CALDAV, "calendar-data"): + data = event["data"] + ET.SubElement(prop, name).text = data.decode("utf-8") + ET.SubElement(propstat, qname(DAV, "status")).text = \ + "HTTP/1.1 200 OK" + return ET.tostring(root, encoding="utf-8", xml_declaration=True) + + +def card_multistatus(cards, properties, requested_hrefs=None): + by_href = {card["href"]: card for card in cards} + hrefs = requested_hrefs if requested_hrefs is not None else list(by_href) + root = ET.Element(qname(DAV, "multistatus")) + for href in hrefs: + response = ET.SubElement(root, qname(DAV, "response")) + ET.SubElement(response, qname(DAV, "href")).text = href + card = by_href.get(href) + if card is None: + ET.SubElement(response, qname(DAV, "status")).text = \ + "HTTP/1.1 404 Not Found" + continue + propstat = ET.SubElement(response, qname(DAV, "propstat")) + prop = ET.SubElement(propstat, qname(DAV, "prop")) + for name in properties: + if name == qname(DAV, "getetag"): + ET.SubElement(prop, name).text = card["etag"] + elif name == qname(CARDDAV, "address-data"): + ET.SubElement(prop, name).text = card["data"].decode("utf-8") + ET.SubElement(propstat, qname(DAV, "status")).text = \ + "HTTP/1.1 200 OK" + return ET.tostring(root, encoding="utf-8", xml_declaration=True) + + +def _resource_type(*types): + element = ET.Element(qname(DAV, "resourcetype")) + for namespace, name in types: + ET.SubElement(element, qname(namespace, name)) + return element + + +def _href_property(name, href, namespace=DAV): + element = ET.Element(qname(namespace, name)) + ET.SubElement(element, qname(DAV, "href")).text = href + return element + + +def discovery_resources(user): + encoded = quote(user, safe="@.-_~") + principal = f"/principals/{encoded}/" + home = f"/calendars/{encoded}/" + calendar = f"{home}personal/" + address_home = f"/addressbooks/{encoded}/" + addressbook = f"{address_home}personal/" + return { + "/calendars/": { + qname(DAV, "resourcetype"): _resource_type((DAV, "collection")), + qname(DAV, "displayname"): "Bongo calendars", + qname(DAV, "current-user-principal"): _href_property( + "current-user-principal", principal), + }, + "/principals/": { + qname(DAV, "resourcetype"): _resource_type((DAV, "collection")), + qname(DAV, "displayname"): "Bongo principals", + qname(DAV, "current-user-principal"): _href_property( + "current-user-principal", principal), + }, + principal: { + qname(DAV, "resourcetype"): _resource_type((DAV, "principal")), + qname(DAV, "displayname"): user, + qname(DAV, "current-user-principal"): _href_property( + "current-user-principal", principal), + qname(CALDAV, "calendar-home-set"): _href_property( + "calendar-home-set", home, CALDAV), + qname(CARDDAV, "addressbook-home-set"): _href_property( + "addressbook-home-set", address_home, CARDDAV), + }, + home: { + qname(DAV, "resourcetype"): _resource_type((DAV, "collection")), + qname(DAV, "displayname"): f"Calendars for {user}", + qname(DAV, "owner"): _href_property("owner", principal), + }, + calendar: { + qname(DAV, "resourcetype"): _resource_type( + (DAV, "collection"), (CALDAV, "calendar")), + qname(DAV, "displayname"): "Personal", + qname(DAV, "owner"): _href_property("owner", principal), + qname(CALDAV, "supported-calendar-component-set"): + _component_set(), + qname(CS, "getctag"): "0", + }, + "/addressbooks/": { + qname(DAV, "resourcetype"): _resource_type((DAV, "collection")), + qname(DAV, "displayname"): "Bongo address books", + qname(DAV, "current-user-principal"): _href_property( + "current-user-principal", principal), + }, + address_home: { + qname(DAV, "resourcetype"): _resource_type((DAV, "collection")), + qname(DAV, "displayname"): f"Address books for {user}", + qname(DAV, "owner"): _href_property("owner", principal), + }, + addressbook: { + qname(DAV, "resourcetype"): _resource_type( + (DAV, "collection"), (CARDDAV, "addressbook")), + qname(DAV, "displayname"): "Personal", + qname(DAV, "owner"): _href_property("owner", principal), + qname(CS, "getctag"): "0", + }, + } + + +def _component_set(): + element = ET.Element(qname(CALDAV, "supported-calendar-component-set")) + ET.SubElement(element, qname(CALDAV, "comp"), {"name": "VEVENT"}) + return element + + +def _append_property(parent, name, value): + if isinstance(value, ET.Element): + parent.append(ET.fromstring(ET.tostring(value))) + else: + ET.SubElement(parent, name).text = value + + +def multistatus(path, user, depth, requested=None): + resources = discovery_resources(user) + if path not in resources: + return None + selected = [path] + if depth == 1: + selected.extend(candidate for candidate in resources + if candidate != path + and candidate.startswith(path) + and "/" not in candidate[len(path):].rstrip("/")) + root = ET.Element(qname(DAV, "multistatus")) + for href in selected: + response = ET.SubElement(root, qname(DAV, "response")) + ET.SubElement(response, qname(DAV, "href")).text = href + available = resources[href] + wanted = list(available) if requested is None else requested + found = [name for name in wanted if name in available] + missing = [name for name in wanted if name not in available] + if found: + propstat = ET.SubElement(response, qname(DAV, "propstat")) + prop = ET.SubElement(propstat, qname(DAV, "prop")) + for name in found: + _append_property(prop, name, available[name]) + ET.SubElement(propstat, qname(DAV, "status")).text = \ + "HTTP/1.1 200 OK" + if missing: + propstat = ET.SubElement(response, qname(DAV, "propstat")) + prop = ET.SubElement(propstat, qname(DAV, "prop")) + for name in missing: + ET.SubElement(prop, name) + ET.SubElement(propstat, qname(DAV, "status")).text = \ + "HTTP/1.1 404 Not Found" + return ET.tostring(root, encoding="utf-8", xml_declaration=True) + + +def options_headers(): + return { + "Allow": "OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, REPORT", + "DAV": "1, 3, access-control, calendar-access, addressbook", + "MS-Author-Via": "DAV", + } diff --git a/src/www/bongo_web/filters.py b/src/www/bongo_web/filters.py new file mode 100644 index 0000000..8d23f28 --- /dev/null +++ b/src/www/bongo_web/filters.py @@ -0,0 +1,133 @@ +"""Validated user filters and compilation to Bongo's legacy rule format.""" + +import re + + +ADDRESS = re.compile(r"^[^\s@<>]+@[^\s@<>]+$") +DOMAIN = re.compile(r"^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$") +LIST_ID = re.compile(r"^[A-Za-z0-9_.-]+$") +FOLDER = re.compile(r"^/mail/[^\x00]+$") + +CONDITION_CODES = { + "sender": "B", "recipient": "C", "subject": "D", + "header": "F", "size_more": "N", "size_less": "O", + "mailing_list": "h", "list_id": "i", +} +ACTION_CODES = {"delete": "B", "forward": "C", "copy": "D", + "move": "E", "stop": "F"} + + +def validate_filter(value): + if not isinstance(value, dict): + raise ValueError("filter must be an object") + conditions = value.get("conditions") + actions = value.get("actions") + if not isinstance(conditions, list) or not conditions: + raise ValueError("at least one condition is required") + if not isinstance(actions, list) or not actions: + raise ValueError("at least one action is required") + if len(conditions) > 100 or len(actions) > 20: + raise ValueError("filter is too complex") + join = value.get("match", "all") + if join not in {"all", "any"}: + raise ValueError("invalid condition operator") + for condition in conditions: + _validate_condition(condition) + for action in actions: + _validate_action(action) + return value + + +def _text(value, maximum=998): + value = str(value) + if not value or len(value.encode("utf-8")) > maximum or "\x00" in value: + raise ValueError("invalid filter value") + return value + + +def _validate_condition(condition): + if not isinstance(condition, dict): + raise ValueError("condition must be an object") + kind = condition.get("type") + if kind == "mailing_list": + return + if kind in {"sender", "recipient"}: + value = _text(condition.get("value", ""), 320) + if "@" in value and not ADDRESS.fullmatch(value): + raise ValueError("invalid mail address") + elif kind == "sender_domain": + if not DOMAIN.fullmatch(_text(condition.get("value", ""), 253)): + raise ValueError("invalid sender domain") + elif kind == "contact_group": + _text(condition.get("value", ""), 255) + elif kind == "list_id": + if not LIST_ID.fullmatch(_text(condition.get("value", ""), 255)): + raise ValueError("invalid List-ID") + elif kind in {"subject", "size_more", "size_less"}: + _text(condition.get("value", "")) + elif kind == "header": + name = _text(condition.get("name", ""), 78) + if not re.fullmatch(r"[!-9;-~]+", name): + raise ValueError("invalid header name") + _text(condition.get("value", "")) + else: + raise ValueError("unknown filter condition") + + +def _validate_action(action): + if not isinstance(action, dict): + raise ValueError("action must be an object") + kind = action.get("type") + if kind in {"move", "copy"}: + folder = _text(action.get("folder", ""), 999) + if not FOLDER.fullmatch(folder) or any(part in {"", ".", ".."} + for part in folder.split("/")[2:]): + raise ValueError("invalid destination folder") + elif kind == "forward": + if not ADDRESS.fullmatch(_text(action.get("address", ""), 320)): + raise ValueError("invalid forwarding address") + elif kind not in {"delete", "stop"}: + raise ValueError("unknown filter action") + + +def _argument(code, first="", second=""): + first = first.encode("utf-8") + second = second.encode("utf-8") + if len(first) > 999 or len(second) > 999: + raise ValueError("compiled rule argument is too long") + return (code.encode("ascii") + f"{len(first):03d}".encode("ascii") + first + + b"Z" + f"{len(second):03d}".encode("ascii") + second + b"Z") + + +def compile_filter(value, rule_id, group_resolver=lambda _name: ()): + """Compile a validated filter; contact groups expand to sender OR terms.""" + validate_filter(value) + compiled = [] + for condition in value["conditions"]: + kind = condition["type"] + if kind == "contact_group": + addresses = sorted(set(group_resolver(condition["value"]))) + if not addresses or any(not ADDRESS.fullmatch(address) + for address in addresses): + raise ValueError("contact group has no valid addresses") + compiled.extend(("B", address, "") for address in addresses) + elif kind == "sender_domain": + compiled.append(("B", "@" + condition["value"].casefold(), "")) + elif kind == "header": + compiled.append(("F", condition["name"], condition["value"])) + elif kind == "mailing_list": + compiled.append(("h", "", "")) + else: + compiled.append((CONDITION_CODES[kind], str(condition["value"]), "")) + join = b"X" if value.get("match", "all") == "all" else b"Y" + output = f"{int(rule_id):08X}".encode("ascii") + b"A" + for index, condition in enumerate(compiled): + output += _argument(*condition) + if index + 1 < len(compiled): + output += join + for action in value["actions"]: + kind = action["type"] + argument = (action.get("folder", "")[6:] if kind in {"move", "copy"} + else action.get("address", "") if kind == "forward" else "") + output += _argument(ACTION_CODES[kind], argument) + return output.decode("utf-8") diff --git a/src/www/bongo_web/html_mail.py b/src/www/bongo_web/html_mail.py new file mode 100644 index 0000000..afd4f16 --- /dev/null +++ b/src/www/bongo_web/html_mail.py @@ -0,0 +1,71 @@ +"""Sanitize untrusted HTML mail while preserving safe inline images.""" + +import re +from urllib.parse import urlsplit + + +ALLOWED_TAGS = { + "a", "abbr", "b", "blockquote", "br", "code", "del", "div", "em", + "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", + "li", "ol", "p", "pre", "s", "span", "strong", "sub", "sup", + "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul", +} +ALLOWED_ATTRIBUTES = { + "a": {"href", "title"}, + "img": {"src", "alt", "title", "width", "height"}, + "td": {"colspan", "rowspan", "style"}, + "th": {"colspan", "rowspan", "style"}, + "*": {"style"}, +} +ALLOWED_STYLE_PROPERTIES = { + "background-color", "border", "border-color", "border-style", + "border-width", "color", "font-family", "font-size", "font-style", + "font-weight", "margin", "margin-bottom", "margin-left", "margin-right", + "margin-top", "padding", "padding-bottom", "padding-left", + "padding-right", "padding-top", "text-align", "text-decoration", + "vertical-align", "white-space", +} +DATA_IMAGE = re.compile( + r"^data:image/(?:png|jpeg|gif|webp);base64,[A-Za-z0-9+/=\r\n]+$", + re.IGNORECASE) +MAX_DATA_IMAGE_URL = 350000 + + +def sanitize_html(html, cleaner=None): + """Return sanitized HTML and remote image URLs in placeholder order.""" + if not isinstance(html, str) or len(html.encode("utf-8")) > 4 * 1024 * 1024: + raise ValueError("HTML mail part is too large") + remote_images = [] + + def filter_attribute(tag, attribute, value): + if attribute == "src" and tag == "img": + scheme = urlsplit(value).scheme.casefold() + if scheme in ("http", "https"): + remote_images.append(value) + return "bongo-remote:{}".format(len(remote_images) - 1) + if scheme == "cid": + return value + if scheme == "data" and len(value) <= MAX_DATA_IMAGE_URL \ + and DATA_IMAGE.fullmatch(value): + return value + return None + if attribute == "href" and tag == "a": + return value if urlsplit(value).scheme.casefold() in ( + "http", "https", "mailto") else None + return value + + if cleaner is None: + try: + import nh3 + except ImportError as error: + raise RuntimeError("the system nh3 package is required") from error + cleaner = nh3.clean + sanitized = cleaner( + html, tags=ALLOWED_TAGS, + clean_content_tags={"script", "style", "template", "iframe", "object", + "embed", "form", "svg", "math", "meta", "link"}, + attributes=ALLOWED_ATTRIBUTES, attribute_filter=filter_attribute, + link_rel="noopener noreferrer nofollow", + url_schemes={"http", "https", "mailto", "cid", "data", "bongo-remote"}, + filter_style_properties=ALLOWED_STYLE_PROPERTIES) + return {"html": sanitized, "remote_images": remote_images} diff --git a/src/www/bongo_web/mail_store.py b/src/www/bongo_web/mail_store.py new file mode 100644 index 0000000..aa6f5fe --- /dev/null +++ b/src/www/bongo_web/mail_store.py @@ -0,0 +1,199 @@ +"""Read-only webmail views backed by the Bongo Store.""" + +import base64 +import re +from email import policy +from email.parser import BytesParser +from email.utils import getaddresses + +from .html_mail import sanitize_html + + +CONVERSATION_PROPERTIES = [ + "bongo.from", "nmap.conversation.count", "nmap.conversation.date", + "nmap.conversation.subject", "bongo.conversation.subject", + "nmap.conversation.unread", +] +DOCUMENT_ID = re.compile(r"^[0-9A-Fa-f]{1,64}$") +INLINE_IMAGE_TYPES = {"image/png", "image/jpeg", "image/gif", "image/webp"} +LIST_ID = re.compile(r"<\s*([^<>\s]+)\s*>") + + +def open_store(user, store_cookie): + from bongo.store.StoreClient import StoreClient + return StoreClient(user, user, authCookie=store_cookie) + + +def _close(client): + if client is not None: + try: + client.Quit() + except Exception: + pass + + +def _folder_path(name): + name = str(name) + if not name.startswith("/mail/") or "\x00" in name: + raise ValueError("invalid mail folder") + components = name.split("/")[2:] + if not components or any(part in ("", ".", "..") for part in components): + raise ValueError("invalid mail folder") + return name + + +def list_folders(user, store_cookie, client_factory=open_store): + client = None + try: + client = client_factory(user, store_cookie) + folders = [] + for collection in client.Collections("/mail"): + path = collection.name + if not path.startswith("/mail/"): + path = "/mail/" + path.lstrip("/") + try: + path = _folder_path(path) + except ValueError: + continue + folders.append({"path": path, "name": path.rsplit("/", 1)[-1]}) + return sorted(folders, key=lambda item: (item["name"].upper() != "INBOX", + item["name"].casefold())) + except Exception as error: + raise RuntimeError("could not list mail folders") from error + finally: + _close(client) + + +def _addresses(value): + if not value: + return [] + return [{"name": name, "address": address} + for name, address in getaddresses(value.splitlines()) if address] + + +def list_conversations(user, store_cookie, folder, offset=0, limit=30, + client_factory=open_store): + folder = _folder_path(folder) + offset = int(offset) + limit = int(limit) + if offset < 0 or limit < 1 or limit > 100: + raise ValueError("invalid result range") + client = None + try: + client = client_factory(user, store_cookie) + known = {item["path"] for item in list_folders( + user, store_cookie, client_factory=lambda _u, _c: _Borrowed(client))} + if folder not in known: + raise FileNotFoundError("mail folder not found") + iterator = client.Conversations(folder, CONVERSATION_PROPERTIES, + start=offset, end=offset + limit - 1, + showTotal=True) + conversations = [] + for item in iterator: + props = item.props + conversations.append({ + "id": str(item.uid), + "from": _addresses(props.get("bongo.from", "")), + "subject": props.get("bongo.conversation.subject") + or props.get("nmap.conversation.subject", ""), + "date": props.get("nmap.conversation.date", ""), + "unread": int(props.get("nmap.conversation.unread", 0)), + "count": int(props.get("nmap.conversation.count", 1)), + "flags": int(item.flags), + }) + return {"folder": folder, "offset": offset, "limit": limit, + "total": int(getattr(iterator, "total", len(conversations))), + "conversations": conversations} + except (ValueError, FileNotFoundError): + raise + except Exception as error: + raise RuntimeError("could not list conversations") from error + finally: + _close(client) + + +def _message_content(raw, sanitizer=sanitize_html): + if isinstance(raw, str): + raw = raw.encode("utf-8", "surrogateescape") + if len(raw) > 25 * 1024 * 1024: + raise ValueError("message is too large for web display") + message = BytesParser(policy=policy.default).parsebytes(raw) + plain = None + html = None + inline_images = {} + parts = message.walk() if message.is_multipart() else (message,) + total_inline = 0 + for part in parts: + if part.is_multipart(): + continue + content_type = part.get_content_type().casefold() + disposition = part.get_content_disposition() + if content_type == "text/plain" and disposition != "attachment" and plain is None: + plain = part.get_content() + elif content_type == "text/html" and disposition != "attachment" and html is None: + html = part.get_content() + elif content_type in INLINE_IMAGE_TYPES: + content_id = (part.get("Content-ID") or "").strip("<> ") + payload = part.get_payload(decode=True) or b"" + total_inline += len(payload) + if content_id and len(payload) <= 2 * 1024 * 1024 \ + and total_inline <= 5 * 1024 * 1024: + inline_images[content_id] = "data:{};base64,{}".format( + content_type, base64.b64encode(payload).decode("ascii")) + sanitized = sanitizer(html) if html is not None else None + list_id_header = str(message.get("List-ID", "")) + list_id_match = LIST_ID.search(list_id_header) + list_id = list_id_match.group(1).casefold() if list_id_match else None + list_post = str(message.get("List-Post", "")) + return { + "from": _addresses(str(message.get("From", ""))), + "to": _addresses(str(message.get("To", ""))), + "cc": _addresses(str(message.get("Cc", ""))), + "subject": str(message.get("Subject", "")), + "date": str(message.get("Date", "")), + "message_id": str(message.get("Message-ID", "")), + "text": plain, + "html": sanitized["html"] if sanitized else None, + "remote_images": sanitized["remote_images"] if sanitized else [], + "inline_images": inline_images, + "mailing_list": ({"id": list_id, "post": list_post} + if list_id else None), + } + + +def read_conversation(user, store_cookie, conversation_id, + client_factory=open_store, sanitizer=sanitize_html): + if not DOCUMENT_ID.fullmatch(str(conversation_id)): + raise ValueError("invalid conversation id") + client = None + try: + client = client_factory(user, store_cookie) + output = [] + for item in client.Conversation(conversation_id): + if int(item.type) != 0x0002: + continue + content = _message_content(client.Read(item.uid), sanitizer) + content["id"] = str(item.uid) + content["flags"] = int(item.flags) + output.append(content) + if not output: + raise FileNotFoundError("conversation not found") + return {"id": conversation_id, "messages": output} + except (ValueError, FileNotFoundError): + raise + except Exception as error: + raise RuntimeError("could not read conversation") from error + finally: + _close(client) + + +class _Borrowed: + """Prevent the folder helper from closing a caller-owned Store client.""" + def __init__(self, client): + self.client = client + + def __getattr__(self, name): + return getattr(self.client, name) + + def Quit(self): + pass diff --git a/src/www/bongo_web/session.py b/src/www/bongo_web/session.py new file mode 100644 index 0000000..4b98b5c --- /dev/null +++ b/src/www/bongo_web/session.py @@ -0,0 +1,89 @@ +"""Opaque, server-side web sessions. + +Only a hash of the browser token is stored. The Bongo Store cookie remains +server-side and the user's password is never retained after login. +""" + +import hashlib +import os +import secrets +import sqlite3 +import time + + +COOKIE_NAME = "__Host-bongo-session" + + +class SessionStore: + def __init__(self, path, lifetime=3600): + self.path = path + self.lifetime = int(lifetime) + directory = os.path.dirname(path) + os.makedirs(directory, mode=0o700, exist_ok=True) + os.chmod(directory, 0o700) + self._initialize() + + def _connect(self): + connection = sqlite3.connect(self.path, timeout=5) + connection.execute("PRAGMA busy_timeout=5000") + connection.execute("PRAGMA journal_mode=WAL") + connection.execute("PRAGMA foreign_keys=ON") + return connection + + def _initialize(self): + with self._connect() as database: + database.execute( + "CREATE TABLE IF NOT EXISTS sessions (" + "id_hash BLOB PRIMARY KEY, user TEXT NOT NULL, " + "store_cookie TEXT NOT NULL, csrf_token TEXT NOT NULL, " + "created INTEGER NOT NULL, expires INTEGER NOT NULL)") + database.execute( + "CREATE INDEX IF NOT EXISTS sessions_expiry ON sessions(expires)") + os.chmod(self.path, 0o600) + + @staticmethod + def _hash(token): + return hashlib.sha256(token.encode("ascii")).digest() + + def create(self, user, store_cookie): + token = secrets.token_urlsafe(32) + csrf_token = secrets.token_urlsafe(32) + now = int(time.time()) + with self._connect() as database: + database.execute("DELETE FROM sessions WHERE expires <= ?", (now,)) + database.execute( + "INSERT INTO sessions VALUES (?, ?, ?, ?, ?, ?)", + (self._hash(token), user, store_cookie, csrf_token, + now, now + self.lifetime)) + return token, csrf_token + + def get(self, token): + if not token: + return None + now = int(time.time()) + with self._connect() as database: + row = database.execute( + "SELECT user, store_cookie, csrf_token, expires " + "FROM sessions WHERE id_hash = ? AND expires > ?", + (self._hash(token), now)).fetchone() + if row is None: + return None + return {"user": row[0], "store_cookie": row[1], + "csrf_token": row[2], "expires": row[3]} + + def delete(self, token): + if not token: + return + with self._connect() as database: + database.execute("DELETE FROM sessions WHERE id_hash = ?", + (self._hash(token),)) + + +def session_cookie(token, max_age): + return (f"{COOKIE_NAME}={token}; Path=/; Max-Age={int(max_age)}; " + "Secure; HttpOnly; SameSite=Strict") + + +def expired_session_cookie(): + return (f"{COOKIE_NAME}=; Path=/; Max-Age=0; " + "Secure; HttpOnly; SameSite=Strict") diff --git a/src/www/bongo_web/tasks.py b/src/www/bongo_web/tasks.py new file mode 100644 index 0000000..1c6b70b --- /dev/null +++ b/src/www/bongo_web/tasks.py @@ -0,0 +1,94 @@ +"""Persistent, per-user decisions surfaced by the web UI.""" + +import json +import sqlite3 +import time + + +VALID_STATES = {"open", "ignored", "disabled", "completed"} +VALID_SEVERITIES = {"info", "warning", "critical"} + + +class TaskStore: + def __init__(self, path): + self.path = path + with self._connect() as database: + database.executescript(""" + CREATE TABLE IF NOT EXISTS user_tasks ( + id INTEGER PRIMARY KEY, + user TEXT NOT NULL, + task_type TEXT NOT NULL, + object_key TEXT NOT NULL, + severity TEXT NOT NULL DEFAULT 'info', + title TEXT NOT NULL, + details TEXT NOT NULL DEFAULT '{}', + state TEXT NOT NULL DEFAULT 'open', + dismissible INTEGER NOT NULL DEFAULT 1, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + UNIQUE(user, task_type, object_key) + ); + CREATE INDEX IF NOT EXISTS user_tasks_open + ON user_tasks(user, state, severity); + """) + + def _connect(self): + database = sqlite3.connect(self.path, timeout=5) + database.row_factory = sqlite3.Row + database.execute("PRAGMA journal_mode=WAL") + database.execute("PRAGMA busy_timeout=5000") + return database + + def upsert(self, user, task_type, object_key, title, details=None, + severity="info", dismissible=True): + if severity not in VALID_SEVERITIES: + raise ValueError("invalid task severity") + now = int(time.time()) + payload = json.dumps(details or {}, ensure_ascii=False, + separators=(",", ":")) + with self._connect() as database: + database.execute(""" + INSERT INTO user_tasks + (user, task_type, object_key, severity, title, details, + dismissible, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(user, task_type, object_key) DO UPDATE SET + severity=excluded.severity, title=excluded.title, + details=excluded.details, updated_at=excluded.updated_at + WHERE user_tasks.state NOT IN ('disabled', 'completed') + """, (user, task_type, object_key, severity, title, payload, + int(dismissible), now, now)) + + def list(self, user, include_closed=False): + clause = "" if include_closed else " AND state = 'open'" + with self._connect() as database: + rows = database.execute( + "SELECT * FROM user_tasks WHERE user = ?" + clause + + " ORDER BY CASE severity WHEN 'critical' THEN 0 " + "WHEN 'warning' THEN 1 ELSE 2 END, updated_at DESC", (user,)) + return [self._row(row) for row in rows] + + def set_state(self, user, task_id, state): + if state not in VALID_STATES: + raise ValueError("invalid task state") + with self._connect() as database: + row = database.execute( + "SELECT severity, dismissible FROM user_tasks " + "WHERE id = ? AND user = ?", (int(task_id), user)).fetchone() + if row is None: + raise FileNotFoundError("task not found") + if state in {"ignored", "disabled"} and (not row["dismissible"] + or row["severity"] == "critical"): + raise PermissionError("task cannot be dismissed") + database.execute( + "UPDATE user_tasks SET state = ?, updated_at = ? " + "WHERE id = ? AND user = ?", + (state, int(time.time()), int(task_id), user)) + + @staticmethod + def _row(row): + return {"id": row["id"], "type": row["task_type"], + "key": row["object_key"], "severity": row["severity"], + "title": row["title"], "details": json.loads(row["details"]), + "state": row["state"], "dismissible": bool(row["dismissible"]), + "created_at": row["created_at"], "updated_at": row["updated_at"]} diff --git a/src/www/static/Bongo-logo.png b/src/www/static/Bongo-logo.png new file mode 100755 index 0000000..a752adc Binary files /dev/null and b/src/www/static/Bongo-logo.png differ diff --git a/src/www/static/agent-control.png b/src/www/static/agent-control.png new file mode 100644 index 0000000..b631d86 Binary files /dev/null and b/src/www/static/agent-control.png differ diff --git a/src/www/static/app.css b/src/www/static/app.css new file mode 100644 index 0000000..5b28193 --- /dev/null +++ b/src/www/static/app.css @@ -0,0 +1,82 @@ +:root { font: 14px/1.45 "Lucida Grande", Verdana, "Bitstream Vera Sans", sans-serif; color: #111; background: #e4eaf4; } +* { box-sizing: border-box; } +html, body { margin: 0; min-height: 100%; } +body { background: #e7e4ef url('/assets/bg-page.png') repeat-x 0 0; } +button, input, select { font: inherit; } +button { cursor: pointer; } +[hidden] { display: none !important; } +.muted { color: #59677a; } +.error { color: #b00000; font-weight: bold; } + +/* Dragonfly login, retaining the original Bongo color and spacing model. */ +.login { position: absolute; top: 25%; left: 0; right: 0; min-height: 190px; padding: 2.8rem 1rem 1.2rem; text-align: center; color: #333; border-top: 2px solid #d4dceb; border-bottom: 2px solid #e4eaf4; background: #b4c5e1 url('/assets/login-bg-middle.png') repeat-x 0 0; } +.login-bg-top, .login-bg-bottom { position: absolute; left: 0; right: 0; width: 100%; pointer-events: none; } +.login-bg-top { top: -188px; height: 185px; border-bottom: 1px solid #385798; background: url('/assets/login-bg-top.png') repeat-x 0 100%; } +.login-bg-bottom { bottom: -296px; height: 293px; border-top: 1px solid #1d3980; background: url('/assets/login-bg-bottom.png') repeat-x 0 0; } +.brand { position: absolute; top: -76px; left: 50%; width: 339px; max-width: calc(100% - 2rem); height: auto; transform: translateX(-50%); } +.login-message { margin: 0 0 .8rem; font-size: 87%; } +#login-form { display: grid; grid-template-columns: max-content 15rem; gap: .45rem .75rem; justify-content: center; align-items: center; text-align: right; } +#login-form label { font-weight: normal; } +#login-form input { width: 15rem; min-height: 1.8rem; padding: .2rem .35rem; color: #000; background: #fff; border: 1px solid #7f8da5; } +#login-form .error, #login-form button { grid-column: 2; } +#login-form button { justify-self: end; padding: .25rem 1rem; color: #111; border: 1px solid #687a96; border-radius: 2px; background: linear-gradient(#fff, #c5d2e5); } + +/* Original Dragonfly page proportions, implemented with modern grid. */ +main { min-height: 100vh; } +.shell { min-height: 100vh; display: grid; grid-template-columns: 14.5rem minmax(0, 1fr); grid-template-rows: auto 1fr; } +header { grid-column: 1 / -1; display: flex; align-items: center; min-height: 68px; padding: 10px 15px; color: #fff; border-top: 4px solid #ffac35; background: #1d3980 url('/assets/top-gradient.png') repeat-y center; } +.wordmark { display: block; margin-right: auto; } +.wordmark img { display: block; width: 240px; max-width: 38vw; height: auto; } +#current-user { font-size: 1.15rem; font-weight: bold; } +.task-button { margin-left: .8rem; border: 1px solid #8b6f00; border-radius: 1rem; background: #eee; color: #333; font-weight: bold; } +.task-button.has-tasks { background: #ffd75a; color: #5c3900; } +.task-panel { margin: .5rem 0 1rem; padding: .8rem; border: 1px solid #d5b64c; background: #fffbea; } +.task-panel h2 { margin-top: 0; } +.task-panel ol { margin: 0; padding-left: 1.5rem; } +.task { margin: .4rem 0; } +.task > span { margin: 0 .8rem; color: #555; } +.task.critical { border-left: 4px solid #b00020; padding-left: .5rem; } +.task button { margin-left: .35rem; } +.settings-button { display: inline-block; margin-left: .5rem; padding: .1rem .45rem; border: 1px solid #7185a0; border-radius: 50%; background: #f4f6f9; color: #324d70; font-size: 1.2rem; line-height: 1.4; text-decoration: none; } +.settings-button:hover { background: #fff; color: #f57900; } +.settings-view { display: grid; grid-template-columns: minmax(180px, 240px) 1fr; gap: 1rem; } +.settings-view > h2 { grid-column: 1 / -1; } +.settings-nav { display: flex; flex-direction: column; align-items: stretch; } +.settings-nav button { padding: .6rem; border: 0; border-bottom: 1px solid #ccd5e0; background: #eef3f8; text-align: left; } +.settings-nav button.active { background: #bbceea; color: #1d3980; font-weight: bold; } +#settings-content { min-height: 14rem; padding: .8rem; border: 1px solid #ccd5e0; background: #fff; } +.settings-task-list { margin-top: 0; } +.quiet { margin-left: 1rem; padding: .25rem .6rem; color: #e4eaf4; border: 1px solid #7a9ac9; background: transparent; } +#section-sidebar { align-self: start; } +#sidebar-nav { min-width: 120px; margin: 12px 0 0 12px; padding: .4rem 0 1rem; background: #fff; border: 1px solid #5f85bf; border-width: 1px 0 1px 1px; } +#sidebar-nav a { display: flex; align-items: center; gap: .6rem; min-height: 30px; margin: .35rem 0; padding: 6px 12px 6px 18px; color: #517faf; font-weight: bold; text-decoration: underline; } +#sidebar-nav a:hover { color: #f57900; background: #f2f5fa; } +#sidebar-nav .tab:has(a.active) { color: #1d3980; border: 1px solid #95a9df; border-width: 1px 0; background: #bbceea url('/assets/bg-selected.png') repeat-x 50% 50%; } +#sidebar-nav a img { width: 22px; height: 22px; object-fit: contain; } +.workspace { min-width: 0; margin-top: 12px; padding: 1rem 1.25rem; background: #fff; border: 1px solid #95a9df; border-right: 0; } +.workspace > h1 { margin: 0 0 .25rem; color: #1d3980; font-size: 1.45rem; } +select { padding: .25rem; border: 1px solid #7f8da5; background: #fff; } +.small { margin: 0 0 0 .35rem; padding: .25rem .7rem; color: #111; border: 1px solid #687a96; background: linear-gradient(#fff, #c5d2e5); } +#conversation-list { margin: 1rem 0 0; padding: 0; list-style: none; border-top: 1px solid #afbdd2; } +#conversation-list li { padding: 0; border-bottom: 1px solid #ccd5e2; } +#conversation-list li.unread { font-weight: bold; background: #f4f7fb; } +#conversation-list button { display: grid; width: 100%; grid-template-columns: minmax(10rem, 1fr) 3fr auto; gap: 1rem; padding: .55rem; color: inherit; text-align: left; border: 0; background: transparent; } +.sender, .subject { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.message { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #afbdd2; } +.message-addresses { color: #59677a; overflow-wrap: anywhere; } +.message-text { white-space: pre-wrap; overflow-wrap: anywhere; } +.message-html { width: 100%; min-height: 20rem; border: 1px solid #afbdd2; background: #fff; } + +@media (max-width: 700px) { + .login { top: 20%; } + #login-form { grid-template-columns: 1fr; text-align: left; } + #login-form input { width: 100%; } + #login-form .error, #login-form button { grid-column: 1; } + .shell { grid-template-columns: 1fr; grid-template-rows: auto auto 1fr; } + header { grid-column: 1; } + #sidebar-nav { display: flex; margin: 0; overflow-x: auto; border-width: 0 0 1px; } + #sidebar-nav .tab { flex: 0 0 auto; } + #sidebar-nav a { margin: 0; } + .workspace { margin: 0; border-width: 0; } + #conversation-list button { grid-template-columns: 1fr; gap: .15rem; } +} diff --git a/src/www/static/app.js b/src/www/static/app.js new file mode 100644 index 0000000..3063a1e --- /dev/null +++ b/src/www/static/app.js @@ -0,0 +1,310 @@ +"use strict"; + +const loginView = document.querySelector("#login-pane"); +const appView = document.querySelector("#app-view"); +const loginForm = document.querySelector("#login-form"); +const loginError = document.querySelector("#login-error"); +let session = null; +let messages = {}; +let openConversation = null; +let composing = false; +let tasks = []; +const tr = text => messages[text] || text; + +function section() { + if (location.pathname === "/admin") return "admin"; + if (location.pathname === "/settings") return "settings"; + return location.hash === "#calendar" ? "calendar" : + location.hash === "#contacts" ? "contacts" : "mail"; +} + +function render() { + document.body.classList.toggle("login", !session); + loginView.hidden = Boolean(session); + appView.hidden = !session; + if (!session) return; + document.querySelector("#current-user").textContent = session.user; + document.querySelector("#compose-from").value = session.user; + const names = {mail: tr("Mail"), calendar: tr("Calendar"), contacts: tr("Contacts"), admin: tr("Administration"), settings: tr("Settings")}; + const messages = { + mail: tr("The secure webmail connection is being enabled."), + calendar: tr("Calendar access is available through CalDAV."), + contacts: tr("Contact access is available through CardDAV."), + admin: tr("Administrative functions require a server-side role check."), + settings: tr("Personal settings") + }; + const current = section(); + document.querySelector("#section-title").textContent = names[current]; + document.querySelector("#section-message").textContent = messages[current]; + document.querySelector("#mail-browser").hidden = current !== "mail" || Boolean(openConversation) || composing; + document.querySelector("#message-view").hidden = current !== "mail" || !openConversation || composing; + document.querySelector("#message-compose").hidden = current !== "mail" || !composing; + document.querySelector("#settings-view").hidden = current !== "settings"; + document.querySelectorAll("nav a").forEach(link => link.classList.toggle("active", link.dataset.section === current)); + if (current === "mail") loadFolders(); + if (current === "settings") renderSettings("tasks"); +} + +async function renderSettings(name) { + const content = document.querySelector("#settings-content"); + document.querySelectorAll(".settings-nav button").forEach(button => + button.classList.toggle("active", button.dataset.settings === name)); + if (name === "tasks") { + const response = await fetch("/api/tasks?include_closed=1", {credentials: "same-origin"}); + if (!response.ok) return; + const allTasks = (await response.json()).tasks; + const list = document.createElement("ol"); + list.className = "settings-task-list"; + list.replaceChildren(...allTasks.map(task => { + const item = document.createElement("li"); + item.textContent = `${task.title}: ${task.details.list_id || task.key} (${task.state})`; + return item; + })); + content.replaceChildren(list); + return; + } + const heading = document.createElement("h3"); + const button = document.querySelector(`.settings-nav button[data-settings="${name}"]`); + heading.textContent = button ? button.textContent : tr("Settings"); + content.replaceChildren(heading); +} + +async function loadFolders() { + const select = document.querySelector("#mail-folder"); + if (select.dataset.loaded) return loadConversations(); + const response = await fetch("/api/mail/folders", {credentials: "same-origin"}); + if (!response.ok) return; + const result = await response.json(); + select.replaceChildren(...result.folders.map(folder => { + const option = document.createElement("option"); + option.value = folder.path; + option.textContent = folder.name; + return option; + })); + select.dataset.loaded = "true"; + loadConversations(); +} + +async function loadConversations() { + const folder = document.querySelector("#mail-folder").value; + if (!folder) return; + const response = await fetch("/api/mail/conversations?folder=" + encodeURIComponent(folder), {credentials: "same-origin"}); + if (!response.ok) return; + const result = await response.json(); + const rows = result.conversations.map(message => { + const row = document.createElement("li"); + row.classList.toggle("unread", message.unread > 0); + const sender = document.createElement("span"); + sender.className = "sender"; + sender.textContent = message.from.map(item => item.name || item.address).join(", "); + const subject = document.createElement("span"); + subject.className = "subject"; + subject.textContent = message.subject || tr("(no subject)"); + const date = document.createElement("time"); + date.textContent = message.date; + const button = document.createElement("button"); + button.type = "button"; + button.addEventListener("click", () => loadConversation(message.id)); + button.append(sender, subject, date); + row.append(button); + return row; + }); + document.querySelector("#conversation-list").replaceChildren(...rows); +} + +function mailDocument(message, allowRemote) { + const parser = new DOMParser(); + const document = parser.parseFromString(message.html || "", "text/html"); + document.querySelectorAll("img[src]").forEach(image => { + const source = image.getAttribute("src"); + if (source.startsWith("cid:")) { + const data = message.inline_images[source.slice(4)]; + if (data) image.setAttribute("src", data); else image.removeAttribute("src"); + } else if (source.startsWith("bongo-remote:")) { + const index = Number(source.slice(13)); + if (allowRemote && message.remote_images[index]) image.setAttribute("src", message.remote_images[index]); + else { + image.removeAttribute("src"); + image.setAttribute("alt", image.getAttribute("alt") || tr("[external image blocked]")); + } + } + }); + return "" + document.body.innerHTML; +} + +function renderConversation(allowRemote = false) { + const container = document.querySelector("#message-items"); + let hasRemote = false; + const items = openConversation.messages.map(message => { + const article = document.createElement("article"); + article.className = "message"; + const title = document.createElement("h2"); + title.textContent = message.subject || tr("(no subject)"); + const addresses = document.createElement("p"); + addresses.className = "message-addresses"; + addresses.textContent = message.from.map(item => item.name ? `${item.name} <${item.address}>` : item.address).join(", "); + article.append(title, addresses); + if (message.html) { + const frame = document.createElement("iframe"); + frame.className = "message-html"; + frame.setAttribute("sandbox", ""); + frame.setAttribute("referrerpolicy", "no-referrer"); + frame.srcdoc = mailDocument(message, allowRemote); + article.append(frame); + } else { + const text = document.createElement("div"); + text.className = "message-text"; + text.textContent = message.text || ""; + article.append(text); + } + hasRemote ||= message.remote_images.length > 0; + return article; + }); + container.replaceChildren(...items); + document.querySelector("#load-remote-images").hidden = !hasRemote || allowRemote; +} + +async function loadConversation(id) { + const response = await fetch("/api/mail/conversation?id=" + encodeURIComponent(id), {credentials: "same-origin"}); + if (!response.ok) return; + openConversation = await response.json(); + render(); + renderConversation(false); +} + +async function loadSession() { + const response = await fetch("/api/session", {credentials: "same-origin"}); + session = response.ok ? await response.json() : null; + if (session) await loadTasks(); + render(); +} + +async function loadTasks() { + const response = await fetch("/api/tasks", {credentials: "same-origin"}); + if (!response.ok) return; + tasks = (await response.json()).tasks; + document.querySelector("#task-count").textContent = String(tasks.length); + document.querySelector("#task-button").classList.toggle("has-tasks", tasks.length > 0); + document.querySelector("#task-empty").hidden = tasks.length > 0; + document.querySelector("#task-list").replaceChildren(...tasks.map(task => { + const item = document.createElement("li"); + item.className = "task " + task.severity; + const title = document.createElement("strong"); + title.textContent = task.title; + const details = document.createElement("span"); + details.textContent = task.details.list_id || task.key; + item.append(title, details); + if (task.dismissible && task.severity !== "critical") { + for (const [state, label] of [["ignored", "Ignore"], ["disabled", "Disable"]]) { + const button = document.createElement("button"); + button.type = "button"; + button.textContent = tr(label); + button.addEventListener("click", () => setTaskState(task.id, state)); + item.append(button); + } + } + const done = document.createElement("button"); + done.type = "button"; + done.textContent = tr("Done"); + done.addEventListener("click", () => setTaskState(task.id, "completed")); + item.append(done); + return item; + })); +} + +async function setTaskState(id, state) { + const response = await fetch("/api/tasks/state", { + method: "POST", credentials: "same-origin", + headers: {"Content-Type": "application/json", "X-Bongo-CSRF": session.csrf_token}, + body: JSON.stringify({id, state}) + }); + if (response.ok) await loadTasks(); +} + +loginForm.addEventListener("submit", async event => { + event.preventDefault(); + loginError.hidden = true; + const button = loginForm.querySelector("button"); + button.disabled = true; + try { + const response = await fetch("/api/session", { + method: "POST", credentials: "same-origin", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({user: loginForm.user.value, password: loginForm.password.value}) + }); + loginForm.password.value = ""; + if (!response.ok) throw new Error(response.status === 429 ? tr("Too many login attempts. Please try again later.") : tr("Login failed.")); + session = await response.json(); + render(); + } catch (error) { + loginError.textContent = error.message; + loginError.hidden = false; + } finally { button.disabled = false; } +}); + +document.querySelector("#logout").addEventListener("click", async () => { + if (!session) return; + await fetch("/api/session", {method: "DELETE", credentials: "same-origin", headers: {"X-Bongo-CSRF": session.csrf_token}}); + session = null; + render(); +}); +document.querySelector("#task-button").addEventListener("click", event => { + const panel = document.querySelector("#task-panel"); + panel.hidden = !panel.hidden; + event.currentTarget.setAttribute("aria-expanded", String(!panel.hidden)); + if (!panel.hidden) loadTasks(); +}); +document.querySelectorAll(".settings-nav button").forEach(button => + button.addEventListener("click", () => renderSettings(button.dataset.settings))); +document.querySelector("#mail-folder").addEventListener("change", loadConversations); +document.querySelector("#mail-refresh").addEventListener("click", loadConversations); +document.querySelector("#message-back").addEventListener("click", () => { openConversation = null; render(); }); +document.querySelector("#load-remote-images").addEventListener("click", () => renderConversation(true)); +document.querySelector("#compose-new").addEventListener("click", () => { + openConversation = null; + composing = true; + document.querySelector("#compose-status").textContent = ""; + render(); + document.querySelector("#compose-to").focus(); +}); +document.querySelector("#compose-cancel").addEventListener("click", () => { + composing = false; + document.querySelector("#message-compose").reset(); + render(); +}); +document.querySelector("#message-compose").addEventListener("submit", async event => { + event.preventDefault(); + const form = event.currentTarget; + const status = document.querySelector("#compose-status"); + const submit = form.querySelector("button[type=submit]"); + submit.disabled = true; + status.textContent = ""; + try { + const response = await fetch("/api/mail/send", { + method: "POST", credentials: "same-origin", + headers: {"Content-Type": "application/json", "X-Bongo-CSRF": session.csrf_token}, + body: JSON.stringify({to: document.querySelector("#compose-to").value, + cc: document.querySelector("#compose-cc").value, + bcc: document.querySelector("#compose-bcc").value, + subject: document.querySelector("#compose-subject").value, + text: document.querySelector("#compose-body").value}) + }); + if (!response.ok) throw new Error(response.status === 400 ? tr("Invalid message fields.") : tr("The message could not be sent.")); + form.reset(); + composing = false; + render(); + document.querySelector("#section-message").textContent = tr("Message sent."); + } catch (error) { + status.textContent = error.message; + } finally { + submit.disabled = false; + } +}); +window.addEventListener("hashchange", render); +fetch("/api/i18n").then(response => response.json()).then(catalog => { + messages = catalog.messages; + document.documentElement.lang = catalog.language.replace("_", "-"); + document.querySelectorAll("[data-i18n]").forEach(element => { + element.textContent = tr(element.dataset.i18n); + }); +}).finally(() => loadSession().catch(() => render())); diff --git a/src/www/static/bg-page.png b/src/www/static/bg-page.png new file mode 100644 index 0000000..5f81a28 Binary files /dev/null and b/src/www/static/bg-page.png differ diff --git a/src/www/static/bg-selected.png b/src/www/static/bg-selected.png new file mode 100644 index 0000000..cbba7d5 Binary files /dev/null and b/src/www/static/bg-selected.png differ diff --git a/src/www/static/calendar-16.png b/src/www/static/calendar-16.png new file mode 100644 index 0000000..018f518 Binary files /dev/null and b/src/www/static/calendar-16.png differ diff --git a/src/www/static/contact-new.png b/src/www/static/contact-new.png new file mode 100644 index 0000000..2640eaf Binary files /dev/null and b/src/www/static/contact-new.png differ diff --git a/src/www/static/css/admin.css b/src/www/static/css/admin.css new file mode 100644 index 0000000..c9ef0aa --- /dev/null +++ b/src/www/static/css/admin.css @@ -0,0 +1,582 @@ +/* + Bongo web administration interface (aka Hawkeye) stylesheet + (c) 2007 Alexander Hixon - see COPYING for details + Last modified: Monday Sep 3, 2007 + + =======[ Global Styles ]======= +*/ + +body { + background-color: #eeeeec; + font-size: 16px; + font-family: luxi sans, trebuchet ms, bitstream vera sans, sans-serif; + color: #eeeeec; + padding: 0px; + margin: 0px; +} + +#body-override { + color: #2e3436; + background: url('/img/bg.png'); + background-repeat: repeat-x; + background-color: #fff; + margin-top: 25px; + margin-left: 60px; + margin-right: 60px; +} + +/* + =======[ Login Styles ]======= +*/ + +#login { + font-size: 110%; + position:absolute; + height:300px; + width:400px; + margin:-150px 0px 0px -200px; + top: 50%; + left: 50%; + + text-align: center; + padding: 0px; + background-image: url('/img/login-pane.png'); + /*background-repeat: no-repeat; /* Make sure this doesn't actually happen. */ + overflow: none; +} + +#login form { + padding: 16px; + padding-top: 6.5em; +} + +.inputbox { + width: 178px; /* 200px - 22px */ + margin-bottom: 2px; +} + +#login select { + width: 202px; +} + +.submit { + padding-top: 1em; + padding-bottom: 0px; + margin-bottom: 0px; +} + +input, select { + color: #2e3436; + background-color: #fff; + border: 1px solid #888a85; +} + +.lang { + background-image: url('/img/locale.png'); +} + +.username { + background-image: url('/img/user.png'); +} + +.password { + background-image: url('/img/locked.png'); +} + +.password, .username, .lang { + padding-left: 22px; + min-height: 18px; + background-repeat: no-repeat; +} + +/* + =======[ Main Styles ]======= +*/ + +#userinfo { + padding-top: 20px; + font-size: 80%; + float: right; +} + +#logo { + margin-bottom: 60px; +} + +p { + font-size: 90%; + color: #2e3436; /* Tango DARK-GREY */ +} + +a { + text-decoration:none; + color: #2e3436; +} + +#page { + background: url('/img/cbg.png'); + background-repeat: repeat-x; + background-color: #eeeeec; /* Tango GREY */ + /*margin-left: 155px; /* Push across width of sidebar, minus 10px, for bg behind round corner */ + /*padding-left: 20px; /* Prints BG for 10px, now flush with sidebar */ + margin: 0px; + padding: 0px; + margin-left: 175px; + /*float: right;*/ + padding-top: 6px; + min-height: 390px; + + -moz-border-radius-bottomright: 20px; + -khtml-border-radius-bottomright: 20px; + border-radius-bottomright: 20px; +} + +#content { + padding-left: 12px; + padding-right: 12px; +} + +.breadcrumb { + font-size: 75%; +} + +.floaty { + float: right; + padding-left: 16px; + padding-right: 16px; + display: block; +} + +#wrap { + background: url('/img/cbg.png'); + background-repeat: repeat-x; + background-color: #eeeeec; /* Tango GREY */ + /*margin-left: 155px; /* Push across width of sidebar, minus 10px, for bg behind round corner */ + /*padding-left: 20px; /* Prints BG for 10px, now flush with sidebar */ + margin: 0px; + padding: 0px; + /*padding-left: 175px;*/ + /*padding-top: 6px;*/ + + -moz-border-radius-bottomleft: 20px; + -khtml-border-radius-bottomleft: 20px; + border-radius-bottomleft: 20px; + + -moz-border-radius-bottomright: 20px; + -khtml-border-radius-bottomright: 20px; + border-radius-bottomright: 20px; + + padding-bottom: 6px; + margin-bottom: 1em; +} + +#footer { + margin-bottom: 1em; + text-align: right; + padding-right: 2em; + font-size: 85%; + display: block; +} + +/* + =======[ Sidebar Styles ]======= +*/ + +#sidebar-wrap { + float: left; + width: 175px; + + /*height: 500px;*/ + + margin: 0px; + padding: 0px; + + background: url('/img/sbg.png'); + background-repeat: no-repeat; + + color: #eeeeec; /* Tango WHITE */ + + text-align:center; +} + +nlink a, a.nlink { + color: #eeeeec; + border-bottom: 0px none; +} + +#sidebartoggle { + display: inline; + position:relative; + top: -50px; +} + +#sidebar { + height: 400px; + /*height: 100%; /* Always make sidebar as big as content. */ + + /*background-color: #2e3436; /* Tango DARK-GREY */ + + text-align: right; + margin: 0px; + padding: 0px; + /*margin-right: 0px; + padding-right: 0px; + padding-bottom: 6px;*/ +} + +.helpitem { + /* FIXME! */ +} + +.selecteditem { + background-image: url('/img/sel.png'); + width: 175px; + background-repeat: no-repeat; + color: #2e3436; +} + +.disabled { + color:#aaa !important; +} + +ul.nav, +.nav ul{ + margin-right: 0px; + padding-left: 4px; + margin-left: 6px; + padding-right: 0px; + list-style-type: none; +} + +a.nav, +.nav a{ + color: #eeeeec; + text-decoration:none; +} + +a.selecteditem, +.selecteditem a:link, +.selecteditem a:visited { + color: #2e3436; + text-decoration:none; +} + +li.nav, +.nav li{ + display: block; + font-size: 115%; /* Bold maybe? */ + min-height: 35px; + text-align: left; + + padding-left: 15%; + padding-right: 15px; + margin-bottom: 15px; + text-decoration:none; + margin-top: 5px; + width: 95%; +} + +img.icon { + margin-right: 6px; + vertical-align:middle; +} + +ul.nav{ + width: 100%; +} + +/* + =======[ Table Styles ]======= +*/ + +.htable { + border: 3px solid #729fcf; + -moz-border-radius: 10px; +} + +.hrow { + color: #d3d7cf; + font-weight: bold; + padding-left: 4px; +} + +.highlight, .highlight0 { + color: #d3d7cf; + background-color: #204a87; +} + +.highlight1 { + color: #d3d7cf; + background-color: #3465a4; +} + +.drow { + color: #d3d7cf; +} + +/* + =======[ Message Notification Styles ]======= +*/ + +.error, .info { + -moz-border-radius: 5px; + + margin-right: 12px; + min-height:18px; /* Icon size - total padding (top & bottom) */ + padding-top: 8px; + padding-bottom: 8px; + + padding-left: 36px; + background-repeat: no-repeat; +} + +.error { + background-image: url('/img/dialog-error-32.png'); + background-color: #a40000; + color: #eeeeec; + border: 3px solid #cc0000; +} + +.info { + background-image: url('/img/dialog-information-32.png'); + background-color: #3465a4; + color: #eeeeec; + border: 3px solid #204a85; +} + +/* + ========[ Icon View ]======= +*/ + +#iconview { + text-align:center; + vertical-align:middle; +} + +#iconview div.icon { + margin:3px; +} + +#iconview div.icon a { + border:1px solid #d3d7cf; + display:block; + float:left; + height:97px !important; + padding:2px 5px 1px; + text-decoration:none; + vertical-align:middle; + width:108px !important; +} + +#iconview div.icon a:link { + color:#888a85; +} + +#iconview div.icon a:hover { + background-color:#d3d7cf; + border:1px solid #2e3436; + color:#2e3436; + padding:3px 4px 0px 6px; +} + +#iconview div.icon a:active { + color:#888a85; +} + +#iconview div.icon a:visited { + color:#888a85; +} + +#iconview div.icon img { + margin-top:13px; +} + +#iconview div.icon span { + display:block; + padding-top:3px; +} + +/* + =======[ Button Styles ]======= + http://www.hedgerwow.com/360/dhtml/css-round-button/ +*/ + +a.button, span.button, del.button { + display:-moz-inline-box; + display:inline-block; + cursor:pointer; + border:none; + font-size:0; + line-height:0; + + /* + for Safari, read this first + http://creativebits.org/webdev/safari_background_repeat_bug_fix + */ + background-position:0 0; + background-repeat:no-repeat; + height:30px; + text-decoration:none; + color:#2e523b; + font-style:normal; + margin:0 6px 0px 0; + padding:0 10px 0 0; + vertical-align:middle; + + padding-top:-2px; + _position:relative; + _width:10px; + _overflow-y:hidden; +} + +a.button, span.button, del.button, +a.button span, span.button button, span.button input, del.button span { + background-image:url('/img/form_buttons.png'); + _background-image:url('/img/form_buttons.gif'); +} + + +a.button span, span.button button, span.button input, del.button span { + white-space:nowrap; + cursor:pointer; + color:#222; + display:-moz-inline-box; + display:inline-block; + line-height:1; + /*letter-spacing:0 !important; + font-family:"Arial" !important;*/ + font-size:12px !important; + font-style:normal; + background-color:transparent; + background-position:100% 0; + background-repeat:no-repeat; + height:30px; + padding:8px 20px 0 10px; + margin:0 -16px 0 10px; + border:none; + vertical-align:text-top; + zoom:1; + _position:relative; + _padding-left:0px; + _padding-right:12px; + _margin-right:-10px; + _display:block; + _top:0; + _right:-5px; +} + + +span.button button { + line-height:2.5; /*Opera need this*/ +} + +html.safari a.button span, +html.safari del.button span { + line-height:1.3; +} + +html.safari span.button button { + line-height:2.6; +} + +html.safari a.button:focus, +html.safari span.button button:focus { + outline:none; +} + +del.button { + /* cursor:not-allowed; */ + background-position:0 -120px; +} + +del.button span { + cursor:default; + color:#aaa !important; + background-position:100% -120px; +} + +span.button button, span.button input { + padding-top:0px; + line-height:2.5; /*Opera need this*/ +} + +/*Hover Style*/ +a.button:hover, +span.button:hover, +a.button:focus, + +a.dom-button-focus, +span.button-behavior-hover { + background-position:0 -60px; + color:#222; + text-decoration:none; +} + +a.button:hover span, +span.button:hover button, +a.button:focus span, + +span.button-behavior-hover button, +span.button-behavior-hover input { + background-position:100% -60px; +} + +a.button:active, a.button:focus span { + color:#444; +} + +del.button-behavior-hover, del.button:hover { + background-position:0 -180px; + /* cursor:not-allowed; */ +} + +del.button-behavior-hover span, del.button:hover span { + background-position:100% -180px; + /* cursor:not-allowed; */ + +} + +/*Optional hack for IE6 to simulate :hover selector*/ + +span.button button, del.button span, span.button input{ + + + + _behavior:expression( + (function(el){ + + if( typeof( behavior_onMouseEnter) == 'undefined'){ + + + behavior_onMouseEnter = function(el){ + + var dEl = this.parentNode; + var sClass = dEl.className ; + dEl.__defaultClassName = sClass ; + dEl.className = sClass + ' button-behavior-hover'; + this.setCapture(); + }; + + behavior_onMouseLeave = function(el) { + var dEl = this.parentNode; + dEl.className = dEl.__defaultClassName ; + dEl.__defaultClassName = undefined; + this.releaseCapture(); + }; + + + }; + + + el.runtimeStyle.behavior = 'none'; + el.onmouseenter = behavior_onMouseEnter; + el.onmouseleave = behavior_onMouseLeave; + + + })(this)); + + + + +} + diff --git a/src/www/static/css/calendar.css b/src/www/static/css/calendar.css new file mode 100644 index 0000000..fdfd157 --- /dev/null +++ b/src/www/static/css/calendar.css @@ -0,0 +1,1070 @@ +#calendar h1, #calendar h2 { text-align: center; } + #calendar h1 { padding: 0.5em; font-size: 16pt; } + #calendar h2 { padding: 7px; } + +/* Generic calendar table styles */ +.calendar { + width: 100%; + height: 100%; + position: relative; + z-index: 5; +} + + /* Modify columnview for the timestrip */ + .calendar.columnview.top, + .calendar.columnview.bottom { + /* + margin-left: 0; + */ + } + + /* Generic calendar rules for columnview and monthview */ + .calendar th, .calendar td { + vertical-align: top; + } + .calendar th { + font-size: 80%; + color: #555; + background-color: transparent; + border-left: 1px solid #eee; + border-bottom: 1px solid #ddd; + font-weight: normal; + } + + .calendar th.last { + border-right: 1px solid #eee; + } + + .calendar td { + border-left: 1px solid #ccc; + border-bottom: 3px double #ccc; + } + .calendar.monthview td { + /* Work around double-border on the left in monthview */ + border-right-width: 0; + border-left-width: 1px; + } + .calendar td.last { + border-right: 1px solid #ccc; + } + + .calendar.bottom td { + border-left: 1px solid #d5d5dd; + border-bottom: none; + } + .calendar.bottom td.last { + border-right: 1px solid #d5d5dd; + } + + .calendar .buble { + font-size: 86%; + list-style: none; + } + + /* Hide timezones and notes for items by default */ + .calendar .tz, .calendar .note { visibility: hidden; } + + /* Show notes on first event only */ + .columnview .timed-events .buble.start .note { visibility: visible; } + + /* Hide spacers */ + .calendar .spacer { visibility: hidden; } + + .calendar .date a, + .calendar th a { + color: inherit; + } + .calendar .date a:hover, + .calendar th a:hover { + text-decoration: none; + color: #000; + } + + .calendar .date a { padding: 2px 5px; } + + .calendar th a { + display: block; + padding: 3px 4px; + } + .calendar th a:hover { + background: transparent url(../img/cal-selected.png); + } + /* IE-only stipple */ + .eng-msie.engv-6 .calendar th a:hover { + background-image: none; + background-color: #eef; + } + +/*** Monthview-specific ***/ + +.monthview { + border-bottom: 1px solid #d5d5dd; + padding: 0 6px; +} + + .calendar td.offmonth { + background: #fcfcfc url(../img/cal-offmonth.png); + color: #bababa; + } + .calendar.cellview td.offmonth { background-image: none; } + + .calendar tr > td.today { + background-image: url(../img/cal-today.png); + color: #b99508; + } + /* IE6 stipple BG for today */ + .eng-msie.engv-6 .calendar td.today { + background-image: url(../img/cal-today-stipple.png); + color: #b99508; + } + .calendar td.offmonth.today { + color: #675922; + } + .monthview td { + height: 8em; + width: 14.28%; + padding: 0 0 1.5em; + border-bottom: 1px solid #d5d5dd; + color: #777; + } + .monthview tr.last td { border-bottom: none; } + + .monthview .date { } + html>body .monthview .date { text-align: right; font-size: 100%; } + + .monthview .timed-events .buble { + background-color: transparent; + width: auto !important; + height: auto !important; + padding: 0 0.1em 0 0.5em; + list-style: disc; + list-style-position: inside; + background-color: transparent; + } + + .monthview .untimed-events .buble { height: auto !important; } + + /* Hide non-starting day-spanning timed events */ + .monthview .timed-events .buble { display: none; } + .monthview .timed-events .buble.start { display: list-item; } + + /* Webkit browsers need the width to be 100% instead of auto */ + .eng-webkit .monthview .timed-events .buble { width: 100%; } + + /* "Simple" fix for IE7 */ + .eng-msie.engv-7 .monthview .untimed-events { + position: relative; + width: 100%; + } + + /* Fix for IE6 */ + /* _REAL_ FIX COMING SOON! (This is a quick placeholder) */ + .eng-msie.engv-6 .monthview .untimed-events { position: relative; } + + /* Turn off non-titles in month view */ + .monthview .item span { display: none; } + + .monthview .timed-events .item .time { + display: inline; + opacity: 0.6; + font-size: 90%; + } + /*.monthview .timed-events .item .time:before { content: " ("; }*/ + .monthview .timed-events .item .time:after { content: ": "; } + .eng-msie.engv-7 .monthview .timed-events .item .time, + .eng-msie.engv-6 .monthview .timed-events .item .time { margin-right: 1ex; /* IE-only */ } + + .monthview .untimed-events .item, + .monthview .untimed-events .spacer { + height: 1em; + line-height: 1; + overflow: hidden; + padding: 2px; + border: 1px solid red; + margin: 1px 0; + } + + .monthview .untimed-events .item strong { + /* Add space between the lines */ + line-height: 2em; + position: relative; + bottom: 0.5em; + padding-left: 0.25em; + } + + .month-header { + width: 100%; + } +.month-header-title { + width: 100%; +} + +/*** Summary-specific ***/ +.cellview { + border: 1px solid #ccc; + border-collapse: collapse; +} + +.cellview td { + width: 50% !important; +} + +.cellview .date { + text-align: center !important; + padding-top: 5px; + padding-bottom: 5px; + text-decoration: underline; +} + + +/*** Columnview-specific ***/ +.columnview { } + + .calendarwrapper .top { margin: 0; } + + .calendarwrapper .bottom { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + .columnview td.untimed { + background-color: #eee; + } + .columnview td.untimed .spacer, + .columnview td.untimed .buble { + position: relative; + display: block; + font-size: 0.9em; + height: 1.75em !important; + line-height: 1.7; + overflow: hidden; + } + + .columnview .timed-events { + /* Wrapper to have event children be absolutely position relative to column */ + position: relative; + /*height: 72em;*/ + } + + .columnview .timed-events .buble { + list-style: none; + position: absolute; + /* We don't want disappearing events... */ + min-height: 1.5em; + overflow: hidden; + height: 8.34% + } + + +/*** Both columnview & monthview ***/ +.calendar {} + + /* Make titles non-bold */ + .calendar .item strong { font-weight: normal; } + + +/*** Event colors ***/ +#calendar .buble { +} + + .calendar .buble { color: #fff; } + #calendar-list .cal-name { + padding-left: 4px; + /* + overflow: hidden; + white-space: nowrap; + */ + } + + /* Hack to fill the buble with the item (for IE6) */ + /* It's approximate, but works pretty ok... */ + .eng-msie.engv-6 #calendar .columnview .buble .item { width: 90%; height: 95%; } + +/**************/ +/*** Bubles ***/ +/**************/ + +/* transitory event styles */ +.buble.moving { + opacity: 0.5; + z-index: 10; + position: relative; +} + +.buble.deleting { + border: 2px solid red; +} + +.buble.saving { + border: 2px solid green; +} + + +.columview .timed-events .buble.moving { + position: absolute; +} + +/* All bubles, regardless of view */ +.monthview .untimed-events .spacer, +.monthview .untimed-events .buble .item, +.columnview td .buble .item { + overflow: hidden; + display: block; +} + +/* ...except don't display dragholders in month view */ +.monthview .untimed-events .dragholder {display: none; } + + /* columnview only */ + .columnview td .buble .item { + position: absolute; + bottom: 0; + right: 0; + left: 0; + top: 0; + } + + .columnview ul .buble .item { + padding: 3px; + } + + .columnview .timed-events .buble .item { + margin: 0 2px 0 2px; + + /* the top and bottom borders are handled by the selectors + * with .start and .end so that events spanning multiple days + * do not have borders on the portion that spans the day + * boundaries. + * + * The border + padding should equal 3px, so that the layout + * on the inside doesn't change just because you selected an + * event. + * + * Garrett, please check multi-day events if you change these + * to not be a border-left and border-right. + */ + border-left: 1px solid red; + border-right: 1px solid red; + padding-left: 2px; + padding-right: 2px; + } + + .columnview .timed-events .buble.popped-out .item { + border-left-width: 2px; + border-right-width: 2px; + padding-left: 1px; + padding-right: 1px; + } + + /* column view timed start */ + .columnview .timed-events .buble.start .item { + -moz-border-radius-topleft: 10px; + -moz-border-radius-topright: 10px; + border-top: 1px solid red; + padding-top: 2px; + margin-top: 1px; + } + .columnview .timed-events .buble.start > .item { + /* + background: url(../img/grad-cal-title.png) repeat-x 0 -480px; + */ + } + .columnview .timed-events .buble.start.popped-out .item { + border-top-width: 2px; + padding-top: 1px; + background-position: 0 -482px; + } + + /* column view timed end */ + .columnview .timed-events .buble.end .item { + -moz-border-radius-bottomleft: 10px; + -moz-border-radius-bottomright: 10px; + border-bottom: 1px solid red; + padding-bottom: 2px; + margin-bottom: 1px; + } + .columnview .timed-events .buble.end.popped-out .item { + border-bottom-width: 2px; + padding-bottom: 1px; + } + + /* Make the cursor a normal pointer */ + #calendar table ul .buble .item, + #calendar table ul .buble .item * { + cursor: default; + cursor: move; + cursor: grab; + cursor: -moz-grab; + } + + #calendar table ul .buble.moving .item, + calendar table ul .buble.moving .item * { + cursor: grabbing; + cursor: -moz-grabbing; + } + +/* Column view bubles' titles */ +.columnview td .buble .item .title { + font-family: luxi sans, myriad, bitstream vera sans, sans-serif; + display: block; + text-align: center; + margin: -0.2em; + margin-bottom: 0.25em; + /* + background: url(../img/stipple-1bit.png); + */ +} +.columnview .timed-events .buble .item .title { + padding: 0.4em 0.1em; +} + +.columnview td .buble .item > .title { +} +.columnview td .buble .item .time { + text-align: center; + display: block; + font-weight: normal; + letter-spacing: 1px; + font-size: 90%; + opacity: 0.75; + /* Let's hide time by default... */ + display: none; +} +.columnview td .buble .item > .inborder { + border: 2px solid #fff; + -moz-border-radius: 2px 2px 1px 2px; + border-radius: 2px 2px 1px 2px; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + opacity: 0.8; +} +.columnview td .buble .item .note { + display: block; + font-size: 90%; + opacity: 0.8; + padding: 0.5em 6pt; +} + +.columnview .buble.end .item .resize { + background: transparent url(../img/grippy-v.png) no-repeat bottom center; + height: 12px; + display: block; + position: absolute; + bottom: 0; + right: 0; + width: 100%; + margin: 0 -1px -1px; + opacity: 0.5; + cursor: resize !important; +} +.eng-msie.engv-6 .columnview .buble.end .item .resize { background-image: url(../img/grippy-v.gif); } +* +.columnview .buble.end.popped-out .item .resize { + opacity: 1; + margin: 0 -1px -3px; +} +.columnview .untimed-events .buble.end .item .resize { + background: transparent url(../img/grippy-h.png) no-repeat right; + width: 12px; + height: 100%; + margin: 0; +} +.columnview .untimed-events .buble.end .item .resize { background-image: url(../img/grippy-h.gif); } + +.columnview .untimed-events .buble.end.popped-out .item .resize { + margin: 0 -2px 0; +} + +.columnview td .buble .item > .resize { + /* Non-IE6 */ +} + +.columnview .item { + padding-top: 2px; + padding-bottom: 2px; + padding-left: 5px; +} + +.columnview .time { + display: block; + text-align: center; +} + +.calendar.view7days th, +.calendar.view7days td { + width: 14%; +} + +.calendar.view4days th, +.calendar.view4days td { + width: 25%; +} + +.calendar.view1days th, +.calendar.view1days td { + width: 100%; +} + +.calendarwrapper { position: relative; width: 100%; } + +.timedwrapper { position: relative; } + + +/*** Untimed continuation "rules" ***/ +.calendar .untimed-events .item { + background-repeat: no-repeat; + background-image: url(../img/continued-right.png); + background-position: right; + margin: 1px 0; + padding-top: 2px 3px; + border-top: 1px solid red; + border-bottom: 1px solid red; +} + .calendar .untimed-events .item strong { + background-repeat: no-repeat; + background-image: url(../img/continued-left.png); + background-position: left; + padding-left: 16px; + } + + .calendar .untimed-events .buble.popped-out .item { + border-top-width: 2px; + border-bottom-width: 2px; + padding-top: 1px; + padding-bottom: 1px; + } + + .calendar .untimed-events .buble.start .item { + -moz-border-radius-topleft: 9px; + -moz-border-radius-bottomleft: 9px; + border-left: 1px solid red; + padding-left: 2px; + margin-left: 1px; + } + .calendar .untimed-events .buble.start.popped-out .item { + border-left-width: 2px; + padding-left: 1px; + } + + .calendar .untimed-events .buble.end .item { + -moz-border-radius-topright: 9px; + -moz-border-radius-bottomright: 9px; + border-right: 1px solid red; + padding-right: 2px; + margin-right: 1px; + } + .calendar .untimed-events .buble.end.popped-out .item { + border-right-width: 2px; + padding-right: 1px; + } + + .calendar .untimed-events .buble.start .item strong, + .calendar .untimed-events .buble.end .item { + background-image: none; + padding-left: 2px; + } + + .calendar .untimed-events .buble.end .item strong { + } + .calendar .untimed-events .buble.start.end .item, + .calendar .untimed-events .buble.start.end .item strong { + background-image: none; + } + + /* Gray */ + #calendar-list .gray label { border-color: #aaa; } + #calendar .columnview .buble.gray .item, + .monthview .untimed-events .buble.gray .item { + background-color: #aaa; + border-color: #888; + } + #calendar-list .gray, + .monthview .timed-events .buble.gray { color: #888; } + #calendar-list .selected.gray, + .monthview .timed-events .buble.gray.popped-out { background-color: #f6f6f6; } + + /* Red */ + #calendar-list .red label { border-color: #e15e50; } + #calendar .columnview .buble.red .item, + .monthview .untimed-events .buble.red .item { + background-color: #e15e50; + border-color: #cc3525; + } + #calendar-list .red, + .monthview .timed-events .buble.red { color: #cc3525; } + #calendar-list .selected.red, + .monthview .timed-events .buble.red.popped-out { background-color: #fae4e1; } + + /* Orange */ + #calendar-list .orange label { border-color: #ee8d03; } + #calendar .columnview .buble.orange .item, + .monthview .untimed-events .buble.orange .item { + background-color: #ee8d03; + border-color: #ae6600; + } + #calendar-list .orange, + .monthview .timed-events .buble.orange { color: #e08400; } + #calendar-list .selected.orange, + .monthview .timed-events .buble.orange.popped-out { background-color: #fff1dd; } + + /* Yellow */ + #calendar-list .yellow label { border-color: #ebbd08; } + #calendar .columnview .buble.yellow .item, + .monthview .untimed-events .buble.yellow .item { + background-color: #ebbd08; + border-color: #c9a204; + } + #calendar-list .yellow, + .monthview .timed-events .buble.yellow { color: #eabb00; } + #calendar-list .selected.yellow, + .monthview .timed-events .buble.yellow.popped-out { background-color: #fff9e1; } + + /* Brown */ + #calendar-list .brown label { border-color: #d29940; } + #calendar .columnview .buble.brown .item, + .monthview .untimed-events .buble.brown .item { + background-color: #d29940; + border-color: #aa7d2e; + } + #calendar-list .brown, + .monthview .timed-events .buble.brown { color: #bd8a36; } + #calendar-list .selected.brown, + .monthview .timed-events .buble.brown.popped-out { background-color: #f7f2e8; } + + /* Green */ + #calendar-list .green label { border-color: #6cb866; } + #calendar .columnview .buble.green .item, + .monthview .untimed-events .buble.green .item { + background-color: #6cb866; + border-color: #4a9b44; + } + #calendar-list .green, + .monthview .timed-events .buble.green { color: #4a9b44; } + #calendar-list .selected.green, + .monthview .timed-events .buble.green.popped-out { background-color: #e6f3e5; } + + /* Blue */ + #calendar-list .blue label { border-color: #7a9ac9; } + #calendar .columnview .buble.blue .item, + .monthview .untimed-events .buble.blue .item { + background-color: #7a9ac9; + border-color: #3c65a3; + } + #calendar-list .blue, + .monthview .timed-events .buble.blue { color: #5f85bf; } + #calendar-list .selected.blue, + .monthview .timed-events .buble.blue.popped-out { background-color: #e4eaf4; } + + /* Indigo */ + #calendar-list .indigo label { border-color: #8e71c5; } + #calendar .columnview .buble.indigo .item, + .monthview .untimed-events .buble.indigo .item { + background-color: #9e81c5; + border-color: #8a67b9; + } + #calendar-list .indigo, + .monthview .timed-events .buble.indigo { color: #8a67b9; } + #calendar-list .selected.indigo, + .monthview .timed-events .buble.indigo.popped-out { background-color: #ebe5f3; } + + /* Violet */ + #calendar-list .violet label { border-color: #ad68b0; } + #calendar .columnview .buble.violet .item, + .monthview .untimed-events .buble.violet .item { + background-color: #ad68b0; + border-color: #904992; + } + #calendar-list .violet, + .monthview .timed-events .buble.violet { color: #ad68b0; } + #calendar-list .selected.violet, + .monthview .timed-events .buble.violet.popped-out { background-color: #f1e5f2; } + + /* (FIXME) Note: IE6 inherits the last class when multiple classes are listed */ + /* See: http://css-discuss.incutio.com/?page=MultipleClasses */ + /* and http://www.quirksmode.org/css/multipleclasses.html */ + /* For now, IE6 users are getting purple highlights. */ + + /* Magic sauce to make text and the bullet the same color */ + .monthview .timed-events .buble .item { color: inherit; } + +/*** Columnview extras (left-side timestrip & background calstripes) ***/ +.timestrip, +.calstripes { + overflow: hidden; + width: 100%; + height: 100%; + /*height: 72em;*/ + position: relative; + z-index: 0; +} + + .timestrip-wrapper { width: 4.5em; text-align: center; } + + .timestrip-wrapper.left, + .timestrip-wrapper.right { + float: none; + } + + .timestrip div { + font-size: 0; + font-weight: normal; + color: #888; + white-space: nowrap; + height: 4.1666667%; /* 100/TotalHours */ + position: relative; + overflow: visible; + } + + .timestrip .spacer { + height: 2.083333333%; + padding: 0; + margin: 0; + } + + .timestrip .even, + .timestrip .odd { + background: url(../img/bg-timestrip.png) no-repeat 100% 50%; + } + + .timestrip-wrapper.right .timestrip .even, + .timestrip-wrapper.right .timestrip .odd { + background: url(../img/bg-timestrip-right.png) no-repeat 0% 50%; + } + + .timestrip div .timeval { + font-size: x-small; + position: absolute; + left: 12px; + right: 12px; + line-height: 1; + margin-top: -0.5em; + top: 50%; + } + + /* IE6 needs a little help with positioning */ + .eng-msie.engv-6 .left .timestrip div .timeval { left: auto; } + + .left .timestrip div .timeval { text-align: right; } + .right .timestrip div .timeval { text-align: left; } + + /* Hide 12-hour (am/pm) by default, and show changes only */ + /* .timestrip div .timeval span { opacity: 0.4; } */ + + .timestrip .first { font-weight: bold; } + .timestrip .first .timeval span { + opacity: 0.75; + font-weight: normal; + } + + .calstripes { } + + .calstripes div, .calstripes td { height: 4.1666667%; /* 100/TotalHours */ } + + /* Calstripe alternative colors */ + .calstripes .odd { background-color: #f5f5ff; } + .calstripes .even { background-color: #fff; } + +/* Styles specific to calendar popups */ + /* Buble rounded corners skin */ + #new-event-popup .popup-buble .t, + #sidebox-calendar .popup-buble .t, + .popup-buble .t { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .tr, + #sidebox-calendar .popup-buble .tr, + .popup-buble .tr { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .b, + #sidebox-calendar .popup-buble .b, + .popup-buble .b { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .br, + #sidebox-calendar .popup-buble .br, + .popup-buble .br { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .c, + #sidebox-calendar .popup-buble .c, + .popup-buble .c { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .cr, + #sidebox-calendar .popup-buble .cr, + .popup-buble .cr { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .t, + #sidebox-calendar .popup-buble .t, + .popup-buble .t { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .tr, + #sidebox-calendar .popup-buble .tr, + .popup-buble .tr { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .b, + #sidebox-calendar .popup-buble .b, + .popup-buble .b { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .br, + #sidebox-calendar .popup-buble .br, + .popup-buble .br { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .c, + #sidebox-calendar .popup-buble .c, + .popup-buble .c { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .cr, + #sidebox-calendar .popup-buble .cr, + .popup-buble .cr { + background-image: url(../img/popup.png); + } + + + #info-buble { z-index: 15; } + + #info-bubble .usericon { + /* + width: 64px; + height: 64px; + float: left; + */ + vertical-align: middle; + width: 32px; + height: 32px; + margin: 0 3px 0 0; + } + + .popup-buble .date { white-space: nowrap; } + + .popup-buble .tags { color: #888; } + + .popup-buble .description, + .popup-buble .note { + margin: 1em 0; + } + + .popup-buble #view-apt, + .popup-buble #edit-apt { display: block; text-align: right; } + + .popup-buble #apt-edit * { margin: 2px; } + + .popup-buble .location { color: #888; } + + /* Forms */ + .popup-buble textarea, + .popup-buble select, + .popup-buble input { + font-size: inherit; + font-family: inherit; + } + .popup-buble td.form-label { + text-align: right; + padding-right: 7px; + } + + .popup-buble .date-range-entry.untimed .time-entry-fields { + display: none; + } + + .popup-buble .time-entry-fields td, + .popup-buble .day-entry { + white-space: nowrap; + } + + .popup-buble h3, + #info-summary { + font-weight: bold; + font-size: 110% !important; + /* + min-width: 15em; + width: 15em; + */ + } + + .bublecal { + margin: 0 auto; + } + + #info-summary { + margin-bottom: 3px; + } + + #info-summary, + #info-location { + /* + width: 20em; + max-width: 100%; + */ + } + + .popup-buble textarea { + /* + width: 20em; + max-width: 100%; + */ + } + + #info-tabcontent table { + margin: 6px 0; + } + #info-tabcontent th { + font-weight: normal; + text-align: right; + padding-right: 0.5em; + } + + #info-tabcontent #info-start-time-span, + #info-tabcontent #info-end-time-span { + padding-left: 0.25em; + } + #info-tabcontent #info-start-time-span input, + #info-tabcontent #info-end-time-span input { + margin-left: 0.25em; + } + + #info-tabcontent table td { } + #info-tabcontent table input { } + + #info-content .date { + opacity: 0.5; + } + + .popup-buble .actions, .tz-editor .actions, + .popup-buble .info-actions { + overflow: auto; + text-align: right; + margin-top: 6px; + } + + .eng-msie.engv-6 .popup-buble .actions { + width: 30em; + white-space: nowrap; + } + .eng-msie.engv-6 .popup-buble label { + white-space: nowrap; + } + + .popup-buble .actions .secondary, .tz-editor .actions .secondary { float: left; } + + .popup-buble .actions input, .tz-editor .actions .input, + .popup-buble .actions a, .tz-editor .actions a { + margin: 0 0 0 1em; + text-decoration: none; + } + + .popup-buble .actions input.secondary, + .popup-buble .actions a.secondary, + .popup-buble .actions .secondary input, + .popup-buble .actions .secondary a { + margin: 0 1em 0 0; + } + + .rrule-editor { + margin: 0; + padding: 0; + } + .rrule-editor li { + list-style: none; + padding: 5px; + margin: 0; + } + .rrule-editor label { + display: inline-block; + display: -moz-inline-box; + line-height: 1.8; + vertical-align: top; + width: 75px; + } + .rrule-editor .controls {display: none;} + .rrule-editor.day .controls.day {display: block;} + .rrule-editor.week .controls.week {display: block;} + .rrule-editor.month-by-date .controls.month-by-date {display: block;} + .rrule-editor.month-by-day .controls.month-by-day {display: block;} + .rrule-editor.year .controls.year {display: block;} + .rrule-editor.end-by-date .controls.end-by-date {display: block;} + .rrule-editor.end-by-count .controls.end-by-count {display: block;} + +.icon { height: 16px; width: 16px; } +.icon.web { background-image: url(../img/applications-internet.png); } +.icon.bongo { background-image: url(../img/bongo-favicon.png); } +.icon.readwrite, .icon.publish { background-image: url(../img/accessories-text-editor.png); } +.icon.readonly, .icon.protect { background-image: url(../img/emblem-readonly.png); } +.icon.searchable { background-image: url(../img/system-search.png); } +.icon.info { background-image: url(../img/dialog-information.png); } + +.icon.calendars { background-image: url(../img/icon-simple-calendars.png); } +.icon.addressbook { background-image: url(../img/icon-simple-contacts.png); } +.icon.todos { background-image: url(../img/icon-simple-todos.png); } + +/* Icons for IE6 */ +.icon.web { background-image: url(../img/applications-internet.gif); } +.icon.bongo { background-image: url(../img/bongo-favicon.gif); } +.icon.readwrite, .icon.publish { background-image: url(../img/accessories-text-editor.gif); } +.icon.readonly, .icon.protect { background-image: url(../img/emblem-readonly.gif); } +.icon.searchable { background-image: url(../img/system-search.gif); } +.icon.info { background-image: url(../img/dialog-information.gif); } + +.eng-msie.engv-6 .icon.calendars { background-image: url(../img/icon-simple-calendars.gif); } +.eng-msie.engv-6 .icon.addressbook { background-image: url(../img/icon-simple-contacts.gif); } +.eng-msie.engv-6 .icon.todos { background-image: url(../img/icon-simple-todos.gif); } + +/* Next and previous links */ +.month-next, +.month-prev { + white-space: nowrap; + font-size: 90%; +} + + td.month-next, + td.month-prev { + vertical-align: middle; + padding: 3px 2em; + } + + a.month-next, + a.month-prev, + td.month-next a, + td.month-prev a { + display: block; + } + + a.month-next:hover, + a.month-prev:hover, + td.month-next a:hover, + td.month-prev a:hover { text-decoration: none; } + + .month-next img, + .month-prev img { + width: 16px; + height: 16px; + background-position: 50% 50%; + background-repeat: no-repeat; + vertical-align: middle; + } + + .month-prev img { background-image: url(../img/go-previous.png); } + .month-next img { background-image: url(../img/go-next.png); } diff --git a/src/www/static/css/composer.css b/src/www/static/css/composer.css new file mode 100644 index 0000000..93d5622 --- /dev/null +++ b/src/www/static/css/composer.css @@ -0,0 +1,116 @@ +.msg-compose { + width: 100%; + font-size: inherit; + padding: 1em; +} + +.msg-compose th { + text-align: right; + width: 1px; + font-weight: normal; + vertical-align: top; + padding: 0.25em 0.25em 0 0; +} + +.msg-compose .to, .msg-compose .cc, +.msg-compose .bcc, .msg-compose .subject, +.msg-compose .body, .msg-compose .from { + width: 99%; + font-family: inherit; + font-size: inherit; +} +.msg-compose .from { + width: auto; + margin-right: 1em; +} +.msg-compose td div.text { + border: 2px inset #ccc; + background-color: #fff; + padding: 5px 1px; +} +.msg-compose td div.text blink { + position: relative; + right: 3px; +} +option.underline { + border-bottom: 1px solid #999; + margin-bottom: 2px; +} +.msg-compose textarea { + min-height: 20em; + bottom: 0; +} +.msg-compose .toolbar { + border: 1px solid #ddd; + background-color: #eee; + padding: 3px; + margin: 0.5em 0 0; +} +.msg-compose .toolbar .split { + padding-right: 1px; + display: inline; +} +.icon, +.icon-plus, +.icon-minus { + width: 24px; height: 24px; + background: transparent no-repeat center; + vertical-align: middle; +} +.icon-plus { + background-image: url(../img/plus.png); +} +.icon-minus { + background-image: url(../img/minus.png); +} + +.msg-compose th.attach .icon, +.msg-compose .toolbar .icon { + border: 1px solid transparent; + /*padding: 2px; + display: block; + float: left;*/ +} +.msg-compose .toolbar .icon.selected { + opacity: 0.9; + background-color: #d4e0ec; + border-color: #c2d2e4; +} +.msg-compose .toolbar .icon:hover { + opacity: 0.9; + background-color: #d4e0ec; + border-color: #c2d2e4; +} +.msg-compose tr.attach a img { + border: 0; + text-align: left; + vertical-align: middle; + margin-right: 1px; +} +.msg-compose tr.attach input { + width: auto; + display: inline; +} + +.space-l { + margin-left: 5px; +} +.space-r { + margin-right: 5px; +} + +.attachment { + margin: 0.25em 0; + min-height: 1.5em; +} + +.msg-compose tr.action { + text-align: right; +} +.msg-compose tr.action input { + width: auto; +} + +.msg-compose tr.action input { + margin-left: 3pt; +} diff --git a/src/www/static/css/contacts.css b/src/www/static/css/contacts.css new file mode 100644 index 0000000..7d17e6b --- /dev/null +++ b/src/www/static/css/contacts.css @@ -0,0 +1,163 @@ +#contact-list { + height: 25em; + max-height: 25em; + overflow: -moz-scrollbars-vertical; + overflow-y: auto; + overflow-x: hidden; +} + +#contact-list a { +} + +#contact-view { + width: 100%; + height: 100%; + vertical-align: top; +} + +#contact-view table { + margin: 18px; +} + +#contact-view td { + padding: 0 0 3pt 0; +} +#contact-view td input { + margin-right: 3pt; +} + +td.contact-pic { + text-align: right; + vertical-align: middle; +} + +#contact-pic { + margin-right: 14px; +} + +#contact-title, +#contact-company, +#contact-name { + color: #000; + margin: 0; +/* height: 100%;*/ +} + +.contact-field th { + text-align: right; + color: #999; + text-transform: lowercase; + min-width: 10em; + display: block; +} + +.contact-field .type-label { + -moz-border-radius: 25% 0 0 25%; + border: 1px solid transparent; + padding: 0 12px 0 10px; +} + +#contact-view.editing .semicolon { + visibility: hidden; +} + +#contact-view.editing .type-label { + background: transparent url(../img/arrow.png) right no-repeat; +} + +#contact-view.editing .type-label:hover { + background: transparent url(../img/arrow.png) right no-repeat; + background-color: #e4ebf3; + /* color: #f1f5f9; */ + border-color: #d8e1ed; + text-decoration: none; + cursor: default; +} + +#contact-view.editing .contact-field.empty input, +#contact-view.editing .contact-field.empty textarea { + color: #bbb; +} + +#contact-view.editing .contact-field.empty input:focus, +#contact-view.editing .contact-field.empty textarea:focus { + color: #000; +} + +.contact-field .value { + padding: 2px; + /*display: block;*/ + /*float: left;*/ +} + +.contact-field .sub-label { + margin-left: 1em; +} + +/* hidey bits */ +.contact-field input, +.contact-field textarea, +.contact-field select, +#contact-view.editing .contact-field .sub-label, +#contact-view.editing .contact-field .value { + display: none; +} + +/* showey bits */ +#contact-view.editing .contact-field select, +#contact-view.editing .contact-field input, +#contact-view.editing .contact-field textarea { + display: inline; +} + +.contact-field input, +.contact-field textarea { + border: 1px solid transparent; +} +.contact-field textarea { + width: 100%; + overflow: hidden; +} + +.contact-field select, +.contact-field input:focus, +.contact-field textarea:focus { + border: 1px solid #999; + color: #000; + background-color: #fff; +} + +.del-field, +.add-field { + width: 16px; + height: 16px; +} + +.contact-field.empty { + display: none; +} + +#contact-view.editing .contact-field.empty { + display: table-row; +} + +#contact-view.editing .contact-field .del-field { + background-image: url(../img/minus.png); + visibility: visible; +} + +#contact-view.editing .contact-field .add-field { + background-image: url(../img/plus.png); + visibility: hidden; +} + +#contact-view.editing .contact-field.empty.only .del-field { + visibility: hidden; +} + +#contact-view.editing .contact-field.filled.last .add-field { + visibility: visible; +} + +.contact-field.last td { +} diff --git a/src/www/static/css/conversation.css b/src/www/static/css/conversation.css new file mode 100644 index 0000000..9a483ae --- /dev/null +++ b/src/www/static/css/conversation.css @@ -0,0 +1,394 @@ +/* Conversation navigation */ +.conv-nav { + white-space: nowrap; + width: 100%; +} + + .conv-nav td { vertical-align: middle; } + + .conv-nav a { + padding: 0 1em; + font-size: 85%; + } + + .conv-nav .prev, + .conv-nav .next { text-align: right; } + + .conv-nav input { margin-left: 0.25em; } + + .conv-nav .prev a, + .conv-nav .next a { + display: block; + max-width: 100%; + overflow: hidden; + } + .conv-nav .next a { text-align: right; } + .conv-nav .next { position: relative; } + + .eng-webkit .conv-nav input, + .eng-webkit .conv-nav select { + font-size: 10pt !important; + } + +/* Conversation */ +#conv-subject { + font-family: trebuchet ms, bitstream vera sans, sans-serif; + padding: 0; + margin: 0.5em 1em 0.5em; +} +#conv-msg-list { +} + + .msg { + /* + margin: 1em; + border-top: 1px solid #cadae9; + background-color: #d7f6b8; + */ + font-size: 90%; + position: relative; + margin: 0 1em 12px 64px; + } + + .msg .face { + display: block; + background: transparent url(../img/contact-unknown.png) no-repeat; + width: 48px; + height: 48px; + background-position: center; + background-repeat: no-repeat; + position: absolute; + top: 1.5em; + left: -60px; + margin-top: -2px; + } + + .eng-msie.engv-6 .msg .face { left: -58px; } + + .msg .pointer-border { + display: block; + position: absolute; + top: 1.5em; + left: -10px; + margin-top: 9px; + border: 12px solid #999; + border-left-color: transparent; + border-top-color: transparent; + border-bottom-color: transparent; + border-left: 0; + width: 0; + height: 0; + } + .msg .pointer { + display: block; + border: 9px solid white; + border-left-color: transparent; + border-top-color: transparent; + border-bottom-color: transparent; + width: 0; + height: 0; + position: absolute; + top: -9px; + left: -6px; + } + + .eng-msie.engv-6 .msg .pointer-border { display: none; } + + .msg .info { float: right; } + + /* People 'bubles' in the message header */ + .msg .people { + padding: 0px 2px 0px 2px; + white-space: nowrap; + text-decoration: none; + } + + .msg .msg-subject { overflow: hidden; } + + .msg .people:hover { + /*background: transparent url(../img/arrow.png) right no-repeat;*/ + background-color: #e4ebf3; + padding: 0 1px 0 1px; + -moz-border-radius: 25%; + border: 1px solid #d8e1ed; + text-decoration: none; + } + + .msg .people:hover .comma { + visibility: hidden; + } + + .msg .sender { + font-weight: bold; + } + + .msg .date { + display: block; + clear: both; + margin: 0.5em 0 0 12px; + } + + .msg-content { + border: 2px solid #999; + background-color: #999; + } + .msg .msg-header, + .msg .msg-content .msg-body { + padding: 4px 12px; + border: 1px solid #fff; + } + .msg .msg-header { + } + + .msg .msg-header .people-group { padding-left: 12px; } + + .msg .msg-header .recipients { + } + + .msg.closed .msg-header .recipients { display: none; } + + .msg.closed .msg-header .date { + position: absolute; + top: 0; + right: 0.75em; + } + + .msg .msg-content > .msg-header { + /* Hide IE/Win 6 and below */ + background: transparent url(../img/grad.png) repeat-x top; + } + .msg .msg-content .msg-header a { + text-decoration: none; + } + .msg .msg-content .msg-body { + background-color: #fff; + min-height: 18px; + padding-top: 0.5em; + padding-bottom: 0.5em; + } + + /* Row of actions (reply, forward, etc.) for individual messages */ + .msg-actions { + /* + font-size: small; + padding: 0.25em 8px; + */ + } + .msg-actions.bottom { + margin-bottom: 1.5em; + text-align: right; + } + .msg-actions.top { + /*background-color: #fff;*/ + position: relative; + /* + bottom: 1em; + line-height: 1em; + margin-top: -1em; + */ + float: right; + margin-left: 1em; + /* Have at least enough margin to trigger an extra line of reflowing contacts... */ + margin-bottom: 1px; + margin-right: -0.75em; + } + + .eng-msie.engv-6 .msg-actions.top { margin-right: -0.25em; } + + .msg-actions a { + /* + text-decoration: none; + */ + display: block; + float: left; + padding: 0 0.25em; + min-height: 16px; + white-space: nowrap; + } + .msg-actions a.reply { + padding-left: 28px; + background: url(../img/mail-reply-sender-16.png) no-repeat 8px 0; + } + .msg-actions a.replyall { + padding-left: 28px; + background: url(../img/mail-reply-all-16.png) no-repeat 8px 0; + } + .msg-actions a.forward { + padding-left: 28px; + background: url(../img/mail-forward-16.png) no-repeat 8px 0; + } + + .msg-actions a:hover { + text-decoration: underline !important; + } + + /* closed message */ + .msg.closed { + } + .msg.closed .msg-content { + } + .msg.closed .msg-header { + /* + margin: 0; + height: 1.25em; + overflow: hidden; + */ + } + .msg.closed .msg-body { + display: none; + } + .msg.closed .msg-snippet { + /* + display: inline; + */ + display: block; + /* + display: none; + position: absolute; + top: 0; + left: 200px; + padding: 5px; + height: 1em; + overflow: hidden; + */ + opacity: 0.75; + padding-left: 12px; + margin-top: 0.5em; + } + .msg.closed .pointer-border { + display: none; + } + .msg.closed .face { + top: 0; + } + .msg.closed .msg-actions { + display: none; + } + + /* Attachments */ + .msg .msg-attachments { + border: 1px solid #fff; + border-top: 0; + padding: 0.5em 0.25em 0.5em 24px; + } + .msg.closed .msg-attachments { + padding-top: 0.25em; + padding-bottom: 0.25em; + } + .msg .msg-content > .msg-attachments { + /* + background: transparent url(../img/grad.png) repeat-x top; + */ + } + + .msg .msg-attachments b { + font-weight: normal; + } + + .msg .msg-attachments ul, + .msg .msg-attachments li { + display: inline; + list-style: none; + } + .msg .msg-attachments li { padding-left: 0.25em; } + + /* Composer in conversation view */ + .msg .msg-compose { + border: 2px solid #ccc; + border-top-width: 0; + background-color: #f6f6f6; + } + + /* Msg color */ + /* Unread (new) messages */ + .msg .pointer-border { border-right-color: #8fd78b; } + .msg .msg-content { border-color: #8fd78b; background-color: #d2f2cf; } + .msg .msg-header { color: #43923d; } + .msg .msg-attachments { background-color: #e6f3e5; color: #4a9210; } + .msg .msg-compose { background-color: #e6f3e5; border-color: #8fd78b; } + .msg .disclosure, .msg.closed .disclosure { border-color: #8fd78b; } + + /* Read messages */ + .msg.read .pointer-border { border-right-color: #ddd; } + .msg.read .msg-content { border-color: #ddd; background-color: #eee; } + .msg.read .msg-header { color: #777; } + .msg.read .msg-attachments { background-color: #f6f6f6; color: #777; } + .msg.read .msg-compose { background-color: #f6f6f6; border-color: #ddd; } + .msg.read .disclosure, .msg.read.closed .disclosure { border-color: #aaa; } + + /* Sent messages */ + .msg.sent .pointer-border { border-right-color: #ffdb4e; } + .msg.sent .msg-content { border-color: #ffdb4e; background-color: #ffeda6; } + .msg.sent .msg-header { color: #a78600; } + .msg.sent .msg-attachments { background-color: #fff9e1; color: #a78600; } + .msg.sent .msg-compose { background-color: #fff9e1; border-color: #ffdb4e; } + .msg.sent .disclosure, .msg.sent.closed .disclosure { border-color: #ffdb4e; } + + .msg .msg-snippet { + display: none; + } + + .msg .disclosure, + .msg.read .disclosure, + .msg.sent .disclosure { + margin: 5px 0 0 -12px; + float: left; + border-style: solid; + border-width: 5px; + border-bottom-width: 0; + height: 0; + border-right-color: transparent; + border-left-color: transparent; + position: relative; + right: 4px; + } + .msg.closed .disclosure, + .msg.read.closed .disclosure, + .msg.sent.closed .disclosure { + margin: 2px 2px 0 -9px; + border-width: 5px; + border-right-width: 0; + border-top-color: transparent; + border-bottom-color: transparent; + } + + +/* Message body rules, especially for HTML email */ +.msg-body { + /* + font-family: bitstream vera sans mono, monaco, monospace; + */ + overflow: auto; +} + .msg-body ul, + .msg-body ol, + .msg-body li { + margin: 0 0 0 1em; + } + .msg-body ul li { list-style: disc; } + .msg-body ol li { list-style: decimal; } + + /* + .msg-body html, + .msg-body body, + .msg-body table { font-family: sans-serif; } + */ + + /* iTunes message fixup */ + .msg-body table[width="583"] { font-size: 9pt; } + + .msg-body h1 , + .msg-body h2 , + .msg-body h3 , + .msg-body h4 , + .msg-body h5 , + .msg-body h6 { + text-align: left; + color: inherit; + margin: 0.5em 0; + padding: 0; + } + + .msg-body table { } + + .msg-body pre { font-family: bistream vera sans mono,monaco,luxi mono,monospace; } diff --git a/src/www/static/css/layout.css b/src/www/static/css/layout.css new file mode 100644 index 0000000..92f2e95 --- /dev/null +++ b/src/www/static/css/layout.css @@ -0,0 +1,1225 @@ +/* Reset a few things to start with a pretty clean slate */ +html, body, +h1, h2, h3, h4, h5, h6, +div, +ul, ol, li, +dl, dt, dd, +form +{ + margin: 0; padding: 0; +} + +p { + margin: 0.5em 0; +} +input, select, option, table { + font-family: inherit; + font-size: inherit; + /* we probably shouldn't inherit the color without inheriting + background */ + /* color: inherit; */ +} + +h2 { + padding: 1em 1em 0; +} + +/* +table { + border-collapse: collapse; +} +*/ +li { + list-style: none; +} + +/* Disable the click focus ring -- still leaves the tabbed focus ring intact */ +a:active { + outline: none; +} + +td { vertical-align: top; } + +/* Begin the real stuff */ +html, body { overflow: hidden; } + +body { + background-color: #e4eaf4; + color: #000; +} + body.os-x11 { + font-family: lucida grande, myriad, myriad pro, verdana, luxi sans, bitstream vera sans, sans-serif; + font-size: 0.9em; + } + body.os-mac { + font-family: lucida grande, myriad, verdana, luxi sans, bitstream vera sans, sans-serif; + font-size: 0.85em; + } + body.os-win { + font-family: tahoma, myriad, sans-serif; + font-size: 0.8em; + } + body.os-win.eng-msie.engv-6 { + /* IE6 doesn't have ClearType font smoothing on by default; other browsers on Windows do, including IE7 */ + font-family: arial, tahoma, myriad, sans-serif; + } + +#page-header, +#section-user, +#notification, +#searchtext, +#section-sidebar, +#sidebox-sourcelist, +#sidebox-actions, +#sidebox-summary, +#section-content, +#toolbar, +#searchbar, +#section-list, +#section-itemview, +#sidebox-upcoming { + /* + border: 1px solid red; + */ +} + +a, +a * { + /* One cursor for everyone but Win/IE, the other for only Win/IE */ + cursor: pointer; + cursor: hand; +} +a { + text-decoration: none; +} + +a img { + border: 0; +} + +#content { } + +#page-header td.header-cell { + border-top: 4px solid #ffac35; +} + +a { + color: #517faf; +} +:visited { + color: #a98ba6; + color: #517faf; +} +:active { +} +a:hover { + text-decoration: underline; + color: #f57900; +} + +/* We should not have in Bongo, but just to be sure... */ +a[name] { + color: inherit; + text-decoration: inherit; + cursor: inherit; +} + +.right { + float: right; +} + +#page-header { + /* Position is relative so that children will treat the container as the region for absolute */ + position: relative; +} +#page-header td { + vertical-align: middle; +} + +#logo { + vertical-align: middle; + padding: 10px 15px 10px; +} + +#user-name { + font-weight: bold; + font-size: 1.3em; + letter-spacing: -1px; + color: #fff; +} + +#section-user { + color: #999; + font-size: x-small; +} + #section-user ul, + #section-user li { + display: inline; + list-style: none; + } + #section-user li a { + margin: 0 0 0 1ex; + color: #e4eaf4; + border-bottom: 1px dotted #7a9ac9; + } + #section-user li a:hover { + text-decoration: none; + border-bottom: 1px dotted #e4eaf4; + } + +#top-gradient { + width: 288px; + background: transparent repeat-y center; + vertical-align: middle; + background-image: url(../img/top-gradient.png); +} +#notification { + left: auto; + top: 1.25em; + left: 80ex; + font-size: 85%; + background-color: #ad7fa8; + color: #fff; + padding: 0.5em 1em; + position: absolute; + display: none; + /* This should really be on top */ + z-index: 101; +} +#notification.error { background-color: #f00; } + +#search { + padding-bottom: 0.25em; + margin: 1ex 0; + white-space: nowrap; + font-size: 86%; + padding: 0 1ex; +} + #search-entry { + width: 28ex; + margin-top: 3px; + } + #search label { color: #555; } + + /* Hide the search buttons for Safari */ + body.eng-webkit #search input[type="button"] { display: none; } + +#page-frame { + background: #1d3980 url(../img/login-bg-bottom.png) repeat-x 0 0; + background: #e7e4ef url(../img/bg-page.png) repeat-x 0 0; + width: 100%; + height: 100%; +} +body.eng-msie #page-frame { + position: absolute; + top: 0; + left: 0; +} + +#page-frame td { + /* + border: 1px solid red; + */ +} + +.right-spacer { + /* + width: 1em; + */ +} + +#section-sidebar, +#section-content { + /* Each section */ + vertical-align: top; + height: 100%; + position: relative; +} + + #section-sidebar { + width: 14.5em; + } + #sidebar-nav { + min-width: 120px; + margin: 0 0 0 12px; + padding: 0px 0 15px; + z-index: 5; + position: relative; + background-color: #fff; + border: 1px solid #5f85bf; + border-width: 1px 0 1px 1px; + } + + #sidebar-tabs-small, + .low-res #sidebar-tabs-large { + display: none; + } + + #sidebar-tabs-large, + .low-res #sidebar-tabs-small { + display: block; + } + + #sidebar-tabs-small { + margin-top: 1ex; + margin-left: 1ex; + } + + /* Sidebar rounded corners skin */ + /* + #sidebar-nav .t { background-image: url(../img/side-tl.png); } + #sidebar-nav .tr { background-image: url(../img/side-tr.png); } + #sidebar-nav .b { background-image: url(../img/side-bl.png); } + #sidebar-nav .br { background-image: url(../img/side-br.png); } + #sidebar-nav .c { background-image: url(../img/side-cl.png); } + #sidebar-nav .cr { background-image: url(../img/side-cr.png); } + */ + + #sidebar-nav .action { + padding: 0 0 0 2.5em; + font-size: 85%; + } + + #sidebar-nav .tab { + padding: 1px 0; + letter-spacing: 0; + font-size: 100%; + text-align: left; + border-width: 0; + margin: 0.75em 0 0.25em; + -moz-border-radius: 0; + } + + #sidebar-nav .tab a { + display: block; + font-size: 95%; + padding: 6px 12px 6px 30px; + background-repeat: no-repeat; + background-position: 10px 50%; + min-height: 14px; + font-weight: bold; + text-decoration: underline; + } + + /* IE needs hasLayout for link block hovers */ + .eng-msie #sidebar-nav .tab a { width: 100%; } + + #sidebar-nav .tab a:hover { + background-color: #f2f5fa; + } + + #sidebar-nav .tab.selected { + padding: 0; + border: 1px solid #95a9df; + border-bottom-color: #5f85bf; + border-width: 1px 0; + background: #bbceea url(../img/bg-selected.png) repeat-x 50% 50%; + } + #sidebar-nav .tab.selected a { + padding-top: 5px; + border-top: 1px solid #cfe5fb; + color: #1d3f70; + text-decoration: none; + } + #sidebar-nav .tab.selected a strong { + background-image: none; + } + + #sidebar-nav .tab.selected a:hover { + background-color: transparent; + text-decoration: none; + } + + #sidebox-calendar, + #sidebox-sidebook { margin: 12px; margin-bottom: 0; } + #sidebox-sidebook { padding-bottom: 12px; } + + #sidebox-calendar { } + + #sidebox-calendar .month-widget { + background-color: #f6f6f6; + border: 1px solid #888; + width: 100%; + } + + #sidebox-calendar .month-widget th { + } + #sidebox-calendar .month-widget td { + font-size: 86%; + width: 14%; + text-align: center; + } + + #sidebox-calendar .month-widget a { + display: block; + padding: 1px; + } + #sidebox-calendar .month-widget a:link, + #sidebox-calendar .month-widget a:visited { + /* + color: #777; + */ + } + #sidebox-calendar .month-widget a:active { + outline: none; + } + + #sidebox-calendar .month-widget a.today { + background-image: url(../img/cal-today.png); + background-color: #fff; + border: 1px solid #ffba57; + color: #ee8d03; + padding: 0; + } + + #sidebox-calendar .month-widget a.visible { + background-color: #d1e9cf; + color: #4e9a48; + } + + #sidebox-calendar .month-widget a:hover { + background-color: #c7d4e7; + color: #41679f; + border: 1px solid #b7c8e1; + padding: 0; + text-decoration: none; + } + + #sidebox-calendar .prev, + #sidebox-calendar .header, + #sidebox-calendar .next { + background: #e6e6e6 url(../img/bg-side-title.png) repeat-x 50% 50%; + border-bottom: 1px solid #ccc; + } + + #sidebox-calendar .header { + color: #555; + font-size: 0.85em; + font-weight: normal; + white-space: nowrap; + } + + #sidebox-calendar .prev { border-left-width: 1px; } + + #sidebox-calendar td.prev, #sidebox-calendar td.next { + padding: 0; + vertical-align: top; + } + #sidebox-calendar td.next { + text-align: right; + } + #sidebox-calendar td.next a { + float: right; + } + #sidebox-calendar td.prev a, #sidebox-calendar td.next a { + background-position: center; + background-repeat: no-repeat; + color: transparent; + font-size: 0; + margin: 1px; + height: 20px; + width: 100%; + overflow: hidden; + text-indent: -2000px; + display: block; + } + #sidebox-calendar td.prev a { + background-image: url(../img/go-previous.png) + } + #sidebox-calendar td.next a { + background-image: url(../img/go-next.png); + } + + #sidebox-calendar .todaylink p { + margin: 0; + padding: 0; + } + #sidebox-calendar .todaylink a { + margin: 0 2px 2px; + } + + #sidebox-calendar .daylabels td { + padding: 4px 0 2px; + background-color: #fff; + border-bottom: 1px solid #ddd; + color: #777; + } + + /* Add a 1px space under the day labels */ + #sidebox-calendar .daylabels + tr td { + padding-top: 1px; + } + + #sidebox-calendar a { } + + #adbk { + margin-top: 3em; + background-color: #fefefe; + padding: 2px; + border: 1px solid #ddd; + border-width: 1px 2px 2px 1px; + overflow: hidden; + font-size: 90%; + } + + #adbk-card { + overflow: auto; + } + + #adbk-header td { + vertical-align: top; + } + + #adbk-search{ + width: 95%; + } + + #adbk-img { + } + + #adbk-card spacer { + min-height: 1em; + height: 1em; + } + + #adbk-card td { + white-space: nowrap; + } + + #adbk-card th { + text-align: right; + overflow: hidden; + white-space: nowrap; + text-transform: lowercase; + } + + #adbk-card th:after { + content: ":" + } + + /* For some strange reason, IE6 wants to compute width differently + * ...unless a negative right margin is set */ + .eng-msie.engv-6 .list-selector .search-field, + .eng-msie.engv-6 .list-selector .unselected-items { margin-right: -5%; } + + #section-content { + border: 1px solid #5f85bf; + border-width: 1px 0 0 1px; + } + + +#sourcebar, +#toolbar { + background: #fcfcfc url(../img/bg-toolbar.png) repeat-x 0 100%; + border-bottom: 1px solid #bbb; + color: #999; + padding: 6px 12px; + font-size: 90%; + clear: both; +} +#sourcebar { + padding: 3px 12px; +} + +#sourcebar + #toolbar { +} + +#toolbar .conv-nav { + border-collapse: collapse; + padding: 0; + margin: 0; +} +#toolbar .conv-nav td { + padding: 0; +} + +#toolbar input { + /* + margin: 0.5em 0.25em; + */ +} + +#toolbar .spacer { + margin: 0 0.25em; +} + +#toolbar .bar { + color: #ccc; + display: none; +} + +#toolbar td { font-size: 88%; } + +/* Add a little spacing around some buttons in the mail toolbar */ +#toolbar #mail-toolbar-archive, +#toolbar #mail-toolbar-star { + margin-left: 2.5ex; + margin-right: 2.5ex; +} + +#sourcebar { + overflow: auto; +} + +#sourcebar li { + display: inline; + margin: 0 0.5ex; +} + +#sourcebar li a, +#toolbar a { + padding: 5px 6px; +} + +#sourcebar li.selected a, +#toolbar a.selected { + background-color: #999; + border: 1px solid #444; + padding: 0 5px; + color: #fff; +} + +#toolbar .next a, +#toolbar .prev a { + padding: 0; +} + +#sourcebar a { + float: left; + display: block; + padding: 6px 8px; +} +#sourcebar li.selected a { + padding: 4px 6px; +} + +.eng-msie.engv-6 #sourcebar { padding: 0.7em; } +.eng-msie.engv-6 #sourcebar a { display: inline; float: none; } + +#sourcebar li a:hover, +#toolbar a:hover { + cursor: pointer; + cursor: hand; + text-decoration: none; +} + +#quickevent { + width: 15em; +} + +.sidebox { + width: 12.5em; +} + + +#section-upcoming h3, +#section-summary h3 { + margin-top: 1em; +} + +#section-summary li.selected { + background-color: #517faf; + +} +#section-summary li.selected a { + color: #fff; +} + +#section-content { +} +#section-content { + background-color: #fff; + margin-bottom: 0; +} + +#searchbar { + background-color: #feefda; + color: #fa9504; + font-weight: bold; + font-size: larger; + position: relative; + border-bottom: 1px solid #fa9504; + padding: 0.25em 0.5em; +} + +.conversation-list .time { + /*text-align: right;*/ + text-align: right; +} +.conversation-list .date { + /*width: 10em;*/ +} +.conversation-list .subject { + overflow: hidden; +} + +#item-list-nav { + text-align: right; + display: none; +} + +.pager { + text-align: center; + margin: 2em; +} + +#searchbar .button-close { + display: block; + position: absolute; + top: 0.5em; + right: 1em; +} +#section-list { + background-color: #fff; +} +#section-itemview { + /* + border: 1px solid #7ed12d; + */ + border-left: 0; + border-right: 0; + background: #fff; + overflow: hidden; + height: 99%; + padding: 0; + margin: 0; +} + +#section-upcoming { + display: none; +} + +#searchbar { +} + +.tab-space { + width: 100%; + border-bottom: 1px solid #fffdfa; +} + +#ifolder-tab { + display: none +} + +#summary-tab { margin-top: 0.5em !important; } + +/* Tab icons */ +#summary-tab-href { background-image: url(../img/home-16.png); } +#mail-tab-href { background-image: url(../img/message-16.png); } +#calendar-tab-href { background-image: url(../img/calendar-16.png); } +#addressbook-tab-href { background-image: url(../img/contact-16.png); } + +/* Again, for IE6 */ +.eng-msie.engv-6 #summary-tab-href { background-image: url(../img/home-16.gif); } +.eng-msie.engv-6 #mail-tab-href { background-image: url(../img/message-16.gif); } +.eng-msie.engv-6 #calendar-tab-href { background-image: url(../img/calendar-16.gif); } +.eng-msie.engv-6 #addressbook-tab-href { background-image: url(../img/contact-16.gif); } + +#search-tab { + width: 100%; + margin-right: -1px; + text-align: left; +} + +#search-tab label { color: #555; } + +#search-tab a { + padding: 0; + display: inline; +} + +.sidebox { + font-size: 90%; + border: 1px solid; + margin: 0 0.5em 1.5em 0.5em; + background-color: #fff; + color: #000; +} +.sidebox a { +} + +.sidebox .title { + color: #fff; + font-size: x-small; + text-transform: uppercase; + letter-spacing: 3px; + text-align: center; + padding: 1px 2px; + font-weight: normal; +} + +.sidebox .body { + padding: 0.5em 0.5em; +} + +.sidebox li { + white-space: nowrap; + overflow: hidden; + display: block; +} + +.sidebox li.selected { + background-color: #7ed12d; + background: #7bc828 url(../img/navselhilight.png) repeat-x top; +} + +.sidebox li.selected a { + color: #fff; +} + +.sidebox li input { + margin: 0 0.5em; +} + +.sidebox li a { +} + +#sidebox-bookmarks .title { + border-color: #fa9504; + background-color: #fa9504; +} + +#sidebox-bookmarks .body { + border-color: #fa9504; +} + +#bookmark-commands { + margin-top: 6px; +} + +#remove-bookmarks { + display: block; + visibility: hidden; +} + +#add-bookmark { + display: block; + float: right; +} + +#sidebox-sourcelist .add-source { + text-align: right; + padding-right: 0.5em; +} + +#sidebox-actions .title { + border-color: #ae7cad; + background-color: #82bf51; +} + +#sidebox-actions .body { + border-color: #ae7cad; + padding-left: 1em; +} + +#sidebox-sourcelist .title { + border-color: #93d94c; + /* + */ + background-color: #93d94c; + background-color: #9f7cc8; + background-color: #937dad; +} + +#sidebox-sourcelist .body { + border-color: #94d94c; + padding: 0; +} + +#sidebox-sourcelist li a { + display: block; + padding: 0.25em 0.5em; + color: #76539f; +} +#sidebox-sourcelist li.selected a, +#sidebox-sourcelist li a.selected { + /* + background-color: #7ed12d; + background: #7bc828 url(../img/navselhilight.png) repeat-x top; + */ + background-color: #dfd3ed; +} +#sidebox-sourcelist li a:hover { + text-decoration: none; + background-color: #f8f2f8; +} +#sidebox-sourcelist li.selected a:hover, +#sidebox-sourcelist li a.selected:hover { + background-color: #d9c8e6; +} +#sidebox-sourcelist li a img { + vertical-align: bottom; + margin-right: 3px; +} + +#sidebox-sourcelist #calendar-list { padding: 0.375em 0; } + +#sidebox-upcoming .title { + border-color: #8f5902; + /* + background-color: #e9b96e; + */ + background-color: #8f5902; +} + +#sidebox-upcoming .body { + border-color: #8f5902; +} + +/* Misc. Development stuff */ +#_MockiKit_LoggingPane { + z-index: 101; +} +#interpreter_form { + background-color: Window; + color: WindowText; + border-bottom: 1px solid ThreeDShadow; + position: relative; + z-index: 102; + font-size: 12pt; +} +#interpreter_area { + background-color: #fff; + color: #000; +} + +.left { float: left; } +.right { float: right; } + +/*** Forms style ***/ +input { + font-family: inherit; + font-size: inherit; + /* we probably shouldn't inherit the color without inheriting + background? */ + /* color: inherit; */ +} + +input, +textarea { + padding: 1px; +} +input[type="text"], +input[type="search"], +input[type="password"], +input:not([type]), +button, +textarea { + border: 1px solid #bbbed0; + padding: 2px; + background: #fff url(../img/grad-textarea.png) repeat-x top; +} +input[type="text"]:focus, +input[type="search"]:focus, +input[type="password"]:focus, +input:not([type]):focus, +button:focus, +textarea:focus { + /* + border: 2px solid Highlight; + padding: 1px; + */ +} +input[type="text"]:hover, +input[type="search"]:hover, +input[type="password"]:hover, +input:not([type]):hover, +button:hover, +textarea:hover { + background-color: #fffdf9; +} + +input.empty { color: #999; } + +.text-entry { + border: 1px solid #bbb; +} + +/* Due to IE6's braindeadness, we cannot combine this with CSS that includes [type=""], as below */ +.toggle-button { + -moz-border-radius: 2px; + background-image: url(../img/bg-button.png); + background-position: bottom; + background-repeat: repeat-x; + font-weight: normal; + padding: 1px 3pt !important; + border-width: 0; +} +.widgetcolor, +.toggle-button { + background-color: #fbf6e3; + color: #3c2b14; + -moz-border-top-colors: #ce9d59 #fdf8f2 transparent; + -moz-border-right-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-bottom-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-left-colors: #ce9d59 #fdf8f2 transparent; + padding: 2pt; +} + +/* The comment above applies here, plus IE6 applies .toggle-button.selected + to all .selected unless it's specifically specified, as below */ +.toggle-button-group .toggle-button.selected { + background-image: url(../img/bg-button-selected.png); + background-color: #ffd69b; +} + +/* Hacks to make buttons look better in Mozilla-based browsers */ +body.os-x11.eng-gecko input[type="submit"], +body.os-x11.eng-gecko input[type="reset"], +body.os-x11.eng-gecko input[type="button"], +body.os-x11.eng-gecko button { + -moz-border-radius: 2px; + background-image: url(../img/bg-button.png); + background-position: bottom; + background-repeat: repeat-x; + font-weight: normal; + padding: 1px 3pt !important; + border-width: 2px; +} + +body.os-x11.eng-gecko input[type="submit"], +body.os-x11.eng-gecko input[type="reset"], +body.os-x11.eng-gecko input[type="button"], +body.os-x11.eng-gecko button { + background-color: #fbf6e3; + color: #3c2b14; + -moz-border-top-colors: #ce9d59 #fdf8f2 transparent; + -moz-border-right-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-bottom-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-left-colors: #ce9d59 #fdf8f2 transparent; +} +body.os-x11.eng-gecko input[type="submit"]:hover, +body.os-x11.eng-gecko input[type="reset"]:hover, +body.os-x11.eng-gecko input[type="button"]:hover, +body.os-x11.eng-gecko button:hover { + background-color: #fff; +} +body.os-x11.eng-gecko input[type="submit"]:focus, +body.os-x11.eng-gecko input[type="reset"]:focus, +body.os-x11.eng-gecko input[type="button"]:focus, +body.os-x11.eng-gecko button:focus { + -moz-border-top-colors: Highlight Highlight transparent; + -moz-border-right-colors: Highlight Highlight transparent; + -moz-border-bottom-colors: Highlight Highlight transparent; + -moz-border-left-colors: Highlight Highlight transparent; +} +body.os-x11.eng-gecko input[type="submit"]:active, +body.os-x11.eng-gecko input[type="reset"]:active, +body.os-x11.eng-gecko input[type="button"]:active, +body.os-x11.eng-gecko button:active { + background-image: url(../img/bg-button-pressed.png); + background-position: top; + -moz-border-top-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-right-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-bottom-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-left-colors: #ce9d59 #fcf7f0 transparent; +} +body.os-x11.eng-gecko input[type="submit"][disabled], +body.os-x11.eng-gecko input[type="reset"][disabled], +body.os-x11.eng-gecko input[type="button"][disabled], +body.os-x11.eng-gecko button[disabled] { + -moz-border-top-colors: #ce9d59 #fdf8f2 transparent; + -moz-border-right-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-bottom-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-left-colors: #ce9d59 #fdf8f2 transparent; +} + +body.os-x11.eng-gecko input[disabled], +body.os-x11.eng-gecko input[disabled]:hover, +body.os-x11.eng-gecko input[disabled]:active, +body.os-x11.eng-gecko select[disabled], +body.os-x11.eng-gecko select[disabled]:hover, +body.os-x11.eng-gecko select[disabled]:active, +body.os-x11.eng-gecko textarea[disabled], +body.os-x11.eng-gecko textarea[disabled]:hover, +body.os-x11.eng-gecko textarea[disabled]:active { + background-image: none; + background-color: #f4f4f4; + border-color: #bbb; + color: #999; + opacity: 0.75; +} +body.os-x11.eng-gecko input[type="button"][disabled], +body.os-x11.eng-gecko input[type="button"][disabled]:hover, +body.os-x11.eng-gecko input[type="button"][disabled]:active, +body.os-x11.eng-gecko input[type="reset"][disabled], +body.os-x11.eng-gecko input[type="reset"][disabled]:hover, +body.os-x11.eng-gecko input[type="reset"][disabled]:active, +body.os-x11.eng-gecko input[type="submit"][disabled], +body.os-x11.eng-gecko input[type="submit"][disabled]:hover, +body.os-x11.eng-gecko input[type="submit"][disabled]:active, +body.os-x11.eng-gecko button[disabled], +body.os-x11.eng-gecko button[disabled]:hover, +body.os-x11.eng-gecko button[disabled]:active { + background-image: none; + background-color: #fbf3e2; + color: #bbb; + opacity: 0.75; +} + +body.eng-msie input, +body.eng-msie button, +body.eng-msie select { + font-family: tahoma, arial, helvetica, sans-serif; + font-size: 9pt; +} + +body.os-mac input, +body.os-mac button, +body.os-mac select { + font-family: lucida grande, helvetica, sans-serif; + font-size: 0.95em; +} +body.os-mac.eng-webkit input, +body.os-mac.eng-webkit button, +body.os-mac.eng-webkit select { + font-size: 1em; +} + +input, button, select, textarea { } + +body.os-x11.eng-gecko input[type="checkbox"], +body.os-x11.eng-gecko select { + -moz-border-top-colors: Transparent #c9bdab; + -moz-border-right-colors: White #c9bdab; + -moz-border-bottom-colors: White #c9bdab; + -moz-border-left-colors: Transparent #c9bdab; +} + body.os-x11.eng-gecko input[type="checkbox"]:focus/*, + body.os-x11.eng-gecko select:focus*/ { + -moz-border-top-colors: Highlight Highlight transparent; + -moz-border-right-colors: Highlight Highlight transparent; + -moz-border-bottom-colors: Highlight Highlight transparent; + -moz-border-left-colors: Highlight Highlight transparent; + } + /* Okay, this :hover is a Moz/FF work-around to make single-clicking selects work. */ + /*select:hover { + -moz-border-top-colors: Transparent #c9bdab; + -moz-border-right-colors: White #c9bdab; + -moz-border-bottom-colors: White #c9bdab; + -moz-border-left-colors: Transparent #c9bdab; + }*/ + + body.os-x11.eng-gecko input[type="checkbox"] { + background-image: url(../img/grad-textarea.png); + background-position: top; + background-repeat: repeat-x; + background: #fff url(../img/grad-textarea.png) repeat-x top; + } + +#content-iframe { + width: 100%; +} + +/* Extra developer-centric links below the sidebox */ +#extra { + color: #999; + font-size: 75%; + text-align: center; + line-height: 1.5; + margin: 3ex 0 0; +} +#extra a { + color: #999; +} + +.indented { + margin-left: 16px; +} + +.scrolledheader { + margin-right: 16px; +} + +#sidebox-sidebook { } + + #sidebox-sidebook .notebook-tabs { + background-color: #ccc; + position: static; + border: 1px solid #999; + height: 24px; + overflow: hidden; + position: relative; + } + + #sidebox-sidebook .notebook-tabs a { + margin: 0; + padding: 0; + text-align: center; + border: 0; + float: left; + width: 50%; + background: #e6e6e6 url(../img/bg-side-title.png) repeat-x 50% 50%; + border-top: 1px solid #eee; + } + #sidebox-sidebook .notebook-tabs a:hover { + background: #f6f6f6 url(../img/bg-side-title-hover.png) repeat-x 50% 50%; + } + + #sidebox-sidebook .notebook-tabs a.selected { + margin: 0; + padding: 0; + background: #bbceea url(../img/bg-selected.png) repeat-x 50% 50%; + border-color: #cfe5fb; + } + #sidebox-sidebook .notebook-tabs a img { + display: block; + border: 1px solid #999; + border-width: 0 1px; + width: 100%; + height: 23px; + } + #sidebox-sidebook .notebook-tabs a img.addressbook { border: 1px solid #999; border-width: 0 1px 0 0; } + #sidebox-sidebook .notebook-tabs a img.todo { border: none; } + + .eng-msie.engv-6 #sidebox-sidebook .notebook-tabs a img { border: none; } + .eng-msie.engv-6 #sidebox-sidebook .notebook-tabs a img.calendars { border-width: 0; } + + #sidebox-sidebook .notebook-content { + margin: 0; + background-color: #f6f6f6; + border: 1px solid #999; + } + +.group-footer, +.search-group { + clear: both; +} + +pre { + white-space: pre-wrap; /* CSS3 */ + white-space: -moz-pre-wrap; /* Moz */ + white-space: -o-pre-wrap; /* Opera */ + word-wrap: break-word; /* IE */ +} + +#loading { + font-family: sans-serif; + font-size: 18pt; + text-decoration: blink !important; + position: absolute; + top: 25%; + left: 0; + right: 0; + width: 100%; + text-align: center; +} + +#tzlabel { color: #fff; } + +#search-no-results +{ + padding-top: 25%; +} + +#nomail-summary +{ + padding-top: 50%; + text-align: center; +} + +#preferences-container +{ + padding-right: 2em; + padding-left: 2em; +} diff --git a/src/www/static/css/log.css b/src/www/static/css/log.css new file mode 100644 index 0000000..5f39c30 --- /dev/null +++ b/src/www/static/css/log.css @@ -0,0 +1,106 @@ +/* + * log.css + * Visual themeing for browser-based logging provided by Log.js + * + * (c) 2008 Alexander Hixon + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#logbox { + display: block; + z-index: 1000; + left: 0px; + bottom: 0px; + position: fixed; + width: 100%; +} + +#log { + font-family: Inconsolata, Dejavu Sans Mono, monospace; + background-color: #2e3436; + color: #eeeeec; + padding: 2px; + height: 100px; + overflow: auto; +} + +#jsprompt { + font-family: Inconsolata, Dejavu Sans Mono, monospace; + background-color: #555753; + color: #eeeeec; + + border: 0px none; + width: 100%; +} + +#log div.error { + color: #ef2929; +} + +#log div.debug { + color: #73d216; +} + +#log div.warn { + color: #f57900; +} + +#log .group div.error { + color: #a40000; +} + +#log a, #log a:active { + text-decoration: none; + border-bottom: 1px dotted #babdb6; + color: #eeeeec; +} + +#log .groupbox a, #log .groupbox a:active { + color: #2e3436; +} + +#log div.a { + color: #eeeeec; +} + +#log a:hover { + border-bottom: 0px none; +} + +#log .titlespan { + background-color: #888a85; + width: 100%; +} + +.group { + padding: 2px; + background-color: #555753; +} + +.groupbox { + margin-top: 0.5em; + margin-bottom: 0.5em; + margin-left: 1em; + border: 1px solid #babdb6; +} + +#log .floatright { + float: right; + color: #eeeeec; + /*padding: 2px; + display: inline;*/ +} diff --git a/src/www/static/css/login.css b/src/www/static/css/login.css new file mode 100644 index 0000000..eb758f6 --- /dev/null +++ b/src/www/static/css/login.css @@ -0,0 +1,92 @@ +body.login { + background-color: #1d3980; + overflow: hidden; +} + + #login-pane { + position: absolute; + top: 25%; + left: 0; + right: 0; + text-align: center; + color: #333; + padding: 1em 0 1em; + border-top: 2px solid #d4dceb; + border-bottom: 2px solid #e4eaf4; + margin-bottom: 298px; + background: #b4c5e1 url(../img/login-bg-middle.png) repeat-x 0 0; + } + + #login-logo { + position: absolute; + top: -76px; + left: 0; + right: 0; + width: 100%; + width: 100%; + font-size: 0; + text-indent: -2000px; + color: transparent; + overflow: hidden; + height: 118px; + display: block; + background: url(../img/login-logo.png) no-repeat 50% 0; + } + .eng-msie.engv-6 #login-logo { background-image: url(../img/login-logo.gif); } + + #login-message { + padding: 3em 0 0.75em; + font-size: 87%; + } + + #login-pane input { + color: #000; + } + + #login-pane table { + text-align: left; + margin: 0 auto; + } + + #login-pane th, + #login-pane td { + text-align: right; + font-weight: normal; + } + + #login-pane td.default { + text-align: left; + } + + #login-pane .text-entry { + width: 15em; + } + + #login-pane h2 { + color: #99b; + margin: 1em; + } + + #login-pane a { + } + + #login-bg-top, + #login-bg-bottom { + position: absolute; + width: 100%; + left: 0; + right: 0; + } + #login-bg-top { + background: url(../img/login-bg-top.png) repeat-x 0 100%; + height: 185px; + top: -188px; + border-bottom: 1px solid #385798; + } + #login-bg-bottom { + background: url(../img/login-bg-bottom.png) repeat-x 0 0; + height: 293px; + bottom: -296px; + border-top: 1px solid #1d3980; + } + .eng-msie.engv-6 #login-bg-bottom { bottom: -298px; } diff --git a/src/www/static/css/print.css b/src/www/static/css/print.css new file mode 100644 index 0000000..a74eba3 --- /dev/null +++ b/src/www/static/css/print.css @@ -0,0 +1,201 @@ +body, +#content { + background: none; + border: none; + font-size: 10pt; + font-family: times new roman, times, serif !important; + /* + display: inline; + */ +} + +/* Turn off UI chrome */ +#notification, +#section-user, +#section-sidebar, +#sourcebar, +#toolbar, +#tabs-and-search, +#conv-breadcrumbs, +#page-header +{ display: none !important; } + +#page-frame #section-content, +#page-frame td td { display: table-cell; } + +#content-iframe { display: block; } + +/* Summary */ + +/* Mail */ +.msg.closed { display: none; } + +#conv-msg-list { padding: 0; } + +.msg { + border-top: 2px dashed #000; + margin-top: 2em; + page-break-before: auto; + page-break-after: auto; + page-break-inside: avoid; +} + + .msg .msg-content { border: none; } + + .msg .msg-header { + background: none !important; + page-break-inside: avoid; + display: block; + margin-bottom: 1em; + } + + .msg .msg-header .sender { + padding: 0.25em 0; + margin: 0; + font-size: 150%; + display: block; + position: relative; + right: 10px; + font-weight: bold; + } + + .msg .msg-header .disclosure { + display: none; + } + .msg .msg-header .date { + display: block; + } + + .msg .msg-header .people { + padding: 0 1ex 0 0; + } + + .msg .pointer-border { + display: none; + } + + .msg .face { + top: 0; + margin: 2px; + border: 1px dashed #ccc; + /* Hide for now */ + display: none; + } + + .msg .msg-body { + clear: both; + display: inline; + line-height: 1.4; + font-size: 10pt; + page-break-before: avoid; + page-break-after: auto; + page-break-inside: avoid; + } + + .msg .msg-actions, + .msg .msg-snippet { + display: none; + } + + +/* Calendar */ +#calendar { } + + #calendar .buble { + overflow: visible; + font-size: 8pt; + } + + .timestrip, + .calstripes, + table.columnview ul.timed-events { + height: 8.25in; + } + + /* Calendar items - generic */ + #calendar .item { + -moz-border-radius: 0; + background: none !important; + } + + #calendar .columnview { } + + #calendar .columnview .item { + overflow: visible; + margin: 0; + padding: 0; + } + + #calendar .columnview .item strong { + color: #000; + background-color: #fff; + padding: 2px; + text-align: left; + margin: 2px 2px 2px 2px; + } + + #calendar .columnview .item .resize { + display: none; + } + + /* Calendar items - timed */ + #calendar .columnview .timed-events .item { + border-left: 10px solid red; + } + + /* Calendar items - untimed */ + #calendar .columnview .untimed-events .buble { + } + #calendar .columnview .untimed-events .item { + height: auto; + border-bottom: 10px solid red; + } + #calendar .columnview .untimed-events .item strong { + padding: 1px; + } + #calendar .columnview .untimed-events .item .note { + background-color: #fff; + color: #000; + font-size: 90%; + } + + .calstripes div { + border-top: 1px dotted #999; + /* Magic to make the lines not count for space */ + margin-top: -1px; + position: relative; + top: 1px; + } + + .timeval span { + } + +.scroll, +.scrollv, +.scrollh { + overflow: visible !important; + height: auto !important; + width: auto !important; + max-height: auto !important; +} + +/* +body { + overflow: auto !important; +} + +.conv-msg-list * { + position: static !important; +} +*/ + +pre { + font-family: bitstream vera sans mono, monaco, monospace; + font-size: 10pt; +} + + + +#conv-subject { + font-size: xx-large; +} diff --git a/src/www/static/css/summary.css b/src/www/static/css/summary.css new file mode 100644 index 0000000..2b81bba --- /dev/null +++ b/src/www/static/css/summary.css @@ -0,0 +1,139 @@ +#summary-table { + width: 100%; +} + +td.summary-column { + vertical-align: top; + width: 50%; +} + +.summary-category { + margin: 1em 1em 2em 1em; +} + + .summary-category h3 { + text-transform: uppercase; + letter-spacing: 1px; + font-weight: normal; + font-size: x-small; + color: #555; + border-bottom: 1px solid #ccc; + padding: 1px; + } + + /* Generic summary items */ + .summary-category li { clear: both; } + + .summary-category li a { padding: 0.5em 0.75em; } + .summary-category li a:hover { + color: #f50; + text-decoration: none; + } + .summary-category #frommycontacts-list li a:hover, + .summary-category #subscriptions-list li a:hover { + text-decoration: underline; + } + + .summary-category li a span { display: block; } + + .summary-category li.unread a { + font-weight: bold; + } + + /* Stars (and other icons, if they're going to exist) */ + .summary-category li a img { + float: left; + margin-right: 0.5em; + } + + /* Truncate multiple-row events and mail subjects */ + .summary-category.cal-summary li a span.summary-event, + .summary-category li a span.summary-subject { + line-height: 1.5em; + height: 1.5em; + overflow: hidden; + position: relative; + } + + /* Mail summary items */ + .summary-category.mail-summary a { + font-size: 85%; + display: block; + } + .summary-category li a span.summary-from { + color: #999; + font-weight: normal; + } + .summary-category li.even a span.summary-from { + /* + color: #8d99a4; + */ + } + + .summary-category li a span.summary-subject { + font-size: 120%; + } + .summary-category li a:hover span.summary-subject { + text-decoration: underline; + } + + /* Stars, etc. */ + .summary-category.mail-summary li a img { + margin-bottom: 0.5em; + } + + /* Calendar summary items */ + .summary-category.cal-summary li a { + display: block; + padding: 0.25em 0.75em; + font-size: 85%; + clear: both; + overflow: auto; + text-align: right; + } + .summary-category.cal-summary #today-list li a span.summary-event { + font-weight: bold; + } + .summary-category.cal-summary li a span.summary-event { + font-size: 120%; + float: left; + } + .summary-category.cal-summary li a:hover span.summary-event { + text-decoration: underline; + } + + .summary-category.cal-summary li a span.summary-day, + .summary-category.cal-summary li a span.summary-time { + color: #555; + display: inline; + white-space: nowrap; + } + .summary-category.cal-summary li a span.summary-day { + /*float: right;*/ + } + .summary-category.cal-summary #later-list li a span.summary-time { + padding-right: 0.75em; + color: #999; + } + +/*** Icons ***/ +.summary-category h3 { + background: transparent url(../img/message-closed-16.png) no-repeat 0 50%; + padding-left: 20px; + padding-top: 4px; +} +.cal-summary h3 { + background-image: url(../img/event-16.png); +} + +#summary-cal { + margin-top: 1em; +} +#summary-cal .date { + font-size: 90%; + text-decoration: none; +} +#summary-cal .date a:hover { + color: #f50; + text-decoration: underline; +} diff --git a/src/www/static/css/widgets.css b/src/www/static/css/widgets.css new file mode 100644 index 0000000..76026e4 --- /dev/null +++ b/src/www/static/css/widgets.css @@ -0,0 +1,568 @@ +.conversation-list { + width: 100%; + font-size: inherit; +} + +/* Force the date and time to 1 line high */ +.conversation-list .date, +.conversation-list .time { white-space: nowrap; } + +/* Eeevil hack to work around the scrollbar eating dates + * issue in IE6 (okay, perhaps it isn't that evil) */ +.eng-msie.engv-6 .conversation-list .date { padding-right: 22px; } + +.conversation-list table { + width: 100%; +} + +.conversation-list tr.unread { + font-weight: bold; +} +.conversation-list td { + color: #517faf; + padding: 0.25em 0 0.25em 0.5em; + vertical-align: top; + font-size: 86%; +} +.conversation-list td.time { + padding-right: 0.5em; +} + +.summary-category.mail-summary a, +.conversation-list tr td.subject a { color: #a38bad; } + +.summary-category .unread a, +.conversation-list tr.unread td.subject a { color: #3974ce; } + +.summary-category a .summary-from, +.conversation-list tr td.from a, +.conversation-list tr:hover td.from a { + color: #777; + text-decoration: none; +} +.summary-category .unread a .summary-from, +.conversation-list tr.unread td.from a { + color: #000; +} + +.conversation-list tr td.date a, +.conversation-list tr td.time a, +.conversation-list tr:hover td.date a, +.conversation-list tr:hover td.time a { + color: #777; + font-weight: normal; + text-decoration: none; +} + +.conversation-list tr:hover td.from a, +.conversation-list tr:hover td.subject a { + text-decoration: underline; + color: #f57900; +} + +.conversation-list .group-header td { + font-size: 105%; + color: #555; + background-color: #f6f6f6; + border: 1px solid #e2e2e2; + border-width: 1px 0; + padding: 1ex 2ex; +} +.conversation-list .group-footer .empty { + color: #aaa; + padding: 0 4ex; +} + +.even, +.conversation-list tr.even { + background-color: #f5f8fb; +} + +/* Constrain checkbox and star widths to minimum */ +.conversation-list td.checkbox, +.conversation-list td.star { width: 1%; } + +.conversation-list div.star { background-position: 0 0; } +.conversation-list div.star.starred { background-position: 0 -32px; } +.conversation-list div.star:hover { background-position: 0 -16px; } +.conversation-list div.star:focus, +.star:hover, +.star:focus { + background-position: 0 -32px; + text-decoration: none; +} +div.star { + float: left; + display: block; + list-style:none; + margin: 0 6px 0 0; + padding:0px; + width: 16px; + height: 16px; + background: url(../img/star-widget.png) 0 -16px no-repeat; + text-indent: -1000em; +} +/* for IE6 only */ +.eng-msie.engv-6 div.star { background-image: url(../img/star-widget.gif); } + +hr.hresize, +hr.vresize { + border: 0; + padding: 2px; + cursor: move; +} +hr.hresize { + border-width: 0 1px; +} +hr.vresize { + border-width: 1px 0; +} + +/* Rounded corners widgets */ +.t, .tr, .c, .cr, .b, .br { + display: block; + z-index: -1; + background-repeat: no-repeat; +} + .t { + /* + height: 100%; + */ + position: absolute; + top: 0; left: 0; right: 0; + bottom: 0; + margin: 0 24px 24px 0; + background-position: top left; + } + .tr { + margin: 0 0 24px 0; + position: absolute; + right: -24px; + top: 0; + height: 100%; + width: 24px; + background-position: top right; + } + .b { + margin: 0 24px 0 0; + position: absolute; + bottom: 0; left: 0; right: 0; + height: 24px; + background-position: bottom left; + } + .br { + z-index: 22; + position: absolute; + bottom: 0; + right: -24px; + width: 24px; + height: 24px; + background-position: bottom right; + } + .c { + display: none; + height: auto; + position: absolute; + top: 24px; bottom: 24px; left: 0; right: 0; + background-repeat: repeat-y; + background-position: left; + } + .cr { + display: none; + margin-right: -24px; + height: auto; + position: absolute; + top: 0; right: 0; bottom: 0; left: 0; + background-repeat: repeat-y; + background-position: right; + } + +/* Define a scrollable region -- just needs the size (height and/or width) set */ +.scroll, +.scrollv, +.scrollh { + position: relative; + overflow: auto; +} +/* Fix for IE */ +.eng-msie.engv-6 .scroll, .eng-msie.engv-6 .scrollv, .eng-msie.engv-6 .scrollh { width: 100%; } + + .scrollv { + overflow-x: hidden; + overflow-y: auto; + } + .scrollh { + overflow-x: auto; + overflow-y: hidden; + } + +#section-sidebar .notebook { font-size: 87%; } + +.notebook-tabs { + margin-top: 1ex; + z-index: 5; + position: relative; + top: 2px; + overflow: auto; +} + .notebook-tabs a { + display: block; + float: left; + margin: 1px; + padding: 0.25ex 1.5ex 0.5ex; + } + .notebook-tabs a.selected { + margin: 0; + border: 1px solid #d6ae12; + border-bottom: none; + background-color: #fffcf4; + outline: none; + color: #4f420e; + text-decoration: none; + cursor: default; + } + +/* IE6 has problems with the tab floats */ +.eng-msie.engv-6 .notebook-content { + margin-top: -8px; + position: relative; + clear: both; +} + +.notebook.tabs-visible .notebook-content { + padding: 1.5ex; + margin-bottom: 1ex; + border: 1px solid #d6ae12; + background-color: #fffcf4; +} + + .autocompletions ul { + list-style-type: none; + background: #fff; + border: 1px solid ThreeDDarkShadow; + margin: 0; + padding: 0; + overflow: auto; + overflow-x: hidden; + overflow-y: auto; + /* Line height * lines = max-height (8 lines * 1.5 = 12em) */ + /* Autocomplete scrolls after 8 lines, in other words */ + max-height: 12em; + } + + .autocompletions li { + white-space: nowrap; + list-style-type: none; + margin: 0; + padding: 1px; + display: block; + } + + .autocompletions li.selected { + /* Native system highlight colors */ + background: Highlight; + color: HighlightText; + } + +/* toggle buttons */ +.toggle-button-group { + text-align: center; + border-collapse: collapse; +} + +.toggle-button-group td { + border: 1px solid #ce9d59; + padding: 0; +} + +.toggle-button-group td.spacer { + border: none; +} +.toggle-button-group td a { + display: block; +} +.toggle-button-group td a:hover { + text-decoration: none; +} +.toggle-button { + display: block; +} + +/* list selector */ +.list-selector { margin-right: 0.5em; } + + .list-selector .search-field { + width: 100%; + margin-bottom: 1ex; + } + + .list-selector .unselected-items { + width: 100%; + background-color: white; + overflow: auto; + overflow-y: auto; + overflow-x: auto; + border: 1px solid #bbbed0; + position: relative; + } + + .list-selector li { + padding: 3px 5px; + cursor: default; + line-height: 1; + white-space: nowrap; + position: relative; + overflow: hidden; + } + .eng-msie.engv-6 .list-selector li { + white-space: normal; + position: static; + } + + .list-selector li.popped-out { background-color: #e1e9f3; } + + .list-selector.searching .unselected-items li {display: none;} + .list-selector.searching .unselected-items li.hit {display: block;} + + .list-selector li input { + vertical-align: middle; + } + .list-selector li .icon { + /* + position: relative; + z-index: 5; + */ + } + .list-selector li .cal-name { + white-space: nowrap; + overflow: hidden; + line-height: 1.5; + position: absolute; + left: 22px; right: 24px; + } + + /* IE6 is going to have to wrap, at least for now. */ + .eng-msie.engv-6 .list-selector li .cal-name { + white-space: normal; + position: static; + overflow: display; + } + + +/*** The Popup Buble ***/ +.popup-container { + position: absolute; + left: 0; + top: 0; + visibility: hidden; +} + + .popup-content p { + overflow: auto; + max-width: 10em; + } + .calendar-popup .popup-content p { + overflow: visible; + max-width: none; + } + + .popup-content .description { + max-width: 700px; + max-height: 250px; + overflow: auto; + } + .eng-msie .popup-content .description { + width: 35em; + height: 10em; + } + + .popup-container .pointer { + position: absolute; + display: block; + width: 25px; + height: 25px; + background-repeat: no-repeat; + } + + .popup-container .pointer.above { + background-image: url(../img/info-pointer-down.png); + bottom: -3px; + left: -8px; + } + + .popup-container .pointer.below { + background-image: url(../img/info-pointer-up.png); + top: -3px; + left: -8px; + } + + .popup-container .pointer.right { + background-image: url(../img/info-pointer-left.png); + top: -8px; + left: -3px; + } + + .popup-container .pointer.left { + background-image: url(../img/info-pointer-right.png); + top: -8px; + right: -3px; + } + + .eng-msie.engv-6 .popup-container .pointer.above { background-image: url(../img/info-pointer-down.gif); } + .eng-msie.engv-6 .popup-container .pointer.below { background-image: url(../img/info-pointer-up.gif); } + .eng-msie.engv-6 .popup-container .pointer.right{ background-image: url(../img/info-pointer-left.gif); } + .eng-msie.engv-6 .popup-container .pointer.left { background-image: url(../img/info-pointer-right.gif); } + +.popup-buble { + position: relative; + font-size: 0.9em; + line-height: 1.4em; + min-height: 48px; + min-width: 125px; + padding: 8px 14px 12px 10px; +} + + .popup-buble.closable .occurrence-entry, + .popup-buble.closable h3 { margin-right: 26px; } + + .popup-buble h2 { + margin: 0; + padding: 0; + font-size: 1.2em; + display: inline; + } + + .popup-buble h3 { padding-bottom: 10px; } + + .popup-buble p { max-width: 25em; } + + .popup-buble td div.date { display: none; } + .popup-buble .calendar { background-color: white; } + + /* (X) graphic */ + .popup-buble .close { + display: block; + position: absolute; + overflow: hidden; + top: 8px; + right: 12px; + width: 20px; + height: 20px; + background: url(../img/x.png) 0 0; + cursor: pointer; cursor: hand; + } + + .popup-buble .close:hover { + background-position: 20px 0; + } + + .popup-buble .pointer { + width: 25px; + height: 25px; + display: block; + position: absolute; + background-repeat: no-repeat; + } + .popup-buble .pointer.right { + background-image: url(../img/info-pointer-right.png); + right: -20px; + } + .popup-buble .pointer.left { + background-image: url(../img/info-pointer-left.png); + left: -23px; + } + .popup-buble .pointer.up { + background-image: url(../img/info-pointer-up.png); + top: -23px; + } + .popup-buble .pointer.down { + background-image: url(../img/info-pointer-down.png); + bottom: -20px; + } + + .eng-msie.engv-6 .popup-buble .pointer.right { background-image: url(../img/info-pointer-right.gif); } + .eng-msie.engv-6 .popup-buble .pointer.left { background-image: url(../img/info-pointer-left.gif); } + .eng-msie.engv-6 .popup-buble .pointer.up { background-image: url(../img/info-pointer-up.gif); } + .eng-msie.engv-6 .popup-buble .pointer.down { background-image: url(../img/info-pointer-down.gif); } + +/* popup-buble IE6 hacks */ +.eng-msie.engv-6 .t, .eng-msie.engv-6 .tr, .eng-msie.engv-6 .c, .eng-msie.engv-6 .cr, .eng-msie.engv-6 .b, .eng-msie.engv-6 .br { display: none; } +.eng-msie.engv-6 .popup-buble { + background-color: #fffbe9; + border: 1px solid #c8a000; + margin: 3px 4px 4px 3px; + width: 16em; +} +.eng-msie.engv-6 .popup-buble .form-list li { white-space: nowrap; } +.eng-msie.engv-6 .popup-buble .form-list li label { margin-right: 1ex; } + +.form-list { + padding: 5px; +} + +.form-list li { + padding-top: 7px; +} + +.form-list label { + display: inline-block; + line-height: 1.8; + text-align: right; + vertical-align: top; + width: 75px; +} +.eng-gecko .form-list label { display: -moz-inline-box; } + +hr { + border: 1px dotted #ddd; + background-color: #ddd; + border-width: 0 0 1px 0; + padding: 0; + height: 1px; +} +.popup-buble hr { + border-color: #c8a000; + background-color: #c8a000; +} + +.action, +.actions { white-space: nowrap; } + +.actions { min-width: 20em; } + +.calendar-popup .actions { + min-width: 0; + overflow: visible !important; +} + +.tz-wrapper { + position: relative; + display: inline; +} + +.tz-editor { + position: absolute; + width: 300px; + padding: 6px; + right: 0px; + display: none; + color: black; + background-color: #f6f6f6; + border: 1px solid #bbb; + z-index: 20; +} + + .tz-editor td { + width: 50%; + } + + .tz-editor select { + border-width: 1px; + width: 100%; + } + + .tz-editor th { + padding: 0px; + font-weight: normal; + text-align: left; + } diff --git a/src/www/static/dragonfly.png b/src/www/static/dragonfly.png new file mode 100644 index 0000000..8c81499 Binary files /dev/null and b/src/www/static/dragonfly.png differ diff --git a/src/www/static/favicon.ico b/src/www/static/favicon.ico new file mode 100644 index 0000000..b65fbce Binary files /dev/null and b/src/www/static/favicon.ico differ diff --git a/src/www/static/img/Bongo-logo.png b/src/www/static/img/Bongo-logo.png new file mode 100755 index 0000000..a752adc Binary files /dev/null and b/src/www/static/img/Bongo-logo.png differ diff --git a/src/www/static/img/accessories-text-editor.gif b/src/www/static/img/accessories-text-editor.gif new file mode 100644 index 0000000..bfd8c2b Binary files /dev/null and b/src/www/static/img/accessories-text-editor.gif differ diff --git a/src/www/static/img/accessories-text-editor.png b/src/www/static/img/accessories-text-editor.png new file mode 100644 index 0000000..3155f7e Binary files /dev/null and b/src/www/static/img/accessories-text-editor.png differ diff --git a/src/www/static/img/addressbook-16.png b/src/www/static/img/addressbook-16.png new file mode 100644 index 0000000..b64b42f Binary files /dev/null and b/src/www/static/img/addressbook-16.png differ diff --git a/src/www/static/img/agent-bongoantispam.png b/src/www/static/img/agent-bongoantispam.png new file mode 100644 index 0000000..08456b3 Binary files /dev/null and b/src/www/static/img/agent-bongoantispam.png differ diff --git a/src/www/static/img/agent-bongoavirus.png b/src/www/static/img/agent-bongoavirus.png new file mode 100644 index 0000000..8714aa2 Binary files /dev/null and b/src/www/static/img/agent-bongoavirus.png differ diff --git a/src/www/static/img/agent-bongocalcmd.png b/src/www/static/img/agent-bongocalcmd.png new file mode 100644 index 0000000..2e3a4b0 Binary files /dev/null and b/src/www/static/img/agent-bongocalcmd.png differ diff --git a/src/www/static/img/agent-bongocollector.png b/src/www/static/img/agent-bongocollector.png new file mode 100644 index 0000000..2e3a4b0 Binary files /dev/null and b/src/www/static/img/agent-bongocollector.png differ diff --git a/src/www/static/img/agent-bongoimap.png b/src/www/static/img/agent-bongoimap.png new file mode 100644 index 0000000..57ac1fc Binary files /dev/null and b/src/www/static/img/agent-bongoimap.png differ diff --git a/src/www/static/img/agent-bongomailprox.png b/src/www/static/img/agent-bongomailprox.png new file mode 100644 index 0000000..eec6a3c Binary files /dev/null and b/src/www/static/img/agent-bongomailprox.png differ diff --git a/src/www/static/img/agent-bongopluspack.png b/src/www/static/img/agent-bongopluspack.png new file mode 100644 index 0000000..9e81e94 Binary files /dev/null and b/src/www/static/img/agent-bongopluspack.png differ diff --git a/src/www/static/img/agent-bongopop3.png b/src/www/static/img/agent-bongopop3.png new file mode 100644 index 0000000..57ac1fc Binary files /dev/null and b/src/www/static/img/agent-bongopop3.png differ diff --git a/src/www/static/img/agent-bongoqueue.png b/src/www/static/img/agent-bongoqueue.png new file mode 100644 index 0000000..2e3a4b0 Binary files /dev/null and b/src/www/static/img/agent-bongoqueue.png differ diff --git a/src/www/static/img/agent-bongosmtp.png b/src/www/static/img/agent-bongosmtp.png new file mode 100644 index 0000000..9655f1e Binary files /dev/null and b/src/www/static/img/agent-bongosmtp.png differ diff --git a/src/www/static/img/agent-control.png b/src/www/static/img/agent-control.png new file mode 100644 index 0000000..b631d86 Binary files /dev/null and b/src/www/static/img/agent-control.png differ diff --git a/src/www/static/img/agent-global.png b/src/www/static/img/agent-global.png new file mode 100644 index 0000000..ae424d1 Binary files /dev/null and b/src/www/static/img/agent-global.png differ diff --git a/src/www/static/img/agent-unknown.png b/src/www/static/img/agent-unknown.png new file mode 100644 index 0000000..2e3a4b0 Binary files /dev/null and b/src/www/static/img/agent-unknown.png differ diff --git a/src/www/static/img/airplane-emblem.png b/src/www/static/img/airplane-emblem.png new file mode 100644 index 0000000..18ac253 Binary files /dev/null and b/src/www/static/img/airplane-emblem.png differ diff --git a/src/www/static/img/ajax-loader.gif b/src/www/static/img/ajax-loader.gif new file mode 100644 index 0000000..8fcc96c Binary files /dev/null and b/src/www/static/img/ajax-loader.gif differ diff --git a/src/www/static/img/applications-internet.gif b/src/www/static/img/applications-internet.gif new file mode 100644 index 0000000..c40fec0 Binary files /dev/null and b/src/www/static/img/applications-internet.gif differ diff --git a/src/www/static/img/applications-internet.png b/src/www/static/img/applications-internet.png new file mode 100644 index 0000000..6ee500b Binary files /dev/null and b/src/www/static/img/applications-internet.png differ diff --git a/src/www/static/img/arrow.png b/src/www/static/img/arrow.png new file mode 100644 index 0000000..3dfbd7a Binary files /dev/null and b/src/www/static/img/arrow.png differ diff --git a/src/www/static/img/bg-button-pressed.png b/src/www/static/img/bg-button-pressed.png new file mode 100644 index 0000000..b1fb90b Binary files /dev/null and b/src/www/static/img/bg-button-pressed.png differ diff --git a/src/www/static/img/bg-button-selected.png b/src/www/static/img/bg-button-selected.png new file mode 100644 index 0000000..2f0c7cc Binary files /dev/null and b/src/www/static/img/bg-button-selected.png differ diff --git a/src/www/static/img/bg-button.png b/src/www/static/img/bg-button.png new file mode 100644 index 0000000..60399d2 Binary files /dev/null and b/src/www/static/img/bg-button.png differ diff --git a/src/www/static/img/bg-header.png b/src/www/static/img/bg-header.png new file mode 100644 index 0000000..dcc54e6 Binary files /dev/null and b/src/www/static/img/bg-header.png differ diff --git a/src/www/static/img/bg-page.png b/src/www/static/img/bg-page.png new file mode 100644 index 0000000..5f81a28 Binary files /dev/null and b/src/www/static/img/bg-page.png differ diff --git a/src/www/static/img/bg-selected.png b/src/www/static/img/bg-selected.png new file mode 100644 index 0000000..cbba7d5 Binary files /dev/null and b/src/www/static/img/bg-selected.png differ diff --git a/src/www/static/img/bg-shine.png b/src/www/static/img/bg-shine.png new file mode 100644 index 0000000..6e502ff Binary files /dev/null and b/src/www/static/img/bg-shine.png differ diff --git a/src/www/static/img/bg-side-title-hover.png b/src/www/static/img/bg-side-title-hover.png new file mode 100644 index 0000000..b736260 Binary files /dev/null and b/src/www/static/img/bg-side-title-hover.png differ diff --git a/src/www/static/img/bg-side-title.png b/src/www/static/img/bg-side-title.png new file mode 100644 index 0000000..5cafa90 Binary files /dev/null and b/src/www/static/img/bg-side-title.png differ diff --git a/src/www/static/img/bg-tab-selected.png b/src/www/static/img/bg-tab-selected.png new file mode 100644 index 0000000..8953008 Binary files /dev/null and b/src/www/static/img/bg-tab-selected.png differ diff --git a/src/www/static/img/bg-timestrip-right.png b/src/www/static/img/bg-timestrip-right.png new file mode 100644 index 0000000..c450143 Binary files /dev/null and b/src/www/static/img/bg-timestrip-right.png differ diff --git a/src/www/static/img/bg-timestrip.png b/src/www/static/img/bg-timestrip.png new file mode 100644 index 0000000..056aa5e Binary files /dev/null and b/src/www/static/img/bg-timestrip.png differ diff --git a/src/www/static/img/bg-toolbar.png b/src/www/static/img/bg-toolbar.png new file mode 100644 index 0000000..1447605 Binary files /dev/null and b/src/www/static/img/bg-toolbar.png differ diff --git a/src/www/static/img/bg.png b/src/www/static/img/bg.png new file mode 100755 index 0000000..086e877 Binary files /dev/null and b/src/www/static/img/bg.png differ diff --git a/src/www/static/img/big-info.png b/src/www/static/img/big-info.png new file mode 100644 index 0000000..3507269 Binary files /dev/null and b/src/www/static/img/big-info.png differ diff --git a/src/www/static/img/blank.gif b/src/www/static/img/blank.gif new file mode 100644 index 0000000..64850d8 Binary files /dev/null and b/src/www/static/img/blank.gif differ diff --git a/src/www/static/img/bongo-favicon.gif b/src/www/static/img/bongo-favicon.gif new file mode 100644 index 0000000..b73d3aa Binary files /dev/null and b/src/www/static/img/bongo-favicon.gif differ diff --git a/src/www/static/img/bongo-favicon.ico b/src/www/static/img/bongo-favicon.ico new file mode 100644 index 0000000..e62b90f Binary files /dev/null and b/src/www/static/img/bongo-favicon.ico differ diff --git a/src/www/static/img/bongo-favicon.png b/src/www/static/img/bongo-favicon.png new file mode 100644 index 0000000..8102990 Binary files /dev/null and b/src/www/static/img/bongo-favicon.png differ diff --git a/src/www/static/img/cal-offmonth.png b/src/www/static/img/cal-offmonth.png new file mode 100644 index 0000000..0c7951e Binary files /dev/null and b/src/www/static/img/cal-offmonth.png differ diff --git a/src/www/static/img/cal-selected-stipple.png b/src/www/static/img/cal-selected-stipple.png new file mode 100644 index 0000000..5e3766d Binary files /dev/null and b/src/www/static/img/cal-selected-stipple.png differ diff --git a/src/www/static/img/cal-selected.png b/src/www/static/img/cal-selected.png new file mode 100644 index 0000000..a448dc8 Binary files /dev/null and b/src/www/static/img/cal-selected.png differ diff --git a/src/www/static/img/cal-today-stipple.png b/src/www/static/img/cal-today-stipple.png new file mode 100644 index 0000000..5897b0b Binary files /dev/null and b/src/www/static/img/cal-today-stipple.png differ diff --git a/src/www/static/img/cal-today.png b/src/www/static/img/cal-today.png new file mode 100644 index 0000000..cd23f08 Binary files /dev/null and b/src/www/static/img/cal-today.png differ diff --git a/src/www/static/img/calendar-16.gif b/src/www/static/img/calendar-16.gif new file mode 100644 index 0000000..45e84d1 Binary files /dev/null and b/src/www/static/img/calendar-16.gif differ diff --git a/src/www/static/img/calendar-16.png b/src/www/static/img/calendar-16.png new file mode 100644 index 0000000..018f518 Binary files /dev/null and b/src/www/static/img/calendar-16.png differ diff --git a/src/www/static/img/cbg.png b/src/www/static/img/cbg.png new file mode 100755 index 0000000..4cfc20c Binary files /dev/null and b/src/www/static/img/cbg.png differ diff --git a/src/www/static/img/close.gif b/src/www/static/img/close.gif new file mode 100644 index 0000000..773013b Binary files /dev/null and b/src/www/static/img/close.gif differ diff --git a/src/www/static/img/closed-read.png b/src/www/static/img/closed-read.png new file mode 100644 index 0000000..cf642b8 Binary files /dev/null and b/src/www/static/img/closed-read.png differ diff --git a/src/www/static/img/closed-unread.png b/src/www/static/img/closed-unread.png new file mode 100644 index 0000000..008fd6c Binary files /dev/null and b/src/www/static/img/closed-unread.png differ diff --git a/src/www/static/img/contact-16.png b/src/www/static/img/contact-16.png new file mode 100644 index 0000000..29cd84b Binary files /dev/null and b/src/www/static/img/contact-16.png differ diff --git a/src/www/static/img/contact-new.png b/src/www/static/img/contact-new.png new file mode 100644 index 0000000..2640eaf Binary files /dev/null and b/src/www/static/img/contact-new.png differ diff --git a/src/www/static/img/contact-unknown.png b/src/www/static/img/contact-unknown.png new file mode 100644 index 0000000..6beef46 Binary files /dev/null and b/src/www/static/img/contact-unknown.png differ diff --git a/src/www/static/img/continued-left.png b/src/www/static/img/continued-left.png new file mode 100644 index 0000000..0b1ab3b Binary files /dev/null and b/src/www/static/img/continued-left.png differ diff --git a/src/www/static/img/continued-right.png b/src/www/static/img/continued-right.png new file mode 100644 index 0000000..d50233f Binary files /dev/null and b/src/www/static/img/continued-right.png differ diff --git a/src/www/static/img/desktop-big.png b/src/www/static/img/desktop-big.png new file mode 100644 index 0000000..d9dc492 Binary files /dev/null and b/src/www/static/img/desktop-big.png differ diff --git a/src/www/static/img/dialog-error-32.png b/src/www/static/img/dialog-error-32.png new file mode 100644 index 0000000..e71bba3 Binary files /dev/null and b/src/www/static/img/dialog-error-32.png differ diff --git a/src/www/static/img/dialog-information-32.png b/src/www/static/img/dialog-information-32.png new file mode 100644 index 0000000..c3c07e1 Binary files /dev/null and b/src/www/static/img/dialog-information-32.png differ diff --git a/src/www/static/img/dialog-information.gif b/src/www/static/img/dialog-information.gif new file mode 100644 index 0000000..093ae4a Binary files /dev/null and b/src/www/static/img/dialog-information.gif differ diff --git a/src/www/static/img/dialog-information.png b/src/www/static/img/dialog-information.png new file mode 100644 index 0000000..8eeebdd Binary files /dev/null and b/src/www/static/img/dialog-information.png differ diff --git a/src/www/static/img/dialog-warning.png b/src/www/static/img/dialog-warning.png new file mode 100644 index 0000000..06148e8 Binary files /dev/null and b/src/www/static/img/dialog-warning.png differ diff --git a/src/www/static/img/document-delete.png b/src/www/static/img/document-delete.png new file mode 100644 index 0000000..184f762 Binary files /dev/null and b/src/www/static/img/document-delete.png differ diff --git a/src/www/static/img/document-properties.png b/src/www/static/img/document-properties.png new file mode 100644 index 0000000..0f27c8b Binary files /dev/null and b/src/www/static/img/document-properties.png differ diff --git a/src/www/static/img/dragonfly-favicon.png b/src/www/static/img/dragonfly-favicon.png new file mode 100644 index 0000000..72c9465 Binary files /dev/null and b/src/www/static/img/dragonfly-favicon.png differ diff --git a/src/www/static/img/dragonfly.png b/src/www/static/img/dragonfly.png new file mode 100644 index 0000000..8c81499 Binary files /dev/null and b/src/www/static/img/dragonfly.png differ diff --git a/src/www/static/img/emblem-readonly.gif b/src/www/static/img/emblem-readonly.gif new file mode 100644 index 0000000..c42067a Binary files /dev/null and b/src/www/static/img/emblem-readonly.gif differ diff --git a/src/www/static/img/emblem-readonly.png b/src/www/static/img/emblem-readonly.png new file mode 100644 index 0000000..9319cc2 Binary files /dev/null and b/src/www/static/img/emblem-readonly.png differ diff --git a/src/www/static/img/event-16.png b/src/www/static/img/event-16.png new file mode 100644 index 0000000..28de8fe Binary files /dev/null and b/src/www/static/img/event-16.png differ diff --git a/src/www/static/img/form_buttons.gif b/src/www/static/img/form_buttons.gif new file mode 100644 index 0000000..ea1cfd8 Binary files /dev/null and b/src/www/static/img/form_buttons.gif differ diff --git a/src/www/static/img/form_buttons.png b/src/www/static/img/form_buttons.png new file mode 100644 index 0000000..21c6f57 Binary files /dev/null and b/src/www/static/img/form_buttons.png differ diff --git a/src/www/static/img/go-next.png b/src/www/static/img/go-next.png new file mode 100644 index 0000000..f722066 Binary files /dev/null and b/src/www/static/img/go-next.png differ diff --git a/src/www/static/img/go-previous.png b/src/www/static/img/go-previous.png new file mode 100644 index 0000000..c7367ed Binary files /dev/null and b/src/www/static/img/go-previous.png differ diff --git a/src/www/static/img/grad-cal-title.png b/src/www/static/img/grad-cal-title.png new file mode 100644 index 0000000..25ebb71 Binary files /dev/null and b/src/www/static/img/grad-cal-title.png differ diff --git a/src/www/static/img/grad-cal.png b/src/www/static/img/grad-cal.png new file mode 100644 index 0000000..c85976c Binary files /dev/null and b/src/www/static/img/grad-cal.png differ diff --git a/src/www/static/img/grad-page.png b/src/www/static/img/grad-page.png new file mode 100644 index 0000000..728cd4c Binary files /dev/null and b/src/www/static/img/grad-page.png differ diff --git a/src/www/static/img/grad-tab.png b/src/www/static/img/grad-tab.png new file mode 100644 index 0000000..3b9da18 Binary files /dev/null and b/src/www/static/img/grad-tab.png differ diff --git a/src/www/static/img/grad-textarea.png b/src/www/static/img/grad-textarea.png new file mode 100644 index 0000000..6b73103 Binary files /dev/null and b/src/www/static/img/grad-textarea.png differ diff --git a/src/www/static/img/grad-toolbar.png b/src/www/static/img/grad-toolbar.png new file mode 100644 index 0000000..34e24b1 Binary files /dev/null and b/src/www/static/img/grad-toolbar.png differ diff --git a/src/www/static/img/grad.png b/src/www/static/img/grad.png new file mode 100644 index 0000000..2184d7a Binary files /dev/null and b/src/www/static/img/grad.png differ diff --git a/src/www/static/img/grippy-h.gif b/src/www/static/img/grippy-h.gif new file mode 100644 index 0000000..6f708c6 Binary files /dev/null and b/src/www/static/img/grippy-h.gif differ diff --git a/src/www/static/img/grippy-h.png b/src/www/static/img/grippy-h.png new file mode 100644 index 0000000..7cba396 Binary files /dev/null and b/src/www/static/img/grippy-h.png differ diff --git a/src/www/static/img/grippy-v.gif b/src/www/static/img/grippy-v.gif new file mode 100644 index 0000000..7797ff6 Binary files /dev/null and b/src/www/static/img/grippy-v.gif differ diff --git a/src/www/static/img/grippy-v.png b/src/www/static/img/grippy-v.png new file mode 100644 index 0000000..7978bc2 Binary files /dev/null and b/src/www/static/img/grippy-v.png differ diff --git a/src/www/static/img/grippy.png b/src/www/static/img/grippy.png new file mode 100644 index 0000000..3dbae7d Binary files /dev/null and b/src/www/static/img/grippy.png differ diff --git a/src/www/static/img/help-browser.png b/src/www/static/img/help-browser.png new file mode 100644 index 0000000..3cb912e Binary files /dev/null and b/src/www/static/img/help-browser.png differ diff --git a/src/www/static/img/histogram.png b/src/www/static/img/histogram.png new file mode 100644 index 0000000..72826e8 Binary files /dev/null and b/src/www/static/img/histogram.png differ diff --git a/src/www/static/img/home-16.gif b/src/www/static/img/home-16.gif new file mode 100644 index 0000000..28eadcb Binary files /dev/null and b/src/www/static/img/home-16.gif differ diff --git a/src/www/static/img/home-16.png b/src/www/static/img/home-16.png new file mode 100644 index 0000000..4e5742e Binary files /dev/null and b/src/www/static/img/home-16.png differ diff --git a/src/www/static/img/icon-simple-calendars.gif b/src/www/static/img/icon-simple-calendars.gif new file mode 100644 index 0000000..4440d01 Binary files /dev/null and b/src/www/static/img/icon-simple-calendars.gif differ diff --git a/src/www/static/img/icon-simple-calendars.png b/src/www/static/img/icon-simple-calendars.png new file mode 100644 index 0000000..39fe005 Binary files /dev/null and b/src/www/static/img/icon-simple-calendars.png differ diff --git a/src/www/static/img/icon-simple-contacts.gif b/src/www/static/img/icon-simple-contacts.gif new file mode 100644 index 0000000..1e1ddb5 Binary files /dev/null and b/src/www/static/img/icon-simple-contacts.gif differ diff --git a/src/www/static/img/icon-simple-contacts.png b/src/www/static/img/icon-simple-contacts.png new file mode 100644 index 0000000..b7be5df Binary files /dev/null and b/src/www/static/img/icon-simple-contacts.png differ diff --git a/src/www/static/img/icon-simple-todos.gif b/src/www/static/img/icon-simple-todos.gif new file mode 100644 index 0000000..dbfe3e9 Binary files /dev/null and b/src/www/static/img/icon-simple-todos.gif differ diff --git a/src/www/static/img/icon-simple-todos.png b/src/www/static/img/icon-simple-todos.png new file mode 100644 index 0000000..ca40bcb Binary files /dev/null and b/src/www/static/img/icon-simple-todos.png differ diff --git a/src/www/static/img/image-x-generic.png b/src/www/static/img/image-x-generic.png new file mode 100644 index 0000000..247ccad Binary files /dev/null and b/src/www/static/img/image-x-generic.png differ diff --git a/src/www/static/img/info-bl.gif b/src/www/static/img/info-bl.gif new file mode 100644 index 0000000..01c89e9 Binary files /dev/null and b/src/www/static/img/info-bl.gif differ diff --git a/src/www/static/img/info-bl.png b/src/www/static/img/info-bl.png new file mode 100644 index 0000000..0aa0338 Binary files /dev/null and b/src/www/static/img/info-bl.png differ diff --git a/src/www/static/img/info-br.gif b/src/www/static/img/info-br.gif new file mode 100644 index 0000000..90baead Binary files /dev/null and b/src/www/static/img/info-br.gif differ diff --git a/src/www/static/img/info-br.png b/src/www/static/img/info-br.png new file mode 100644 index 0000000..1698287 Binary files /dev/null and b/src/www/static/img/info-br.png differ diff --git a/src/www/static/img/info-cl.gif b/src/www/static/img/info-cl.gif new file mode 100644 index 0000000..f868b54 Binary files /dev/null and b/src/www/static/img/info-cl.gif differ diff --git a/src/www/static/img/info-cl.png b/src/www/static/img/info-cl.png new file mode 100644 index 0000000..cebf055 Binary files /dev/null and b/src/www/static/img/info-cl.png differ diff --git a/src/www/static/img/info-cr.gif b/src/www/static/img/info-cr.gif new file mode 100644 index 0000000..e7c1efd Binary files /dev/null and b/src/www/static/img/info-cr.gif differ diff --git a/src/www/static/img/info-cr.png b/src/www/static/img/info-cr.png new file mode 100644 index 0000000..4499a9a Binary files /dev/null and b/src/www/static/img/info-cr.png differ diff --git a/src/www/static/img/info-pointer-down.gif b/src/www/static/img/info-pointer-down.gif new file mode 100644 index 0000000..fc3176c Binary files /dev/null and b/src/www/static/img/info-pointer-down.gif differ diff --git a/src/www/static/img/info-pointer-down.png b/src/www/static/img/info-pointer-down.png new file mode 100644 index 0000000..a4efe5d Binary files /dev/null and b/src/www/static/img/info-pointer-down.png differ diff --git a/src/www/static/img/info-pointer-left.gif b/src/www/static/img/info-pointer-left.gif new file mode 100644 index 0000000..44df30b Binary files /dev/null and b/src/www/static/img/info-pointer-left.gif differ diff --git a/src/www/static/img/info-pointer-left.png b/src/www/static/img/info-pointer-left.png new file mode 100644 index 0000000..3feaeab Binary files /dev/null and b/src/www/static/img/info-pointer-left.png differ diff --git a/src/www/static/img/info-pointer-right.gif b/src/www/static/img/info-pointer-right.gif new file mode 100644 index 0000000..0e1e3cf Binary files /dev/null and b/src/www/static/img/info-pointer-right.gif differ diff --git a/src/www/static/img/info-pointer-right.png b/src/www/static/img/info-pointer-right.png new file mode 100644 index 0000000..d1a30a4 Binary files /dev/null and b/src/www/static/img/info-pointer-right.png differ diff --git a/src/www/static/img/info-pointer-up.gif b/src/www/static/img/info-pointer-up.gif new file mode 100644 index 0000000..7fa6e72 Binary files /dev/null and b/src/www/static/img/info-pointer-up.gif differ diff --git a/src/www/static/img/info-pointer-up.png b/src/www/static/img/info-pointer-up.png new file mode 100644 index 0000000..8f54782 Binary files /dev/null and b/src/www/static/img/info-pointer-up.png differ diff --git a/src/www/static/img/info-tl.gif b/src/www/static/img/info-tl.gif new file mode 100644 index 0000000..4559092 Binary files /dev/null and b/src/www/static/img/info-tl.gif differ diff --git a/src/www/static/img/info-tl.png b/src/www/static/img/info-tl.png new file mode 100644 index 0000000..512d021 Binary files /dev/null and b/src/www/static/img/info-tl.png differ diff --git a/src/www/static/img/info-tr.gif b/src/www/static/img/info-tr.gif new file mode 100644 index 0000000..67db3f1 Binary files /dev/null and b/src/www/static/img/info-tr.gif differ diff --git a/src/www/static/img/info-tr.png b/src/www/static/img/info-tr.png new file mode 100644 index 0000000..4b882d4 Binary files /dev/null and b/src/www/static/img/info-tr.png differ diff --git a/src/www/static/img/lightbulb.png b/src/www/static/img/lightbulb.png new file mode 100644 index 0000000..0c54f52 Binary files /dev/null and b/src/www/static/img/lightbulb.png differ diff --git a/src/www/static/img/link-underline-view-active.gif b/src/www/static/img/link-underline-view-active.gif new file mode 100644 index 0000000..43eef64 Binary files /dev/null and b/src/www/static/img/link-underline-view-active.gif differ diff --git a/src/www/static/img/link-underline-view.gif b/src/www/static/img/link-underline-view.gif new file mode 100644 index 0000000..8b847c1 Binary files /dev/null and b/src/www/static/img/link-underline-view.gif differ diff --git a/src/www/static/img/list-add.png b/src/www/static/img/list-add.png new file mode 100644 index 0000000..c541e6e Binary files /dev/null and b/src/www/static/img/list-add.png differ diff --git a/src/www/static/img/list-remove.png b/src/www/static/img/list-remove.png new file mode 100644 index 0000000..6b8361e Binary files /dev/null and b/src/www/static/img/list-remove.png differ diff --git a/src/www/static/img/locale.png b/src/www/static/img/locale.png new file mode 100644 index 0000000..20f4363 Binary files /dev/null and b/src/www/static/img/locale.png differ diff --git a/src/www/static/img/locked.png b/src/www/static/img/locked.png new file mode 100644 index 0000000..4009def Binary files /dev/null and b/src/www/static/img/locked.png differ diff --git a/src/www/static/img/login-bg-bottom.png b/src/www/static/img/login-bg-bottom.png new file mode 100644 index 0000000..23a3c5a Binary files /dev/null and b/src/www/static/img/login-bg-bottom.png differ diff --git a/src/www/static/img/login-bg-middle.png b/src/www/static/img/login-bg-middle.png new file mode 100644 index 0000000..c4b939c Binary files /dev/null and b/src/www/static/img/login-bg-middle.png differ diff --git a/src/www/static/img/login-bg-top.png b/src/www/static/img/login-bg-top.png new file mode 100644 index 0000000..2cf97bd Binary files /dev/null and b/src/www/static/img/login-bg-top.png differ diff --git a/src/www/static/img/login-logo.gif b/src/www/static/img/login-logo.gif new file mode 100644 index 0000000..3917b04 Binary files /dev/null and b/src/www/static/img/login-logo.gif differ diff --git a/src/www/static/img/login-logo.png b/src/www/static/img/login-logo.png new file mode 100644 index 0000000..919e6d0 Binary files /dev/null and b/src/www/static/img/login-logo.png differ diff --git a/src/www/static/img/login-pane.png b/src/www/static/img/login-pane.png new file mode 100644 index 0000000..28b28d7 Binary files /dev/null and b/src/www/static/img/login-pane.png differ diff --git a/src/www/static/img/login-pane.xcf b/src/www/static/img/login-pane.xcf new file mode 100644 index 0000000..c95d52c Binary files /dev/null and b/src/www/static/img/login-pane.xcf differ diff --git a/src/www/static/img/mail-closed-16.png b/src/www/static/img/mail-closed-16.png new file mode 100644 index 0000000..f68f416 Binary files /dev/null and b/src/www/static/img/mail-closed-16.png differ diff --git a/src/www/static/img/mail-forward-16.png b/src/www/static/img/mail-forward-16.png new file mode 100644 index 0000000..c95ba24 Binary files /dev/null and b/src/www/static/img/mail-forward-16.png differ diff --git a/src/www/static/img/mail-inbox-16.png b/src/www/static/img/mail-inbox-16.png new file mode 100644 index 0000000..274d2c3 Binary files /dev/null and b/src/www/static/img/mail-inbox-16.png differ diff --git a/src/www/static/img/mail-inbox-22.png b/src/www/static/img/mail-inbox-22.png new file mode 100644 index 0000000..e3a6fcf Binary files /dev/null and b/src/www/static/img/mail-inbox-22.png differ diff --git a/src/www/static/img/mail-reply-all-16.png b/src/www/static/img/mail-reply-all-16.png new file mode 100644 index 0000000..768be24 Binary files /dev/null and b/src/www/static/img/mail-reply-all-16.png differ diff --git a/src/www/static/img/mail-reply-sender-16.png b/src/www/static/img/mail-reply-sender-16.png new file mode 100644 index 0000000..240c60e Binary files /dev/null and b/src/www/static/img/mail-reply-sender-16.png differ diff --git a/src/www/static/img/message-16.gif b/src/www/static/img/message-16.gif new file mode 100644 index 0000000..781cf62 Binary files /dev/null and b/src/www/static/img/message-16.gif differ diff --git a/src/www/static/img/message-16.png b/src/www/static/img/message-16.png new file mode 100644 index 0000000..e4b653e Binary files /dev/null and b/src/www/static/img/message-16.png differ diff --git a/src/www/static/img/message-closed-16.png b/src/www/static/img/message-closed-16.png new file mode 100644 index 0000000..cfd5021 Binary files /dev/null and b/src/www/static/img/message-closed-16.png differ diff --git a/src/www/static/img/minus.png b/src/www/static/img/minus.png new file mode 100644 index 0000000..2724a51 Binary files /dev/null and b/src/www/static/img/minus.png differ diff --git a/src/www/static/img/msg-gradient.png b/src/www/static/img/msg-gradient.png new file mode 100644 index 0000000..55b31ab Binary files /dev/null and b/src/www/static/img/msg-gradient.png differ diff --git a/src/www/static/img/navselhilight.png b/src/www/static/img/navselhilight.png new file mode 100644 index 0000000..d5f21d2 Binary files /dev/null and b/src/www/static/img/navselhilight.png differ diff --git a/src/www/static/img/network-server.png b/src/www/static/img/network-server.png new file mode 100644 index 0000000..4f863f9 Binary files /dev/null and b/src/www/static/img/network-server.png differ diff --git a/src/www/static/img/open-read.png b/src/www/static/img/open-read.png new file mode 100644 index 0000000..857dce9 Binary files /dev/null and b/src/www/static/img/open-read.png differ diff --git a/src/www/static/img/open-unread.png b/src/www/static/img/open-unread.png new file mode 100644 index 0000000..0490dbb Binary files /dev/null and b/src/www/static/img/open-unread.png differ diff --git a/src/www/static/img/plus.png b/src/www/static/img/plus.png new file mode 100644 index 0000000..a4a5149 Binary files /dev/null and b/src/www/static/img/plus.png differ diff --git a/src/www/static/img/popup.png b/src/www/static/img/popup.png new file mode 100644 index 0000000..8ad1449 Binary files /dev/null and b/src/www/static/img/popup.png differ diff --git a/src/www/static/img/read-gradient.png b/src/www/static/img/read-gradient.png new file mode 100644 index 0000000..29f57d4 Binary files /dev/null and b/src/www/static/img/read-gradient.png differ diff --git a/src/www/static/img/sad.png b/src/www/static/img/sad.png new file mode 100644 index 0000000..9eeca4f Binary files /dev/null and b/src/www/static/img/sad.png differ diff --git a/src/www/static/img/sbg.png b/src/www/static/img/sbg.png new file mode 100644 index 0000000..61cf43d Binary files /dev/null and b/src/www/static/img/sbg.png differ diff --git a/src/www/static/img/sel.png b/src/www/static/img/sel.png new file mode 100755 index 0000000..9596486 Binary files /dev/null and b/src/www/static/img/sel.png differ diff --git a/src/www/static/img/side-bl.png b/src/www/static/img/side-bl.png new file mode 100644 index 0000000..e7f85e7 Binary files /dev/null and b/src/www/static/img/side-bl.png differ diff --git a/src/www/static/img/side-br.png b/src/www/static/img/side-br.png new file mode 100644 index 0000000..1834fdf Binary files /dev/null and b/src/www/static/img/side-br.png differ diff --git a/src/www/static/img/side-cl.png b/src/www/static/img/side-cl.png new file mode 100644 index 0000000..d279050 Binary files /dev/null and b/src/www/static/img/side-cl.png differ diff --git a/src/www/static/img/side-cr.png b/src/www/static/img/side-cr.png new file mode 100644 index 0000000..c044344 Binary files /dev/null and b/src/www/static/img/side-cr.png differ diff --git a/src/www/static/img/side-tl.png b/src/www/static/img/side-tl.png new file mode 100644 index 0000000..40a3fa0 Binary files /dev/null and b/src/www/static/img/side-tl.png differ diff --git a/src/www/static/img/side-tr.png b/src/www/static/img/side-tr.png new file mode 100644 index 0000000..c7131b5 Binary files /dev/null and b/src/www/static/img/side-tr.png differ diff --git a/src/www/static/img/star-widget.gif b/src/www/static/img/star-widget.gif new file mode 100644 index 0000000..b9d139d Binary files /dev/null and b/src/www/static/img/star-widget.gif differ diff --git a/src/www/static/img/star-widget.png b/src/www/static/img/star-widget.png new file mode 100644 index 0000000..6dd9c68 Binary files /dev/null and b/src/www/static/img/star-widget.png differ diff --git a/src/www/static/img/star.png b/src/www/static/img/star.png new file mode 100644 index 0000000..119a81c Binary files /dev/null and b/src/www/static/img/star.png differ diff --git a/src/www/static/img/system-installer.png b/src/www/static/img/system-installer.png new file mode 100644 index 0000000..c26576e Binary files /dev/null and b/src/www/static/img/system-installer.png differ diff --git a/src/www/static/img/system-search.gif b/src/www/static/img/system-search.gif new file mode 100644 index 0000000..da1731b Binary files /dev/null and b/src/www/static/img/system-search.gif differ diff --git a/src/www/static/img/system-search.png b/src/www/static/img/system-search.png new file mode 100644 index 0000000..fd7f0b0 Binary files /dev/null and b/src/www/static/img/system-search.png differ diff --git a/src/www/static/img/system-users.png b/src/www/static/img/system-users.png new file mode 100644 index 0000000..749c825 Binary files /dev/null and b/src/www/static/img/system-users.png differ diff --git a/src/www/static/img/text-x-generic.png b/src/www/static/img/text-x-generic.png new file mode 100644 index 0000000..2d7f2d6 Binary files /dev/null and b/src/www/static/img/text-x-generic.png differ diff --git a/src/www/static/img/top-gradient.png b/src/www/static/img/top-gradient.png new file mode 100644 index 0000000..8779a87 Binary files /dev/null and b/src/www/static/img/top-gradient.png differ diff --git a/src/www/static/img/unread-gradient.png b/src/www/static/img/unread-gradient.png new file mode 100644 index 0000000..7c57c31 Binary files /dev/null and b/src/www/static/img/unread-gradient.png differ diff --git a/src/www/static/img/user-desktop.png b/src/www/static/img/user-desktop.png new file mode 100644 index 0000000..da09d12 Binary files /dev/null and b/src/www/static/img/user-desktop.png differ diff --git a/src/www/static/img/user.png b/src/www/static/img/user.png new file mode 100644 index 0000000..ed35fe0 Binary files /dev/null and b/src/www/static/img/user.png differ diff --git a/src/www/static/img/x.png b/src/www/static/img/x.png new file mode 100644 index 0000000..7ed40ce Binary files /dev/null and b/src/www/static/img/x.png differ diff --git a/src/www/static/index.html b/src/www/static/index.html new file mode 100644 index 0000000..4b634e0 --- /dev/null +++ b/src/www/static/index.html @@ -0,0 +1,106 @@ + + + + + + Bongo + + + + + + + + + +
+ + + +
+ + + diff --git a/src/www/static/login-bg-bottom.png b/src/www/static/login-bg-bottom.png new file mode 100644 index 0000000..23a3c5a Binary files /dev/null and b/src/www/static/login-bg-bottom.png differ diff --git a/src/www/static/login-bg-middle.png b/src/www/static/login-bg-middle.png new file mode 100644 index 0000000..c4b939c Binary files /dev/null and b/src/www/static/login-bg-middle.png differ diff --git a/src/www/static/login-bg-top.png b/src/www/static/login-bg-top.png new file mode 100644 index 0000000..2cf97bd Binary files /dev/null and b/src/www/static/login-bg-top.png differ diff --git a/src/www/static/login-logo.png b/src/www/static/login-logo.png new file mode 100644 index 0000000..919e6d0 Binary files /dev/null and b/src/www/static/login-logo.png differ diff --git a/src/www/static/mail-inbox-22.png b/src/www/static/mail-inbox-22.png new file mode 100644 index 0000000..e3a6fcf Binary files /dev/null and b/src/www/static/mail-inbox-22.png differ diff --git a/src/www/static/top-gradient.png b/src/www/static/top-gradient.png new file mode 100644 index 0000000..8779a87 Binary files /dev/null and b/src/www/static/top-gradient.png differ diff --git a/src/www/tests/test_calendar_store.py b/src/www/tests/test_calendar_store.py new file mode 100644 index 0000000..36d5706 --- /dev/null +++ b/src/www/tests/test_calendar_store.py @@ -0,0 +1,122 @@ +import unittest + +from bongo_web.calendar_store import (CalendarNotFound, CalendarPathError, + PreconditionFailed, delete_event, + etag_matches, event_href, ical_to_json, + list_events, make_etag, parse_calendar_path, + write_event) + + +class FakeStore: + def __init__(self, documents=None): + self.documents = dict(documents or {}) + self.closed = False + + def Info(self, path): + return object() if path in self.documents else None + + def Read(self, path): + return self.documents[path] + + def Replace(self, path, document): + self.documents[path] = document + + def Write(self, collection, doc_type, document, filename=None, link=None): + self.documents["/events/" + filename] = document + + def Delete(self, path): + del self.documents[path] + + def Quit(self): + self.closed = True + + def Events(self, calendar, props, start=None, end=None): + return [] + + +class CalendarPathTests(unittest.TestCase): + user = "mario@example.test" + + def test_personal_event(self): + result = parse_calendar_path( + "/calendars/mario@example.test/personal/event-1.ics", self.user) + self.assertEqual(result["event"], "event-1.ics") + self.assertEqual(result["store_path"], "/events/event-1") + + def test_other_user_is_forbidden(self): + with self.assertRaises(PermissionError): + parse_calendar_path( + "/calendars/other@example.test/personal/event.ics", self.user) + + def test_unknown_calendar(self): + with self.assertRaises(CalendarNotFound): + parse_calendar_path( + "/calendars/mario@example.test/work/event.ics", self.user) + + def test_path_tricks_are_rejected(self): + for event in ("bad%2Fevent.ics", "bad event.ics", ".ics"): + with self.subTest(event=event), self.assertRaises(CalendarPathError): + parse_calendar_path( + f"/calendars/mario@example.test/personal/{event}", self.user) + + def test_event_href_is_encoded(self): + self.assertEqual(event_href(self.user, "event~1.ics"), + "/calendars/mario@example.test/personal/event~1.ics") + + def test_calendar_shape_checked_before_native_converter(self): + for data in (b"not a calendar", + b"BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n", + b"BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nUID:x\r\n" + b"END:VTODO\r\nEND:VCALENDAR\r\n"): + with self.subTest(data=data), self.assertRaises(ValueError): + ical_to_json(data) + + +class CalendarMutationTests(unittest.TestCase): + def factory(self, store): + return lambda user, cookie: store + + def test_create_event(self): + store = FakeStore() + created, etag = write_event( + "u", "c", "one.ics", b"ical", if_none_match="*", + client_factory=self.factory(store), to_json=lambda value: "json", + to_ical=lambda value: b"canonical") + self.assertTrue(created) + self.assertEqual(etag, make_etag(b"canonical")) + self.assertEqual(store.documents["/events/one"], "json") + self.assertTrue(store.closed) + + def test_if_none_match_rejects_overwrite(self): + store = FakeStore({"/events/one": "old"}) + with self.assertRaises(PreconditionFailed): + write_event("u", "c", "one.ics", b"ical", if_none_match="*", + client_factory=self.factory(store), + to_json=lambda value: "new", + to_ical=lambda value: b"old canonical") + + def test_if_match_controls_update_and_delete(self): + store = FakeStore({"/events/one": "old"}) + current = make_etag(b"old canonical") + self.assertTrue(etag_matches(current, current)) + created, unused = write_event( + "u", "c", "one.ics", b"ical", if_match=current, + client_factory=self.factory(store), to_json=lambda value: "new", + to_ical=lambda value: b"old canonical" if value == "old" + else b"new canonical") + self.assertFalse(created) + delete_event("u", "c", "one.ics", make_etag(b"new canonical"), + client_factory=self.factory(store), + to_ical=lambda value: b"new canonical") + self.assertNotIn("/events/one", store.documents) + + def test_empty_calendar_list(self): + store = FakeStore({"/calendars/personal": "calendar"}) + calendar = type("Calendar", (), {"uid": "123"})() + store.Info = lambda path: calendar + self.assertEqual(list_events("u", "c", + client_factory=self.factory(store)), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_card_store.py b/src/www/tests/test_card_store.py new file mode 100644 index 0000000..425001c --- /dev/null +++ b/src/www/tests/test_card_store.py @@ -0,0 +1,98 @@ +import unittest + +from bongo_web.card_store import (CardNotFound, CardPathError, + CardPreconditionFailed, card_href, + delete_card, make_etag, parse_card_path, + vcard_to_json, write_card) + + +class FakeStore: + def __init__(self, documents=None): + self.documents = dict(documents or {}) + + def Info(self, path): + return object() if path in self.documents else None + + def Read(self, path): + return self.documents[path] + + def Replace(self, path, document): + self.documents[path] = document + + def Write(self, collection, doc_type, document, filename=None): + self.documents[collection + "/" + filename] = document + + def Delete(self, path): + del self.documents[path] + + def Quit(self): + pass + + +class CardPathTests(unittest.TestCase): + user = "mario@example.test" + + def test_personal_card(self): + result = parse_card_path( + "/addressbooks/mario@example.test/personal/contact-1.vcf", self.user) + self.assertEqual(result["card"], "contact-1.vcf") + self.assertEqual(result["store_path"], + "/addressbook/personal/contact-1") + + def test_other_user_is_forbidden(self): + with self.assertRaises(PermissionError): + parse_card_path( + "/addressbooks/other@example.test/personal/a.vcf", self.user) + + def test_unknown_addressbook(self): + with self.assertRaises(CardNotFound): + parse_card_path( + "/addressbooks/mario@example.test/work/a.vcf", self.user) + + def test_bad_card_names(self): + for card in ("bad%2Fcard.vcf", "bad card.vcf", ".vcf"): + with self.subTest(card=card), self.assertRaises(CardPathError): + parse_card_path( + f"/addressbooks/{self.user}/personal/{card}", self.user) + + def test_href(self): + self.assertEqual(card_href(self.user, "contact~1.vcf"), + "/addressbooks/mario@example.test/personal/contact~1.vcf") + + def test_shape_is_rejected_before_vobject_import(self): + for data in (b"garbage", b"BEGIN:VCARD\r\nEND:VCARD\r\n"): + with self.assertRaises(ValueError): + vcard_to_json(data) + + +class CardMutationTests(unittest.TestCase): + def factory(self, store): + return lambda user, cookie: store + + def test_create_card(self): + store = FakeStore() + created, etag = write_card( + "u", "c", "one.vcf", b"vcard", if_none_match="*", + client_factory=self.factory(store), to_json=lambda value: "json", + to_vcard=lambda value, uid: b"canonical") + self.assertTrue(created) + self.assertEqual(etag, make_etag(b"canonical")) + self.assertIn("/addressbook/personal/one", store.documents) + + def test_preconditions_and_delete(self): + path = "/addressbook/personal/one" + store = FakeStore({path: "old"}) + current = make_etag(b"old card") + with self.assertRaises(CardPreconditionFailed): + write_card("u", "c", "one.vcf", b"new", if_match='"wrong"', + client_factory=self.factory(store), + to_json=lambda value: "new", + to_vcard=lambda value, uid: b"old card") + delete_card("u", "c", "one.vcf", current, + client_factory=self.factory(store), + converter=lambda value, uid: b"old card") + self.assertNotIn(path, store.documents) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_compose.py b/src/www/tests/test_compose.py new file mode 100644 index 0000000..2a4df88 --- /dev/null +++ b/src/www/tests/test_compose.py @@ -0,0 +1,83 @@ +import unittest + +from bongo_web.compose import build_message, send_message +from bongo.store.CommandStream import CommandStream + + +class FakeQueue: + def __init__(self, _user): + self.calls = [] + + def __getattr__(self, name): + return lambda *values: self.calls.append((name, *values)) + + +class FakeWire: + def __init__(self): + self.writes = [] + + def write(self, value): + if not isinstance(value, bytes): + raise TypeError("wire protocol must be bytes") + self.writes.append(value) + + def readline(self): + return b"1000 ready\r\n" + + def read(self, _length=-1): + return b"" + + def flush(self): + pass + + def close(self): + pass + + +class FakeSocket: + def __init__(self): + self.wire = FakeWire() + + def makefile(self, mode): + if mode != "rwb": + raise AssertionError("binary socket stream required") + return self.wire + + +class ComposeTests(unittest.TestCase): + def test_builds_unicode_message_without_bcc_header(self): + message, recipients = build_message( + "sender@example.test", + {"to": "Alice ", + "bcc": "hidden@example.test", "subject": "Grüße", "text": "Hallo"}) + self.assertEqual(recipients, ["alice@example.test", "hidden@example.test"]) + self.assertNotIn("Bcc", message) + self.assertIn("Grüße", str(message["Subject"])) + + def test_rejects_injection_and_missing_recipient(self): + with self.assertRaises(ValueError): + build_message("sender@example.test", {"to": "a@example.test\r\nBcc: x@y.test"}) + with self.assertRaises(ValueError): + build_message("sender@example.test", {"subject": "nobody"}) + + def test_uses_native_queue_sequence(self): + queues = [] + def factory(user): + queue = FakeQueue(user) + queues.append(queue) + return queue + send_message("sender@example.test", {"to": "to@example.test", "text": "Hi"}, factory) + names = [call[0] for call in queues[0].calls] + self.assertEqual(names, ["Create", "StoreFrom", "StoreTo", + "StoreMessage", "Run", "Quit"]) + + def test_python3_command_stream_uses_binary_protocol(self): + sock = FakeSocket() + stream = CommandStream(sock) + stream.Write("NOOP") + self.assertEqual(sock.wire.writes, [b"NOOP", b"\r\n"]) + self.assertEqual(stream.GetResponse().message, "ready") + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_dav.py b/src/www/tests/test_dav.py new file mode 100644 index 0000000..af34e97 --- /dev/null +++ b/src/www/tests/test_dav.py @@ -0,0 +1,85 @@ +import unittest +from xml.etree import ElementTree as ET + +from bongo_web.dav import (CALDAV, CARDDAV, DAV, card_multistatus, + event_multistatus, multistatus, parse_card_report, + parse_report, qname, requested_properties) + + +class DavDiscoveryTests(unittest.TestCase): + user = "mario@example.test" + home = "/calendars/mario@example.test/" + + def test_depth_one_lists_personal_calendar(self): + root = ET.fromstring(multistatus(self.home, self.user, 1)) + responses = root.findall(f"{{{DAV}}}response") + self.assertEqual(len(responses), 2) + hrefs = [item.find(f"{{{DAV}}}href").text for item in responses] + self.assertIn(self.home + "personal/", hrefs) + + def test_calendar_root_points_to_authenticated_principal(self): + output = multistatus("/calendars/", self.user, 0) + self.assertIn(b"current-user-principal", output) + self.assertIn(b"/principals/mario@example.test/", output) + + def test_requested_missing_property_has_404_propstat(self): + body = (b'' + b'' + b'') + requested = requested_properties(body) + output = multistatus(self.home, self.user, 0, requested) + self.assertIn(b"200 OK", output) + self.assertIn(b"404 Not Found", output) + + def test_rejects_non_propfind_xml(self): + with self.assertRaises(ValueError): + requested_properties(b'') + + def test_unknown_resource_is_not_found(self): + self.assertIsNone(multistatus("/not-found/", self.user, 0)) + + def test_calendar_query_time_range(self): + report = parse_report( + b'' + b'' + b'' + b'') + self.assertEqual(report["kind"], "query") + self.assertEqual(report["start"].year, 2026) + self.assertIn(qname(CALDAV, "calendar-data"), report["properties"]) + + def test_multiget_reports_missing_href(self): + report = parse_report( + b'' + b'/calendars/u/personal/missing.ics' + b'') + output = event_multistatus([], report["properties"], report["hrefs"]) + self.assertIn(b"404 Not Found", output) + + def test_addressbook_multiget(self): + href = "/addressbooks/mario@example.test/personal/person.vcf" + report = parse_card_report( + ('' + f'{href}').encode()) + output = card_multistatus( + [{"href": href, "etag": '"etag"', + "data": b"BEGIN:VCARD\r\nEND:VCARD\r\n"}], + report["properties"], report["hrefs"]) + root = ET.fromstring(output) + self.assertIsNotNone(root.find(f".//{qname(CARDDAV, 'address-data')}")) + self.assertIn(b"BEGIN:VCARD", output) + + def test_addressbook_query(self): + report = parse_card_report( + b'' + b'') + self.assertEqual(report["kind"], "query") + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_filters.py b/src/www/tests/test_filters.py new file mode 100644 index 0000000..0eb095c --- /dev/null +++ b/src/www/tests/test_filters.py @@ -0,0 +1,41 @@ +import unittest + +from bongo_web.filters import compile_filter, validate_filter + + +class FilterTests(unittest.TestCase): + def test_family_group_moves_to_family_folder(self): + value = {"match": "any", + "conditions": [{"type": "contact_group", "value": "Family"}], + "actions": [{"type": "move", "folder": "/mail/Family"}, + {"type": "stop"}]} + result = compile_filter(value, 42, + lambda name: ["mother@example.test", + "father@example.test"]) + self.assertTrue(result.startswith("0000002AAB")) + self.assertIn("mother@example.test", result) + self.assertIn("father@example.test", result) + self.assertIn("E006FamilyZ000Z", result) + self.assertTrue(result.endswith("F000Z000Z")) + + def test_domain_and_list_id(self): + value = {"match": "all", + "conditions": [{"type": "sender_domain", + "value": "family.example"}, + {"type": "list_id", + "value": "news.family.example"}], + "actions": [{"type": "copy", "folder": "/mail/News"}]} + result = compile_filter(value, 1) + self.assertIn("B015@family.exampleZ000ZX", result) + self.assertIn("i019news.family.exampleZ000Z", result) + + def test_rejects_unsafe_folder_and_forwarding_address(self): + for action in ({"type": "move", "folder": "/mail/../_system"}, + {"type": "forward", "address": "x\r\nBcc:y"}): + with self.assertRaises(ValueError): + validate_filter({"conditions": [{"type": "mailing_list"}], + "actions": [action]}) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_html_mail.py b/src/www/tests/test_html_mail.py new file mode 100644 index 0000000..aa499d4 --- /dev/null +++ b/src/www/tests/test_html_mail.py @@ -0,0 +1,49 @@ +import unittest + +from bongo_web.html_mail import sanitize_html + + +def exercising_cleaner(html, **options): + """Exercise the URL callback without duplicating nh3's HTML parser.""" + callback = options["attribute_filter"] + values = [ + callback("img", "src", "cid:logo@example"), + callback("img", "src", "https://tracker.example/pixel.gif"), + callback("img", "src", "javascript:alert(1)"), + callback("a", "href", "https://example.test/"), + callback("a", "href", "javascript:alert(1)"), + ] + return "|".join(value or "REMOVED" for value in values) + + +class HtmlMailTests(unittest.TestCase): + def test_keeps_inline_and_marks_remote_images(self): + result = sanitize_html("

mail

", exercising_cleaner) + self.assertIn("cid:logo@example", result["html"]) + self.assertIn("bongo-remote:0", result["html"]) + self.assertIn("REMOVED", result["html"]) + self.assertEqual(result["remote_images"], + ["https://tracker.example/pixel.gif"]) + + def test_rejects_oversized_html(self): + with self.assertRaises(ValueError): + sanitize_html("x" * (4 * 1024 * 1024 + 1), exercising_cleaner) + + def test_real_sanitizer_removes_active_content_and_unsafe_css(self): + result = sanitize_html( + '

ok' + '' + '' + 'bad

') + output = result["html"] + self.assertNotIn("alert", output) + self.assertNotIn("position", output) + self.assertNotIn("background-image", output) + self.assertIn('src="cid:logo"', output) + self.assertIn('src="bongo-remote:0"', output) + self.assertEqual(result["remote_images"], + ["https://tracker.example/pixel"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_mail_store.py b/src/www/tests/test_mail_store.py new file mode 100644 index 0000000..3725f16 --- /dev/null +++ b/src/www/tests/test_mail_store.py @@ -0,0 +1,87 @@ +import unittest +from types import SimpleNamespace + +from bongo_web.mail_store import list_conversations, list_folders, read_conversation + + +class FakeStore: + def __init__(self): + self.closed = False + + def Collections(self, path): + self.assert_path = path + return [SimpleNamespace(name="/mail/Archive"), + SimpleNamespace(name="/mail/INBOX")] + + def Conversations(self, folder, props, start, end, showTotal): + item = SimpleNamespace(uid="abc", flags=2, props={ + "bongo.from": "Example User ", + "nmap.conversation.subject": "Hello", + "nmap.conversation.date": "20260715T120000Z", + "nmap.conversation.unread": "1", + "nmap.conversation.count": "2", + }) + result = iter([item]) + # StoreClient's iterator supplies total; a simple iterator exercises fallback. + return result + + def Conversation(self, uid, props=()): + return [SimpleNamespace(uid="b" * 32, flags=2, type=2)] + + def Read(self, uid): + return (b"From: Example User \r\n" + b"To: recipient@example.test\r\nSubject: Hello\r\n" + b"Content-Type: text/plain; charset=utf-8\r\n\r\nBody") + + def Quit(self): + self.closed = True + + +class MailStoreTests(unittest.TestCase): + def test_inbox_is_first(self): + store = FakeStore() + folders = list_folders("u", "c", lambda _u, _c: store) + self.assertEqual(folders[0]["path"], "/mail/INBOX") + self.assertTrue(store.closed) + + def test_conversation_values_are_structured(self): + stores = [] + def factory(_user, _cookie): + store = FakeStore() + stores.append(store) + return store + result = list_conversations("u", "c", "/mail/INBOX", 0, 30, factory) + message = result["conversations"][0] + self.assertEqual(message["subject"], "Hello") + self.assertEqual(message["from"][0]["address"], "user@example.test") + + def test_rejects_path_traversal_and_unbounded_page(self): + with self.assertRaises(ValueError): + list_conversations("u", "c", "/mail/../_system") + with self.assertRaises(ValueError): + list_conversations("u", "c", "/mail/INBOX", limit=101) + + def test_reads_plain_text_conversation(self): + store = FakeStore() + result = read_conversation("u", "c", "a" * 32, + lambda _u, _c: store) + self.assertEqual(result["messages"][0]["text"], "Body") + self.assertEqual(result["messages"][0]["subject"], "Hello") + + def test_extracts_canonical_mailing_list_id(self): + store = FakeStore() + store.Read = lambda _uid: (b"From: list@example.test\r\n" + b"List-ID: Example List \r\n" + b"List-Post: \r\n\r\nBody") + result = read_conversation("u", "c", "a" * 32, + lambda _u, _c: store) + self.assertEqual(result["messages"][0]["mailing_list"]["id"], + "list.example.test") + + def test_rejects_store_command_in_conversation_id(self): + with self.assertRaises(ValueError): + read_conversation("u", "c", "abc READ /_system") + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_tasks.py b/src/www/tests/test_tasks.py new file mode 100644 index 0000000..1b1e571 --- /dev/null +++ b/src/www/tests/test_tasks.py @@ -0,0 +1,38 @@ +import os +import tempfile +import unittest + +from bongo_web.tasks import TaskStore + + +class TaskStoreTests(unittest.TestCase): + def setUp(self): + handle, self.path = tempfile.mkstemp() + os.close(handle) + self.tasks = TaskStore(self.path) + + def tearDown(self): + os.unlink(self.path) + + def test_tasks_are_isolated_by_user(self): + self.tasks.upsert("alice", "mailing_list", "list.example", "New list") + self.assertEqual(len(self.tasks.list("alice")), 1) + self.assertEqual(self.tasks.list("bob"), []) + + def test_disabled_task_does_not_reappear(self): + self.tasks.upsert("alice", "mailing_list", "list.example", "New list") + task = self.tasks.list("alice")[0] + self.tasks.set_state("alice", task["id"], "disabled") + self.tasks.upsert("alice", "mailing_list", "list.example", "New list") + self.assertEqual(self.tasks.list("alice"), []) + + def test_critical_task_cannot_be_dismissed(self): + self.tasks.upsert("alice", "quarantine", "42", "Malware", + severity="critical", dismissible=False) + task = self.tasks.list("alice")[0] + with self.assertRaises(PermissionError): + self.tasks.set_state("alice", task["id"], "ignored") + + +if __name__ == "__main__": + unittest.main()