From 918ef6c2462e79e47fd636a1b8f0e32e89df75d0 Mon Sep 17 00:00:00 2001 From: Adrien Beudin Date: Thu, 18 Dec 2014 18:08:26 +0100 Subject: [PATCH] use Z-Push-contrib --- sources/README.md | 112 + sources/autodiscover/INSTALL | 208 ++ sources/autodiscover/autodiscover.php | 268 ++ sources/autodiscover/config.php | 89 + sources/autodiscover/response.xml | 19 + sources/backend/caldav/AUTHOR | 3 + sources/backend/caldav/REQUIREMENTS | 5 + sources/backend/caldav/caldav.php | 1466 ++++++++++ sources/backend/caldav/config.php | 67 + sources/backend/carddav/README | 12 + sources/backend/carddav/REQUIREMENTS | 5 + sources/backend/carddav/THANKS | 8 + sources/backend/carddav/carddav.php | 1452 ++++++++++ sources/backend/carddav/config.php | 109 + sources/backend/combined/combined.php | 294 ++- sources/backend/combined/config.php | 9 + sources/backend/combined/exporter.php | 2 +- sources/backend/combined/importer.php | 18 + sources/backend/imap/README | 28 + sources/backend/imap/REQUIREMENTS | 7 + sources/backend/imap/config.php | 104 +- sources/backend/imap/imap.php | 2346 ++++++++++++----- sources/backend/imap/mime_encode.php | 295 +++ sources/backend/ldap/AUTHOR | 3 + sources/backend/ldap/config.php | 60 + sources/backend/ldap/ldap.php | 584 ++++ sources/backend/maildir/maildir.php | 17 + sources/backend/searchldap/searchldap.php | 2 +- sources/backend/vcarddir/vcarddir.php | 16 + sources/backend/zarafa/importer.php | 15 + sources/backend/zarafa/mapi/mapidefs.php | 1 + sources/backend/zarafa/mapi/mapitags.php | 9 + sources/backend/zarafa/mapimapping.php | 2 +- sources/backend/zarafa/mapiprovider.php | 37 +- sources/backend/zarafa/zarafa.php | 45 +- sources/config.php | 63 + sources/include/Auth/SASL.php | 150 ++ sources/include/Auth/SASL/Anonymous.php | 71 + sources/include/Auth/SASL/Common.php | 129 + sources/include/Auth/SASL/CramMD5.php | 68 + sources/include/Auth/SASL/DigestMD5.php | 197 ++ sources/include/Auth/SASL/External.php | 63 + sources/include/Auth/SASL/Login.php | 65 + sources/include/Auth/SASL/Plain.php | 63 + sources/include/Auth/SASL/SCRAM.php | 305 +++ sources/include/Mail.php | 300 +++ sources/include/Mail/mail.php | 193 ++ sources/include/Mail/sendmail.php | 197 ++ sources/include/Mail/smtp.php | 490 ++++ sources/include/Net/SMTP.php | 1254 +++++++++ sources/include/Net/Socket.php | 716 +++++ sources/include/caldav-client-v2.php | 1113 ++++++++ sources/include/iCalendar.php | 1770 +++++++++++++ sources/include/mimeDecode.php | 130 +- sources/include/mimePart.php | 1270 +++++++++ sources/include/z_RFC822.php | 123 +- sources/include/z_RTF.php | 707 +++++ sources/include/z_caldav.php | 972 +++++++ sources/include/z_carddav.php | 892 +++++++ sources/index.php | 28 +- sources/lib/core/asdevice.php | 1 + sources/lib/core/changesmemorywrapper.php | 1 + sources/lib/core/devicemanager.php | 62 + sources/lib/core/interprocessdata.php | 2 +- sources/lib/core/streamimporter.php | 35 + sources/lib/core/synccollections.php | 9 +- sources/lib/core/zpush.php | 34 +- sources/lib/core/zpushdefs.php | 1 + sources/lib/default/backend.php | 11 + .../lib/default/diffbackend/diffbackend.php | 19 + sources/lib/default/diffbackend/diffstate.php | 37 +- .../default/diffbackend/exportchangesdiff.php | 4 + .../default/diffbackend/importchangesdiff.php | 30 + sources/lib/default/filestatemachine.php | 111 + sources/lib/default/sqlstatemachine.php | 781 ++++++ sources/lib/interface/ibackend.php | 10 + sources/lib/interface/iimportchanges.php | 13 + sources/lib/request/foldersync.php | 31 +- sources/lib/request/itemoperations.php | 247 +- sources/lib/request/request.php | 47 +- sources/lib/request/search.php | 36 +- sources/lib/request/sync.php | 14 +- sources/lib/syncobjects/syncappointment.php | 2 +- sources/lib/syncobjects/syncmail.php | 2 +- .../lib/syncobjects/syncmeetingrequest.php | 5 + sources/lib/utils/compat.php | 48 +- sources/lib/utils/timezoneutil.php | 38 +- sources/lib/utils/utils.php | 72 + sources/sql/mysql.sql | 19 + sources/testing/samples/meeting_request.txt | 51 + sources/testing/samples/messages/emoticon.txt | 28 + .../samples/messages/emoticon_base64.txt | 39 + .../samples/messages/emoticon_subject.txt | 20 + sources/testing/samples/messages/french.txt | 48 + sources/testing/samples/messages/m0001.txt | 31 + sources/testing/samples/messages/m0002.txt | 31 + sources/testing/samples/messages/m0003.txt | 30 + sources/testing/samples/messages/m0004.txt | 31 + sources/testing/samples/messages/m0005.txt | 31 + sources/testing/samples/messages/m0006.txt | 34 + sources/testing/samples/messages/m0007.txt | 32 + sources/testing/samples/messages/m0008.txt | 32 + sources/testing/samples/messages/m0009.txt | 27 + sources/testing/samples/messages/m0010.txt | 30 + sources/testing/samples/messages/m0011.txt | 135 + sources/testing/samples/messages/m0012.txt | 43 + sources/testing/samples/messages/m0013.txt | 83 + sources/testing/samples/messages/m0014.txt | 72 + sources/testing/samples/messages/m0015.txt | 144 + sources/testing/samples/messages/m0016.txt | 156 ++ sources/testing/samples/messages/m0017.txt | 188 ++ sources/testing/samples/messages/m0018.txt | 131 + sources/testing/samples/messages/m1001.txt | 32 + sources/testing/samples/messages/m1002.txt | 61 + sources/testing/samples/messages/m1003.txt | 137 + sources/testing/samples/messages/m1004.txt | 133 + sources/testing/samples/messages/m1005.txt | 212 ++ sources/testing/samples/messages/m1006.txt | 171 ++ sources/testing/samples/messages/m1007.txt | 17 + sources/testing/samples/messages/m1008.txt | 27 + sources/testing/samples/messages/m1009.txt | 111 + sources/testing/samples/messages/m1010.txt | 33 + sources/testing/samples/messages/m1011.txt | 26 + sources/testing/samples/messages/m1012.txt | 28 + sources/testing/samples/messages/m1013.txt | 51 + sources/testing/samples/messages/m1014.txt | 155 ++ sources/testing/samples/messages/m1015.txt | 60 + sources/testing/samples/messages/m1016.txt | 58 + sources/testing/samples/messages/m2001.txt | 29 + sources/testing/samples/messages/m2002.txt | 63 + sources/testing/samples/messages/m2003.txt | 36 + sources/testing/samples/messages/m2004.txt | 138 + sources/testing/samples/messages/m2005.txt | 198 ++ sources/testing/samples/messages/m2006.txt | 109 + sources/testing/samples/messages/m2007.txt | 171 ++ sources/testing/samples/messages/m2008.txt | 161 ++ sources/testing/samples/messages/m2009.txt | 109 + sources/testing/samples/messages/m2010.txt | 102 + sources/testing/samples/messages/m2011.txt | 95 + sources/testing/samples/messages/m2012.txt | 108 + sources/testing/samples/messages/m2013.txt | 84 + sources/testing/samples/messages/m2014.txt | 36 + sources/testing/samples/messages/m2015.txt | 21 + sources/testing/samples/messages/m2016.txt | 18 + sources/testing/samples/messages/m3001.txt | 96 + sources/testing/samples/messages/m3002.txt | 26 + sources/testing/samples/messages/m3003.txt | 57 + sources/testing/samples/messages/m3004.txt | 42 + sources/testing/samples/messages/m4000.txt | 629 +++++ sources/testing/samples/messages/smime001.txt | 40 + sources/testing/samples/messages/smime002.txt | 18 + sources/testing/samples/smime.txt | 63 + sources/testing/testing-caldav.php | 52 + sources/testing/testing-carddav.php | 65 + sources/testing/testing-forward.php | 223 ++ sources/testing/testing-imap.php | 10 + sources/testing/testing-imap_date.php | 53 + sources/testing/testing-imap_from.php | 135 + sources/testing/testing-imap_meeting.php | 67 + sources/testing/testing-imap_smtp.php | 38 + sources/testing/testing-mime-mail-parse.php | 63 + sources/testing/testing-mime-split.php | 24 + sources/testing/testing-mime.php | 184 ++ sources/testing/testing-mimetype.php | 40 + sources/testing/testing-preg_split.php | 12 + sources/testing/testing-ternary.php | 29 + .../tools/fix-meetings-2.0.8+2.1.0-ios7.py | 90 + sources/tools/migrate-2.0.x-2.1.0.php | 4 +- sources/version.php | 5 +- sources/z-push-top.php | 6 +- 170 files changed, 26073 insertions(+), 1003 deletions(-) create mode 100644 sources/README.md create mode 100644 sources/autodiscover/INSTALL create mode 100644 sources/autodiscover/autodiscover.php create mode 100644 sources/autodiscover/config.php create mode 100644 sources/autodiscover/response.xml create mode 100644 sources/backend/caldav/AUTHOR create mode 100644 sources/backend/caldav/REQUIREMENTS create mode 100644 sources/backend/caldav/caldav.php create mode 100644 sources/backend/caldav/config.php create mode 100644 sources/backend/carddav/README create mode 100644 sources/backend/carddav/REQUIREMENTS create mode 100644 sources/backend/carddav/THANKS create mode 100644 sources/backend/carddav/carddav.php create mode 100644 sources/backend/carddav/config.php create mode 100644 sources/backend/imap/README create mode 100644 sources/backend/imap/REQUIREMENTS create mode 100644 sources/backend/imap/mime_encode.php create mode 100644 sources/backend/ldap/AUTHOR create mode 100644 sources/backend/ldap/config.php create mode 100644 sources/backend/ldap/ldap.php create mode 100755 sources/include/Auth/SASL.php create mode 100755 sources/include/Auth/SASL/Anonymous.php create mode 100755 sources/include/Auth/SASL/Common.php create mode 100755 sources/include/Auth/SASL/CramMD5.php create mode 100755 sources/include/Auth/SASL/DigestMD5.php create mode 100644 sources/include/Auth/SASL/External.php create mode 100755 sources/include/Auth/SASL/Login.php create mode 100755 sources/include/Auth/SASL/Plain.php create mode 100644 sources/include/Auth/SASL/SCRAM.php create mode 100644 sources/include/Mail.php create mode 100644 sources/include/Mail/mail.php create mode 100644 sources/include/Mail/sendmail.php create mode 100644 sources/include/Mail/smtp.php create mode 100644 sources/include/Net/SMTP.php create mode 100644 sources/include/Net/Socket.php create mode 100644 sources/include/caldav-client-v2.php create mode 100644 sources/include/iCalendar.php create mode 100644 sources/include/mimePart.php create mode 100644 sources/include/z_RTF.php create mode 100644 sources/include/z_caldav.php create mode 100644 sources/include/z_carddav.php create mode 100644 sources/lib/default/sqlstatemachine.php create mode 100644 sources/sql/mysql.sql create mode 100644 sources/testing/samples/meeting_request.txt create mode 100644 sources/testing/samples/messages/emoticon.txt create mode 100644 sources/testing/samples/messages/emoticon_base64.txt create mode 100644 sources/testing/samples/messages/emoticon_subject.txt create mode 100644 sources/testing/samples/messages/french.txt create mode 100644 sources/testing/samples/messages/m0001.txt create mode 100644 sources/testing/samples/messages/m0002.txt create mode 100644 sources/testing/samples/messages/m0003.txt create mode 100644 sources/testing/samples/messages/m0004.txt create mode 100644 sources/testing/samples/messages/m0005.txt create mode 100644 sources/testing/samples/messages/m0006.txt create mode 100644 sources/testing/samples/messages/m0007.txt create mode 100644 sources/testing/samples/messages/m0008.txt create mode 100644 sources/testing/samples/messages/m0009.txt create mode 100644 sources/testing/samples/messages/m0010.txt create mode 100644 sources/testing/samples/messages/m0011.txt create mode 100644 sources/testing/samples/messages/m0012.txt create mode 100644 sources/testing/samples/messages/m0013.txt create mode 100644 sources/testing/samples/messages/m0014.txt create mode 100644 sources/testing/samples/messages/m0015.txt create mode 100644 sources/testing/samples/messages/m0016.txt create mode 100644 sources/testing/samples/messages/m0017.txt create mode 100644 sources/testing/samples/messages/m0018.txt create mode 100644 sources/testing/samples/messages/m1001.txt create mode 100644 sources/testing/samples/messages/m1002.txt create mode 100644 sources/testing/samples/messages/m1003.txt create mode 100644 sources/testing/samples/messages/m1004.txt create mode 100644 sources/testing/samples/messages/m1005.txt create mode 100644 sources/testing/samples/messages/m1006.txt create mode 100644 sources/testing/samples/messages/m1007.txt create mode 100644 sources/testing/samples/messages/m1008.txt create mode 100644 sources/testing/samples/messages/m1009.txt create mode 100644 sources/testing/samples/messages/m1010.txt create mode 100644 sources/testing/samples/messages/m1011.txt create mode 100644 sources/testing/samples/messages/m1012.txt create mode 100644 sources/testing/samples/messages/m1013.txt create mode 100644 sources/testing/samples/messages/m1014.txt create mode 100644 sources/testing/samples/messages/m1015.txt create mode 100644 sources/testing/samples/messages/m1016.txt create mode 100644 sources/testing/samples/messages/m2001.txt create mode 100644 sources/testing/samples/messages/m2002.txt create mode 100644 sources/testing/samples/messages/m2003.txt create mode 100644 sources/testing/samples/messages/m2004.txt create mode 100644 sources/testing/samples/messages/m2005.txt create mode 100644 sources/testing/samples/messages/m2006.txt create mode 100644 sources/testing/samples/messages/m2007.txt create mode 100644 sources/testing/samples/messages/m2008.txt create mode 100644 sources/testing/samples/messages/m2009.txt create mode 100644 sources/testing/samples/messages/m2010.txt create mode 100644 sources/testing/samples/messages/m2011.txt create mode 100644 sources/testing/samples/messages/m2012.txt create mode 100644 sources/testing/samples/messages/m2013.txt create mode 100644 sources/testing/samples/messages/m2014.txt create mode 100644 sources/testing/samples/messages/m2015.txt create mode 100644 sources/testing/samples/messages/m2016.txt create mode 100644 sources/testing/samples/messages/m3001.txt create mode 100644 sources/testing/samples/messages/m3002.txt create mode 100644 sources/testing/samples/messages/m3003.txt create mode 100644 sources/testing/samples/messages/m3004.txt create mode 100644 sources/testing/samples/messages/m4000.txt create mode 100644 sources/testing/samples/messages/smime001.txt create mode 100644 sources/testing/samples/messages/smime002.txt create mode 100644 sources/testing/samples/smime.txt create mode 100644 sources/testing/testing-caldav.php create mode 100644 sources/testing/testing-carddav.php create mode 100644 sources/testing/testing-forward.php create mode 100644 sources/testing/testing-imap.php create mode 100644 sources/testing/testing-imap_date.php create mode 100644 sources/testing/testing-imap_from.php create mode 100644 sources/testing/testing-imap_meeting.php create mode 100644 sources/testing/testing-imap_smtp.php create mode 100644 sources/testing/testing-mime-mail-parse.php create mode 100644 sources/testing/testing-mime-split.php create mode 100644 sources/testing/testing-mime.php create mode 100644 sources/testing/testing-mimetype.php create mode 100644 sources/testing/testing-preg_split.php create mode 100644 sources/testing/testing-ternary.php create mode 100644 sources/tools/fix-meetings-2.0.8+2.1.0-ios7.py diff --git a/sources/README.md b/sources/README.md new file mode 100644 index 0000000..aca9208 --- /dev/null +++ b/sources/README.md @@ -0,0 +1,112 @@ +Z-Push-contrib +============== + +This is a Z-Push fork with changes that I will try to put into the contrib branch, so they can get into the official Z-Push + +IMPORTANT: +For them to get into the official Z-Push, you must release the code under AGPLv3. Add this text to your commits "Released under the Affero GNU General Public License (AGPL) version 3" before merging. + +If you see some changes here, and you are the author, I will not contrib them, as I have no rights over them. +But I will try to reimplement with different code/approach so I can contribute them. When a sustitution is ready I will remove your changes from this repo. +If you want to help the community, contribute them yourself. + + +IMPORTANT 2: +All the code is AGPL licensed (or compatible, like the "include" files). So you can get a copy, modify it, use... your only obligation it's to publish your changes somewhere. + + +---------------------------------------------------- +Original Z-Push + +URL: http://www.zpush.org + +Z-Push is an implementation of the ActiveSync protocol, which is used 'over-the-air' for multi platform ActiveSync devices, including Windows Mobile, Ericsson and Nokia phones. With Z-Push any groupware can be connected and synced with these devices. + +License: GNU Affero Genaral Public License v3.0 (AGPLv3) + + +Documentation +============= +You can find some configuration guidelines in the Wiki https://github.com/fmbiete/Z-Push-contrib/wiki + +Requisites +========== +- PHP 5.5 (5.3 should also work, 5.4 it's fine, but 5.5 is better) +- NGINX or APACHE +- PHP-FPM or MOD_PHP + +Configuration +============= + +NGINX, 1.4 at least or you will need to enable chunkin mode (Use google for Apache configuration) + + server { + listen 443; + server_name zpush.domain.com; + + ssl on; + ssl_certificate /etc/ssl/certs/zpush.pem; + ssl_certificate_key /etc/ssl/private/zpush.key; + + root /usr/share/www/z-push-contrib; + index index.php; + + error_log /var/log/nginx/zpush-error.log; + access_log /var/log/nginx/zpush-access.log; + + location / { + try_files $uri $uri/ index.php; + } + + location /Microsoft-Server-ActiveSync { + rewrite ^(.*)$ /index.php last; + } + + location ~ .php$ { + include /etc/nginx/fastcgi_params; + fastcgi_index index.php; + fastcgi_param HTTPS on; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_pass unix:/var/run/php5-fpm.sock; + # Z-Push Ping command will be alive for 470s, but be safe + fastcgi_read_timeout 630; + } + } + +PHP-FPM + + max_execution_time=600 + short_open_tag=On + +And configure enough php-fpm processes, as a rough estimation you will need 1.5 x number users. + + +Backends +======== +Each backend has a README file, or comments in their config.php file. Look at them to configure correctly. Also you can look here https://github.com/fmbiete/Z-Push-contrib/wiki + + +StateMachine +============ +You have 2 StateMachine methods. + +- FILE - FileStateMachine : will store state info into files. You will use it in Active-Pasive setups +- SQL - SqlStateMachine: will store state info into a database. It uses PHP-PDO, so you will need to install the required packages for your database flavour and create the database. You will use it in Active-Active setups. + + +User-Device Permissions +======================= +Disabled by default, when enabled will limit what users and device can sync against your Z-Push installation. +It can auto-accept users, users and device until a maximum number of devices is reached. + +If using with FileStateMachine, edit the file STATE_DIR/PreAuthUserDevices to modificate the behaivour. That file is JSON formatted and it's filled each time a new user connect. + +If using with SqlStateMachine, look at the zpush_preauth_users table. + + + + +Links +===== +Microsoft ActiveSync Specification +http://msdn.microsoft.com/en-us/library/cc425499%28v=exchg.80%29.aspx diff --git a/sources/autodiscover/INSTALL b/sources/autodiscover/INSTALL new file mode 100644 index 0000000..5f334d5 --- /dev/null +++ b/sources/autodiscover/INSTALL @@ -0,0 +1,208 @@ +Z-Push AutoDiscover manual +-------------------------- +This manual gives an introduction to the Z-Push AutoDiscover service, discusses +technical details and explains the installation. + +Introduction +------------ +AutoDiscover is the service used to simplify the configuration of collaboration +accounts for clients, especially for mobile phones. +While in the past the user was required to enter the server name, user name and +password manually into his mobile phone in order to connect, with AutoDiscover +the user is only required to fill in his email address and the password. +AutoDiscover will try several methods to reach the correct server automatically. + + +How does it work? +----------------- +When speaking about AutoDiscover, this includes two distinct realms: +- AutoDiscover is a specification which defines the steps a client should take + in order to contact a service to request additional data. +- The AutoDiscover service is piece of software which accepts requests from the + clients, authenticates them, requests some additional data from the + collaboration server and sends this data back to the client. +The specification suggests several ways for client to contact the responsible +server to receive additional information. Tests have shown, that basically all +mobile phones tested support only the most basic ways. These are sufficient for +almost all types of scenarios and are the ones implemented by Z-Push AutoDiscover. +Please refer to the Mobile Compatibility List (http://z-push.sf.net/compatibility) +for an overview of supported and tested devices. +The used email address is the key for the process. The client splits it up into +the local and domain part (before and after the @-sign). The client then tries +to connect to this domain in order to get in contact with the AutoDiscover +service. The local part of the email address is used as "login" to the +AutoDiscover service. There is also an option, to use the full email address as +login name (see "Configuration" section below for details). + + + --------------- + | Client | + | e.g. mobile | + --------------- + / \ + 1. Searches for / \ 2. Data access + information / \ + / \ + V V + ---------------- -------------- + | AutoDiscover | redirects to | Z-Push | + | | --------------------> | ActiveSync | + ---------------- -------------- + \ / + Authen- \ / Synchronizes + ticates \ / + via Z-Push \ / + Backend V V + ----------------- + | Collaboration | + | Platform | + ----------------- + +Requirements +------------ +As described in the previous chapter, the local part of the email address or +the email address is used in order to log in. +Your configuration requires that this type of login is possible: +- either the user name is used to login and must be used in the email address + entered on the mobile, or +- the entire email address is used to login. + +Which option is used has to be configured in the AutoDiscover configuration and +in the underlying platform (e.g. ZCP (hosting mode)). +Most companies use the user name as local part of the email by default. From the +AutoDiscover point of view, it is not required that user is able to receive +emails at the used email address. It is recommended allowing that in order not +to confuse end users. + +AutoDiscover also requires a valid SSL certificate to work as expected. A very +little percentage of mobiles support self-signed certificates (showing a +pop-up alerting the user). Most mobiles silently ignore self-signed certificates +and just declare the AutoDiscover process as failed in such cases. +If AutoDiscover fails, the user is generally redirected to the +"manual configuration" of the client. + +If you do not plan to acquire an official certificate, you will probably not be +able to use the AutoDiscover service. +Depending on your setup, it could be necessary to add new DNS entries for your +mail domain. + + +Domain setup +------------ +There are two general ways the AutoDiscover process can be configured: +1. Directly with "yourdomain.com" website ("www.yourdomain.com" will most + probably not work) +2. With the sub-domain "autodiscover.yourdomain.com" +In both cases, an official SSL certificate is required. If you already have a +certificate for your domain, the webserver answering for that domain could be +reconfigured to allow AutoDiscover requests as well. In the case that you do +not have direct access to this type of configuration (e.g. hosting provider), +it's recommended to acquire a dedicated certificate for +"autodiscover.yourdomain.com". Please note, that this sub-domain can NOT be +renamed. In general, "wildcard" certificates can be used, as long they are +valid for the required domain. + + +Software requirements +--------------------- +Like Z-Push, AutoDiscover is written in PHP, where PHP 5.1 or newer is required. +Please consult the Z-Push INSTALL file for further information about PHP versions. +If only AutoDiscover is to be executed on a host, the Z-Push PHP dependencies do +NOT need to be installed. +AutoDiscover has one direct dependency, the php-xml parser library. + +These packages vary in names between the distributions. +- Generally install the packages: php-xml +- On Suse (SLES & OpenSuse) install the packages: php53-xml +- On RHEL based systems install the package: php-xml + + +Installation +------------ +AutoDiscover is part of the Z-Push package and uses some of the functionality +available in Z-Push. +It is possible to install AutoDiscover on the same host as Z-Push, or to +install them on different hosts. + +Currently, independently from the setup, it's recommended to extract the entire +z-push tarball and configure the services as required. +Please follow the install instructions from the Z-Push INSTALL file (section +"How to install") to copy the files to your server. +If you do not want to setup Z-Push on the host, do not add the "Alias" for +ActiveSync. + +To setup the SSL certificate, please refer to one of the many setup guides +available on the internet, like that one: +http://www.apache.com/resources/how-to-setup-an-ssl-certificate-on-apache/ + +The mobiles requests these URLs (where "yourdomain.com" corresponds to the +domain part of the email used in the client): + https://yourdomain.com/Autodiscover/Autodiscover.xml and/or + https://autodiscover.yourdomain.com/Autodiscover/Autodiscover.xml + +Add the following line to the apache site configuration file. + AliasMatch (?i)/Autodiscover/Autodiscover.xml "/usr/share/z-push/autodiscover/autodiscover.php" + +This line assumes that Z-Push is installed in /usr/share/z-push. If the path +is different, please adjust it accordingly. + +Note: some mobiles use different casings, like "AutoDiscover" in the URL. The +above statement is valid for these as well. + +Please restart Apache afterwards. + + +Configuration +------------- +There are several parameters in the configuration file, which allow to customize +the behaviour of the AutoDiscover Service. +The configuration, generally is located in the z-push/autodiscover directory and +is called "config.php". + +The parameters: +BASE_PATH This property specifies where the AutoDiscover files are + located. Normally there is no need to adjust this parameter. +SERVERURL This is the full URL where the Z-Push server is available. + You should adjust it to the domain/server where Z-Push is + installed. + +USE_FULLEMAIL_FOR_LOGIN If this is set to "true", AutoDiscover will attempt to + login on the collaboration server with the full email + address sent by the client. If disabled (default), the + local part of the email address is used. + +LOGFILEDIR The directory where logfiles are created. + +LOGFILE The default AutoDiscover log file. + +LOGERRORFILE The default AutoDiscover error log file. + +LOGLEVEL The loglevel, set it to WBXML to see the data received + and sent from/to clients. + +LOGAUTHFAIL Set to true, to explicitly log failed login attempts. + +BACKEND_PROVIDER The backend to be used. If empty (default) the code + will auto detect which backend to use. + +Please note: the desired backend also needs to be configured, in the +"backends//config.php" file. + + +Test installation +----------------- +If everything is correct, accessing with a browser the URL for your setup, you +should see: + 1. a pop-up asking for your username + password. Always use the email + address which you would also enter on the mobile (independently from + the configuration). + 2. if the authentication was successful, you will see a Z-Push informational + page (like when accessing the Z-Push location). + +Note: The same test can also be performed in the mobiles web browser to check +if the access works correctly from the mobile network. + +If the authentication fails, please check the configuration options of AutoDiscover. +Also check the logfiles for possible failures. + +If the manual method works, try setting up your mobile phone! :) diff --git a/sources/autodiscover/autodiscover.php b/sources/autodiscover/autodiscover.php new file mode 100644 index 0000000..301ebc7 --- /dev/null +++ b/sources/autodiscover/autodiscover.php @@ -0,0 +1,268 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +include_once('../lib/core/zpushdefs.php'); +include_once('../lib/exceptions/exceptions.php'); +include_once('../lib/utils/utils.php'); +include_once('../lib/core/zpush.php'); +include_once('../lib/core/zlog.php'); +include_once('../lib/interface/ibackend.php'); +include_once('../lib/interface/ichanges.php'); +include_once('../lib/interface/iexportchanges.php'); +include_once('../lib/interface/iimportchanges.php'); +include_once('../lib/interface/isearchprovider.php'); +include_once('../lib/interface/istatemachine.php'); +include_once('../version.php'); +include_once('config.php'); + +class ZPushAutodiscover { + const ACCEPTABLERESPONSESCHEMA = 'http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006'; + const MAXINPUTSIZE = 8192; // Bytes, the autodiscover request shouldn't exceed that value + + private static $instance; + + /** + * Static method to start the autodiscover process. + * + * @access public + * + * @return void + */ + public static function DoZPushAutodiscover() { + ZLog::Write(LOGLEVEL_DEBUG, '-------- Start ZPushAutodiscover'); + // TODO use filterevilinput? + if (stripos($_SERVER["REQUEST_METHOD"], "GET") !== false) { + ZLog::Write(LOGLEVEL_WARN, "GET request for autodiscover. Exiting."); + if (!headers_sent()) { + ZPush::PrintZPushLegal('GET not supported'); + } + ZLog::Write(LOGLEVEL_DEBUG, '-------- End ZPushAutodiscover'); + exit(1); + } + if (!isset(self::$instance)) { + self::$instance = new ZPushAutodiscover(); + } + self::$instance->DoAutodiscover(); + ZLog::Write(LOGLEVEL_DEBUG, '-------- End ZPushAutodiscover'); + } + + /** + * Does the complete autodiscover. + * @access public + * @throws AuthenticationRequiredException if login to the backend failed. + * @throws ZPushException if the incoming XML is invalid.. + * + * @return void + */ + public function DoAutodiscover() { + if (!defined('REAL_BASE_PATH')) { + define('REAL_BASE_PATH', str_replace('autodiscover/', '', BASE_PATH)); + } + set_include_path(get_include_path() . PATH_SEPARATOR . REAL_BASE_PATH); + $response = ""; + + try { + $incomingXml = $this->getIncomingXml(); + $backend = ZPush::GetBackend(); + $username = $this->login($backend, $incomingXml); + $userDetails = $backend->GetUserDetails($username); + $email = ($this->getAttribFromUserDetails($userDetails, 'emailaddress')) ? $this->getAttribFromUserDetails($userDetails, 'emailaddress') : $incomingXml->Request->EMailAddress; + $userFullname = ($this->getAttribFromUserDetails($userDetails, 'fullname')) ? $this->getAttribFromUserDetails($userDetails, 'fullname') : $email; + ZLog::Write(LOGLEVEL_WBXML, sprintf("Resolved user's '%s' fullname to '%s'", $username, $userFullname)); + $response = $this->createResponse($email, $userFullname); + setcookie("membername", $username); + } + + catch (AuthenticationRequiredException $ex) { + if (isset($incomingXml)) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("Unable to complete autodiscover because login failed for user with email '%s'", $incomingXml->Request->EMailAddress)); + } + else { + ZLog::Write(LOGLEVEL_ERROR, sprintf("Unable to complete autodiscover incorrect request: '%s'", $ex->getMessage())); + } + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Basic realm="ZPush"'); + http_response_code(401); + } + catch (ZPushException $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("Unable to complete autodiscover because of ZPushException. Error: %s", $ex->getMessage())); + if(!headers_sent()) { + header('HTTP/1.1 '. $ex->getHTTPCodeString()); + foreach ($ex->getHTTPHeaders() as $h) { + header($h); + } + } + } + $this->sendResponse($response); + } + + /** + * Processes the incoming XML request and parses it to a SimpleXMLElement. + * + * @access private + * @throws ZPushException if the XML is invalid. + * @throws AuthenticationRequiredException if no login data was sent. + * + * @return SimpleXMLElement + */ + private function getIncomingXml() { + if ($_SERVER['CONTENT_LENGTH'] > ZPushAutodiscover::MAXINPUTSIZE) { + throw new ZPushException('The request input size exceeds 8kb.'); + } + + if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) { + throw new AuthenticationRequiredException(); + } + + $input = @file_get_contents('php://input'); + $xml = simplexml_load_string($input); + + if (LOGLEVEL >= LOGLEVEL_WBXML) { + ZLog::Write(LOGLEVEL_WBXML, sprintf("ZPushAutodiscover->getIncomingXml() incoming XML data:%s%s", PHP_EOL, $xml->asXML())); + } + + if (!isset($xml->Request->EMailAddress)) { + throw new FatalException('Invalid input XML: no email address.'); + } + + if (Utils::GetLocalPartFromEmail($xml->Request->EMailAddress) != Utils::GetLocalPartFromEmail($_SERVER['PHP_AUTH_USER'])) { + ZLog::Write(LOGLEVEL_WARN, sprintf("The local part of the server auth user is different from the local part in the XML request ('%s' != '%s')", + Utils::GetLocalPartFromEmail($xml->Request->EMailAddress), Utils::GetLocalPartFromEmail($_SERVER['PHP_AUTH_USER']))); + } + + if (!isset($xml->Request->AcceptableResponseSchema)) { + throw new FatalException('Invalid input XML: no AcceptableResponseSchema.'); + } + + if ($xml->Request->AcceptableResponseSchema != ZPushAutodiscover::ACCEPTABLERESPONSESCHEMA) { + throw new FatalException('Invalid input XML: not a mobilesync responseschema.'); + } + + return $xml; + } + + /** + * Logins using the backend's Logon function. + * + * @param IBackend $backend + * @param String $incomingXml + * @access private + * @throws AuthenticationRequiredException if no login data was sent. + * + * @return string $username + */ + private function login($backend, $incomingXml) { + // Determine the login name depending on the configuration: complete email address or + // the local part only. + if (USE_FULLEMAIL_FOR_LOGIN) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Using the complete email address for login.")); + $username = $incomingXml->Request->EMailAddress; + } + else{ + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Using the username only for login.")); + $username = Utils::GetLocalPartFromEmail($incomingXml->Request->EMailAddress); + } + + if($backend->Logon($username, "", $_SERVER['PHP_AUTH_PW']) == false) { + throw new AuthenticationRequiredException("Access denied. Username or password incorrect."); + } + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAutodiscover->login() Using '%s' as the username.", $username)); + return $username; + } + + /** + * Creates the XML response. + * + * @param string $email + * @param string $userFullname + * @access private + * + * @return string + */ + private function createResponse($email, $userFullname) { + $xml = file_get_contents('response.xml'); + $response = new SimpleXMLElement($xml); + $response->Response->User->DisplayName = $userFullname; + $response->Response->User->EMailAddress = $email; + $response->Response->Action->Settings->Server->Url = SERVERURL; + $response->Response->Action->Settings->Server->Name = SERVERURL; + $response = $response->asXML(); + ZLog::Write(LOGLEVEL_WBXML, sprintf("ZPushAutodiscover->createResponse() XML response:%s%s", PHP_EOL, $response)); + return $response; + } + + /** + * Sends the response to the device. + * @param string $response + * @access private + * + * @return void + */ + private function sendResponse($response) { + ZLog::Write(LOGLEVEL_DEBUG, "ZPushAutodiscover->sendResponse() sending response..."); + header("Content-type: text/html"); + $output = fopen("php://output", "w+"); + fwrite($output, $response); + fclose($output); + ZLog::Write(LOGLEVEL_DEBUG, "ZPushAutodiscover->sendResponse() response sent."); + } + + /** + * Gets an attribute from user details. + * @param Array $userDetails + * @param String $attrib + * @access private + * + * @return String or false on error. + */ + private function getAttribFromUserDetails($userDetails, $attrib) { + if (isset($userDetails[$attrib]) && $userDetails[$attrib]) { + return $userDetails[$attrib]; + } + ZLog::Write(LOGLEVEL_WARN, sprintf("The backend was not able to find attribute '%s' of the user. Fall back to the default value.")); + return false; + } +} + +ZPushAutodiscover::DoZPushAutodiscover(); +?> \ No newline at end of file diff --git a/sources/autodiscover/config.php b/sources/autodiscover/config.php new file mode 100644 index 0000000..5403b59 --- /dev/null +++ b/sources/autodiscover/config.php @@ -0,0 +1,89 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +/********************************************************************************** + * Default settings + */ + // Defines the base path on the server + define('BASE_PATH', dirname($_SERVER['SCRIPT_FILENAME']). '/'); + + // The Z-Push server location for the autodiscover response + define('SERVERURL', 'https://localhost/Microsoft-Server-ActiveSync'); + + /* + * Whether to use the complete email address as a login name + * (e.g. user@company.com) or the username only (user). + * Possible values: + * false - use the username only (default). + * true - use the complete email address. + */ + define('USE_FULLEMAIL_FOR_LOGIN', false); + +/********************************************************************************** + * Logging settings + * Possible LOGLEVEL and LOGUSERLEVEL values are: + * LOGLEVEL_OFF - no logging + * LOGLEVEL_FATAL - log only critical errors + * LOGLEVEL_ERROR - logs events which might require corrective actions + * LOGLEVEL_WARN - might lead to an error or require corrective actions in the future + * LOGLEVEL_INFO - usually completed actions + * LOGLEVEL_DEBUG - debugging information, typically only meaningful to developers + * LOGLEVEL_WBXML - also prints the WBXML sent to/from the device + * LOGLEVEL_DEVICEID - also prints the device id for every log entry + * LOGLEVEL_WBXMLSTACK - also prints the contents of WBXML stack + * + * The verbosity increases from top to bottom. More verbose levels include less verbose + * ones, e.g. setting to LOGLEVEL_DEBUG will also output LOGLEVEL_FATAL, LOGLEVEL_ERROR, + * LOGLEVEL_WARN and LOGLEVEL_INFO level entries. + */ + define('LOGFILEDIR', '/var/log/z-push/'); + define('LOGFILE', LOGFILEDIR . 'autodiscover.log'); + define('LOGERRORFILE', LOGFILEDIR . 'autodiscover-error.log'); + define('LOGLEVEL', LOGLEVEL_INFO); + define('LOGUSERLEVEL', LOGLEVEL); +/********************************************************************************** + * Backend settings + */ + // the backend data provider + define('BACKEND_PROVIDER', ''); +?> \ No newline at end of file diff --git a/sources/autodiscover/response.xml b/sources/autodiscover/response.xml new file mode 100644 index 0000000..9fa473d --- /dev/null +++ b/sources/autodiscover/response.xml @@ -0,0 +1,19 @@ + + + +en:us + + + + + + + +MobileSync + + + + + + + \ No newline at end of file diff --git a/sources/backend/caldav/AUTHOR b/sources/backend/caldav/AUTHOR new file mode 100644 index 0000000..af8002c --- /dev/null +++ b/sources/backend/caldav/AUTHOR @@ -0,0 +1,3 @@ +The Author of this backend is dupondje, I could have modified it. +You can found the original code here: +https://github.com/dupondje/PHP-Push-2 diff --git a/sources/backend/caldav/REQUIREMENTS b/sources/backend/caldav/REQUIREMENTS new file mode 100644 index 0000000..15735e8 --- /dev/null +++ b/sources/backend/caldav/REQUIREMENTS @@ -0,0 +1,5 @@ +REQUIREMENTS: +php-curl +libawl-php + +CalDAV server (DAViCal, Sabredav, Sogo, Owncloud...) \ No newline at end of file diff --git a/sources/backend/caldav/caldav.php b/sources/backend/caldav/caldav.php new file mode 100644 index 0000000..c8d3b9a --- /dev/null +++ b/sources/backend/caldav/caldav.php @@ -0,0 +1,1466 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// config file +require_once("backend/caldav/config.php"); + +include_once('lib/default/diffbackend/diffbackend.php'); +include_once('include/z_caldav.php'); +include_once('include/z_RTF.php'); +include_once('include/iCalendar.php'); + +class BackendCalDAV extends BackendDiff { + private $_caldav; + private $_caldav_path; + private $_collection = array(); + private $_username; + + private $changessinkinit; + private $sinkdata; + private $sinkmax; + + + /** + * Constructor + * + */ + public function BackendCalDAV() { + if (!function_exists("curl_init")) { + throw new FatalException("BackendCalDAV(): php-curl is not found", 0, null, LOGLEVEL_FATAL); + } + + $this->changessinkinit = false; + $this->sinkdata = array(); + $this->sinkmax = array(); + } + + /** + * Login to the CalDAV backend + * @see IBackend::Logon() + */ + public function Logon($username, $domain, $password) { + $this->_username = $username; + $this->_caldav_path = str_replace('%u', $username, CALDAV_PATH); + $this->_caldav = new CalDAVClient(CALDAV_SERVER . ":" . CALDAV_PORT . $this->_caldav_path, $username, $password); + $options = $this->_caldav->DoOptionsRequest(); + if (isset($options["PROPFIND"])) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->Logon(): User '%s' is authenticated on CalDAV", $username)); + return true; + } + else { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendCalDAV->Logon(): User '%s' is not authenticated on CalDAV", $username)); + return false; + } + } + + /** + * The connections to CalDAV are always directly closed. So nothing special needs to happen here. + * @see IBackend::Logoff() + */ + public function Logoff() { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->Logoff()")); + $this->_caldav = null; + + $this->SaveStorages(); + + unset($this->sinkdata); + unset($this->sinkmax); + + return true; + } + + /** + * CalDAV doesn't need to handle SendMail + * @see IBackend::SendMail() + */ + public function SendMail($sm) { + return false; + } + + /** + * No attachments in CalDAV + * @see IBackend::GetAttachmentData() + */ + public function GetAttachmentData($attname) { + return false; + } + + /** + * Deletes are always permanent deletes. Messages doesn't get moved. + * @see IBackend::GetWasteBasket() + */ + public function GetWasteBasket() { + return false; + } + + /** + * Get a list of all the folders we are going to sync. + * Each caldav calendar can contain tasks (prefix T) and events (prefix C), so duplicate each calendar found. + * @see BackendDiff::GetFolderList() + */ + public function GetFolderList() { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->GetFolderList(): Getting all folders.")); + $folders = array(); + $calendars = $this->_caldav->FindCalendars(); + foreach ($calendars as $val) { + $folder = array(); + $fpath = explode("/", $val->url, -1); + if (is_array($fpath)) { + $folderid = array_pop($fpath); + $id = "C" . $folderid; + $folders[] = $this->StatFolder($id); + $id = "T" . $folderid; + $folders[] = $this->StatFolder($id); + } + } + return $folders; + } + + /** + * Returning a SyncFolder + * @see BackendDiff::GetFolder() + */ + public function GetFolder($id) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->GetFolder('%s')", $id)); + $val = $this->_caldav->GetCalendarDetails($this->_caldav_path . substr($id, 1) . "/"); + $folder = new SyncFolder(); + $folder->parentid = "0"; + $folder->displayname = $val->displayname; + $folder->serverid = $id; + if ($id[0] == "C") { + if (defined('CALDAV_PERSONAL') && strtolower(substr($id, 1)) == CALDAV_PERSONAL) { + $folder->type = SYNC_FOLDER_TYPE_APPOINTMENT; + } + else { + $folder->type = SYNC_FOLDER_TYPE_USER_APPOINTMENT; + } + } + else { + if (defined('CALDAV_PERSONAL') && strtolower(substr($id, 1)) == CALDAV_PERSONAL) { + $folder->type = SYNC_FOLDER_TYPE_TASK; + } + else { + $folder->type = SYNC_FOLDER_TYPE_USER_TASK; + } + } + return $folder; + } + + /** + * Returns information on the folder. + * @see BackendDiff::StatFolder() + */ + public function StatFolder($id) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->StatFolder('%s')", $id)); + $val = $this->GetFolder($id); + $folder = array(); + $folder["id"] = $id; + $folder["parent"] = $val->parentid; + $folder["mod"] = $val->serverid; + return $folder; + } + + /** + * ChangeFolder is not supported under CalDAV + * @see BackendDiff::ChangeFolder() + */ + public function ChangeFolder($folderid, $oldid, $displayname, $type) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangeFolder('%s','%s','%s','%s')", $folderid, $oldid, $displayname, $type)); + return false; + } + + /** + * DeleteFolder is not supported under CalDAV + * @see BackendDiff::DeleteFolder() + */ + public function DeleteFolder($id, $parentid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->DeleteFolder('%s','%s')", $id, $parentid)); + return false; + } + + /** + * Get a list of all the messages. + * @see BackendDiff::GetMessageList() + */ + public function GetMessageList($folderid, $cutoffdate) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->GetMessageList('%s','%s')", $folderid, $cutoffdate)); + + /* Calculating the range of events we want to sync */ + $begin = gmdate("Ymd\THis\Z", $cutoffdate); + $finish = gmdate("Ymd\THis\Z", 2147483647); + + $path = $this->_caldav_path . substr($folderid, 1) . "/"; + if ($folderid[0] == "C") { + $msgs = $this->_caldav->GetEvents($begin, $finish, $path); + } + else { + $msgs = $this->_caldav->GetTodos($begin, $finish, false, false, $path); + } + + $messages = array(); + foreach ($msgs as $e) { + $id = $e['href']; + $this->_collection[$id] = $e; + $messages[] = $this->StatMessage($folderid, $id); + } + return $messages; + } + + /** + * Get a SyncObject by its ID + * @see BackendDiff::GetMessage() + */ + public function GetMessage($folderid, $id, $contentparameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->GetMessage('%s','%s')", $folderid, $id)); + $data = $this->_collection[$id]['data']; + + if ($folderid[0] == "C") { + return $this->_ParseVEventToAS($data, $contentparameters); + } + if ($folderid[0] == "T") { + return $this->_ParseVTodoToAS($data, $contentparameters); + } + return false; + } + + /** + * Return id, flags and mod of a messageid + * @see BackendDiff::StatMessage() + */ + public function StatMessage($folderid, $id) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->StatMessage('%s','%s')", $folderid, $id)); + $type = "VEVENT"; + if ($folderid[0] == "T") { + $type = "VTODO"; + } + $data = null; + if (array_key_exists($id, $this->_collection)) { + $data = $this->_collection[$id]; + } + else { + $path = $this->_caldav_path . substr($folderid, 1) . "/"; + $e = $this->_caldav->GetEntryByUid(substr($id, 0, strlen($id)-4), $path, $type); + if ($e == null && count($e) <= 0) + return; + $data = $e[0]; + } + $message = array(); + $message['id'] = $data['href']; + $message['flags'] = "1"; + $message['mod'] = $data['etag']; + return $message; + } + + /** + * Change/Add a message with contents received from ActiveSync + * @see BackendDiff::ChangeMessage() + */ + public function ChangeMessage($folderid, $id, $message, $contentParameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangeMessage('%s','%s')", $folderid, $id)); + + if ($id) { + $mod = $this->StatMessage($folderid, $id); + $etag = $mod['mod']; + } + else { + $etag = "*"; + $date = gmdate("Ymd\THis\Z"); + $random = hash("md5", microtime()); + $id = $date . "-" . $random . ".ics"; + } + + $data = $this->_ParseASToVCalendar($message, $folderid, substr($id, 0, strlen($id)-4)); + + $url = $this->_caldav_path . substr($folderid, 1) . "/" . $id; + $etag_new = $this->_caldav->DoPUTRequest($url, $data, $etag); + + $item = array(); + $item['href'] = $id; + $item['etag'] = $etag_new; + $item['data'] = $data; + $this->_collection[$id] = $item; + + return $this->StatMessage($folderid, $id); + } + + /** + * Change the read flag is not supported. + * @see BackendDiff::SetReadFlag() + */ + public function SetReadFlag($folderid, $id, $flags, $contentParameters) { + return false; + } + + /** + * Changes the 'star' flag of a message on disk + * + * @param string $folderid id of the folder + * @param string $id id of the message + * @param int $flags star flag of the message + * + * @access public + * @return boolean status of the operation + * @throws StatusException could throw specific SYNC_STATUS_* exceptions + */ + public function SetStarFlag($folderid, $id, $flags, $contentParameters) { + return false; + } + + /** + * Delete a message from the CalDAV server. + * @see BackendDiff::DeleteMessage() + */ + public function DeleteMessage($folderid, $id, $contentParameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->DeleteMessage('%s','%s')", $folderid, $id)); + $url = $this->_caldav_path . substr($folderid, 1) . "/" . $id; + $http_status_code = $this->_caldav->DoDELETERequest($url); + if ($http_status_code == "204") { + return true; + } + return false; + } + + /** + * Move a message is not supported by CalDAV. + * @see BackendDiff::MoveMessage() + */ + public function MoveMessage($folderid, $id, $newfolderid, $contentParameters) { + return false; + } + + /** + * Indicates which AS version is supported by the backend. + * + * @access public + * @return string AS version constant + */ + public function GetSupportedASVersion() { + return ZPush::ASV_14; + } + + /** + * Indicates if the backend has a ChangesSink. + * A sink is an active notification mechanism which does not need polling. + * The CalDAV backend simulates a sink by polling revision dates from the events or use the native sync-collection. + * + * @access public + * @return boolean + */ + public function HasChangesSink() { + return true; + } + + /** + * The folder should be considered by the sink. + * Folders which were not initialized should not result in a notification + * of IBackend->ChangesSink(). + * + * @param string $folderid + * + * @access public + * @return boolean false if found can not be found + */ + public function ChangesSinkInitialize($folderid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangesSinkInitialize(): folderid '%s'", $folderid)); + + // We don't need the actual events, we only need to get the changes since this moment + $init_ok = true; + $url = $this->_caldav_path . substr($folderid, 1) . "/"; + $this->sinkdata[$folderid] = $this->_caldav->GetSync($url, true, CALDAV_SUPPORTS_SYNC); + if (CALDAV_SUPPORTS_SYNC) { + // we don't need to store the sinkdata if the caldav server supports native sync + unset($this->sinkdata[$url]); + $this->sinkdata[$folderid] = array(); + } + + $this->changessinkinit = $init_ok; + $this->sinkmax = array(); + + return $this->changessinkinit; + } + + /** + * The actual ChangesSink. + * For max. the $timeout value this method should block and if no changes + * are available return an empty array. + * If changes are available a list of folderids is expected. + * + * @param int $timeout max. amount of seconds to block + * + * @access public + * @return array + */ + public function ChangesSink($timeout = 30) { + $notifications = array(); + $stopat = time() + $timeout - 1; + + //We can get here and the ChangesSink not be initialized yet + if (!$this->changessinkinit) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangesSink - Not initialized ChangesSink, sleep and exit")); + // We sleep and do nothing else + sleep($timeout); + return $notifications; + } + + while($stopat > time() && empty($notifications)) { + + foreach ($this->sinkdata as $k => $v) { + $changed = false; + + $url = $this->_caldav_path . substr($k, 1) . "/"; + $response = $this->_caldav->GetSync($url, false, CALDAV_SUPPORTS_SYNC); + + if (CALDAV_SUPPORTS_SYNC) { + if (count($response) > 0) { + $changed = true; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangesSink - Changes detected")); + } + } + else { + // If the numbers of events are different, we know for sure, there are changes + if (count($response) != count($v)) { + $changed = true; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangesSink - Changes detected")); + } + else { + // If the numbers of events are equals, we compare the biggest date + // FIXME: we are comparing strings no dates + if (!isset($this->sinkmax[$k])) { + $this->sinkmax[$k] = ''; + for ($i = 0; $i < count($v); $i++) { + if ($v[$i]['getlastmodified'] > $this->sinkmax[$k]) { + $this->sinkmax[$k] = $v[$i]['getlastmodified']; + } + } + } + + for ($i = 0; $i < count($response); $i++) { + if ($response[$i]['getlastmodified'] > $this->sinkmax[$k]) { + $changed = true; + } + } + + if ($changed) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangesSink - Changes detected")); + } + } + } + + if ($changed) { + $notifications[] = $k; + } + } + + if (empty($notifications)) + sleep(5); + } + + return $notifications; + } + + + /** + * Convert a iCAL VEvent to ActiveSync format + * @param ical_vevent $data + * @param ContentParameters $contentparameters + * @return SyncAppointment + */ + private function _ParseVEventToAS($data, $contentparameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->_ParseVEventToAS(): Parsing VEvent")); + $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation()); + $message = new SyncAppointment(); + + $ical = new iCalComponent($data); + $timezones = $ical->GetComponents("VTIMEZONE"); + $timezone = ""; + if (count($timezones) > 0) { + $timezone = Utils::ParseTimezone($timezones[0]->GetPValue("TZID")); + } + if (!$timezone) { + $timezone = date_default_timezone_get(); + } + $message->timezone = $this->_GetTimezoneString($timezone); + + $vevents = $ical->GetComponents("VTIMEZONE", false); + foreach ($vevents as $event) { + $rec = $event->GetProperties("RECURRENCE-ID"); + if (count($rec) > 0) { + $recurrence_id = reset($rec); + $exception = new SyncAppointmentException(); + $tzid = Utils::ParseTimezone($recurrence_id->GetParameterValue("TZID")); + if (!$tzid) { + $tzid = $timezone; + } + $exception->exceptionstarttime = Utils::MakeUTCDate($recurrence_id->Value(), $tzid); + $exception->deleted = "0"; + $exception = $this->_ParseVEventToSyncObject($event, $exception, $truncsize); + if (!isset($message->exceptions)) { + $message->exceptions = array(); + } + $message->exceptions[] = $exception; + } + else { + $message = $this->_ParseVEventToSyncObject($event, $message, $truncsize); + } + } + return $message; + } + + /** + * Parse 1 VEvent + * @param ical_vevent $event + * @param SyncAppointment(Exception) $message + * @param int $truncsize + */ + private function _ParseVEventToSyncObject($event, $message, $truncsize) { + //Defaults + $message->busystatus = "2"; + + $properties = $event->GetProperties(); + foreach ($properties as $property) { + switch ($property->Name()) { + case "LAST-MODIFIED": + $message->dtstamp = Utils::MakeUTCDate($property->Value()); + break; + + case "DTSTART": + $message->starttime = Utils::MakeUTCDate($property->Value(), Utils::ParseTimezone($property->GetParameterValue("TZID"))); + if (strlen($property->Value()) == 8) { + $message->alldayevent = "1"; + } + break; + + case "SUMMARY": + $message->subject = $property->Value(); + break; + + case "UID": + $message->uid = $property->Value(); + break; + + case "ORGANIZER": + $org_mail = str_ireplace("MAILTO:", "", $property->Value()); + $message->organizeremail = $org_mail; + $org_cn = $property->GetParameterValue("CN"); + if ($org_cn) { + $message->organizername = $org_cn; + } + break; + + case "LOCATION": + $message->location = $property->Value(); + break; + + case "DTEND": + $message->endtime = Utils::MakeUTCDate($property->Value(), Utils::ParseTimezone($property->GetParameterValue("TZID"))); + if (strlen($property->Value()) == 8) { + $message->alldayevent = "1"; + } + break; + + case "DURATION": + if (!isset($message->endtime)) { + $start = date_create("@" . $message->starttime); + $val = str_replace("+", "", $property->Value()); + $interval = new DateInterval($val); + $message->endtime = date_timestamp_get(date_add($start, $interval)); + } + break; + + case "RRULE": + $message->recurrence = $this->_ParseRecurrence($property->Value(), "vevent"); + break; + + case "CLASS": + switch ($property->Value()) { + case "PUBLIC": + $message->sensitivity = "0"; + break; + case "PRIVATE": + $message->sensitivity = "2"; + break; + case "CONFIDENTIAL": + $message->sensitivity = "3"; + break; + } + break; + + case "TRANSP": + switch ($property->Value()) { + case "TRANSPARENT": + $message->busystatus = "0"; + break; + case "OPAQUE": + $message->busystatus = "2"; + break; + } + break; + + // SYNC_POOMCAL_MEETINGSTATUS + // Meetingstatus values + // 0 = is not a meeting + // 1 = is a meeting + // 3 = Meeting received + // 5 = Meeting is canceled + // 7 = Meeting is canceled and received + // 9 = as 1 + // 11 = as 3 + // 13 = as 5 + // 15 = as 7 + case "STATUS": + switch ($property->Value()) { + case "TENTATIVE": + $message->meetingstatus = "3"; // was 1 + break; + case "CONFIRMED": + $message->meetingstatus = "1"; // was 3 + break; + case "CANCELLED": + $message->meetingstatus = "5"; // could also be 7 + break; + } + break; + + case "ATTENDEE": + $attendee = new SyncAttendee(); + $att_email = str_ireplace("MAILTO:", "", $property->Value()); + $attendee->email = $att_email; + $att_cn = $property->GetParameterValue("CN"); + if ($att_cn) { + $attendee->name = $att_cn; + } + if (isset($message->attendees) && is_array($message->attendees)) { + $message->attendees[] = $attendee; + } + else { + $message->attendees = array($attendee); + } + break; + + case "DESCRIPTION": + if (Request::GetProtocolVersion() >= 12.0) { + $message->asbody = new SyncBaseBody(); + $message->asbody->data = str_replace("\n","\r\n", str_replace("\r","",Utils::ConvertHtmlToText($property->Value()))); + // truncate body, if requested + if (strlen($message->asbody->data) > $truncsize) { + $message->asbody->truncated = 1; + $message->asbody->data = Utils::Utf8_truncate($message->asbody->data, $truncsize); + } + else { + $message->asbody->truncated = 0; + } + $message->nativebodytype = SYNC_BODYPREFERENCE_PLAIN; + } + else { + $body = $property->Value(); + // truncate body, if requested + if(strlen($body) > $truncsize) { + $body = Utils::Utf8_truncate($body, $truncsize); + $message->bodytruncated = 1; + } else { + $body = $body; + $message->bodytruncated = 0; + } + $body = str_replace("\n","\r\n", str_replace("\r","",$body)); + $message->body = $body; + } + break; + + case "CATEGORIES": + $categories = explode(",", $property->Value()); + $message->categories = $categories; + break; + + case "EXDATE": + $exception = new SyncAppointmentException(); + $exception->deleted = "1"; + $exception->exceptionstarttime = Utils::MakeUTCDate($property->Value()); + if (!isset($message->exceptions)) { + $message->exceptions = array(); + } + $message->exceptions[] = $exception; + break; + + //We can ignore the following + case "PRIORITY": + case "SEQUENCE": + case "CREATED": + case "DTSTAMP": + case "X-MOZ-GENERATION": + case "X-MOZ-LASTACK": + case "X-LIC-ERROR": + case "RECURRENCE-ID": + break; + + default: + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->_ParseVEventToSyncObject(): '%s' is not yet supported.", $property->Name())); + } + } + + $valarm = current($event->GetComponents("VALARM")); + if ($valarm) { + $properties = $valarm->GetProperties(); + foreach ($properties as $property) { + if ($property->Name() == "TRIGGER") { + $parameters = $property->Parameters(); + if (array_key_exists("VALUE", $parameters) && $parameters["VALUE"] == "DATE-TIME") { + $trigger = date_create("@" . Utils::MakeUTCDate($property->Value())); + $begin = date_create("@" . $message->starttime); + $interval = date_diff($begin, $trigger); + $message->reminder = $interval->format("%i") + $interval->format("%h") * 60 + $interval->format("%a") * 60 * 24; + } + elseif (!array_key_exists("VALUE", $parameters) || $parameters["VALUE"] == "DURATION") { + $val = str_replace("-", "", $property->Value()); + $interval = new DateInterval($val); + $message->reminder = $interval->format("%i") + $interval->format("%h") * 60 + $interval->format("%a") * 60 * 24; + } + } + } + } + + return $message; + } + + /** + * Parse a RRULE + * @param string $rrulestr + */ + private function _ParseRecurrence($rrulestr, $type) { + $recurrence = new SyncRecurrence(); + if ($type == "vtodo") { + $recurrence = new SyncTaskRecurrence(); + } + $rrules = explode(";", $rrulestr); + foreach ($rrules as $rrule) { + $rule = explode("=", $rrule); + switch ($rule[0]) { + case "FREQ": + switch ($rule[1]) { + case "DAILY": + $recurrence->type = "0"; + break; + case "WEEKLY": + $recurrence->type = "1"; + break; + case "MONTHLY": + $recurrence->type = "2"; + break; + case "YEARLY": + $recurrence->type = "5"; + } + break; + + case "UNTIL": + $recurrence->until = Utils::MakeUTCDate($rule[1]); + break; + + case "COUNT": + $recurrence->occurrences = $rule[1]; + break; + + case "INTERVAL": + $recurrence->interval = $rule[1]; + break; + + case "BYDAY": + $dval = 0; + $days = explode(",", $rule[1]); + foreach ($days as $day) { + if ($recurrence->type == "2") { + if (strlen($day) > 2) { + $recurrence->weekofmonth = intval($day); + $day = substr($day,-2); + } + else { + $recurrence->weekofmonth = 1; + } + $recurrence->type = "3"; + } + switch ($day) { + // 1 = Sunday + // 2 = Monday + // 4 = Tuesday + // 8 = Wednesday + // 16 = Thursday + // 32 = Friday + // 62 = Weekdays // not in spec: daily weekday recurrence + // 64 = Saturday + case "SU": + $dval += 1; + break; + case "MO": + $dval += 2; + break; + case "TU": + $dval += 4; + break; + case "WE": + $dval += 8; + break; + case "TH": + $dval += 16; + break; + case "FR": + $dval += 32; + break; + case "SA": + $dval += 64; + break; + } + } + $recurrence->dayofweek = $dval; + break; + + //Only 1 BYMONTHDAY is supported, so BYMONTHDAY=2,3 will only include 2 + case "BYMONTHDAY": + $days = explode(",", $rule[1]); + $recurrence->dayofmonth = $days[0]; + break; + + case "BYMONTH": + $recurrence->monthofyear = $rule[1]; + break; + + default: + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->_ParseRecurrence(): '%s' is not yet supported.", $rule[0])); + } + } + return $recurrence; + } + + /** + * Generate a iCAL VCalendar from ActiveSync object. + * @param string $data + * @param string $folderid + * @param string $id + */ + private function _ParseASToVCalendar($data, $folderid, $id) { + $ical = new iCalComponent(); + $ical->SetType("VCALENDAR"); + $ical->AddProperty("VERSION", "2.0"); + $ical->AddProperty("PRODID", "-//php-push//NONSGML PHP-Push Calendar//EN"); + $ical->AddProperty("CALSCALE", "GREGORIAN"); + + if ($folderid[0] == "C") { + $vevent = $this->_ParseASEventToVEvent($data, $id); + $vevent->AddProperty("UID", $id); + $ical->AddComponent($vevent); + if (isset($data->exceptions) && is_array($data->exceptions)) { + foreach ($data->exceptions as $ex) { + $exception = $this->_ParseASEventToVEvent($ex, $id); + if ($data->alldayevent == 1) { + $exception->AddProperty("RECURRENCE-ID", $this->_GetDateFromUTC("Ymd", $ex->exceptionstarttime, $data->timezone), array("VALUE" => "DATE")); + } + else { + $exception->AddProperty("RECURRENCE-ID", gmdate("Ymd\THis\Z", $ex->exceptionstarttime)); + } + $exception->AddProperty("UID", $id); + $ical->AddComponent($exception); + } + } + } + if ($folderid[0] == "T") { + $vtodo = $this->_ParseASTaskToVTodo($data, $id); + $vtodo->AddProperty("UID", $id); + $vtodo->AddProperty("DTSTAMP", gmdate("Ymd\THis\Z")); + $ical->AddComponent($vtodo); + } + + return $ical->Render(); + } + + /** + * Generate a VEVENT from a SyncAppointment(Exception). + * @param string $data + * @param string $id + * @return iCalComponent + */ + private function _ParseASEventToVEvent($data, $id) { + $vevent = new iCalComponent(); + $vevent->SetType("VEVENT"); + + if (isset($data->dtstamp)) { + $vevent->AddProperty("DTSTAMP", gmdate("Ymd\THis\Z", $data->dtstamp)); + $vevent->AddProperty("LAST-MODIFIED", gmdate("Ymd\THis\Z", $data->dtstamp)); + } + if (isset($data->starttime)) { + if ($data->alldayevent == 1) { + $vevent->AddProperty("DTSTART", $this->_GetDateFromUTC("Ymd", $data->starttime, $data->timezone), array("VALUE" => "DATE")); + } + else { + $vevent->AddProperty("DTSTART", gmdate("Ymd\THis\Z", $data->starttime)); + } + } + if (isset($data->subject)) { + $vevent->AddProperty("SUMMARY", $data->subject); + } + if (isset($data->organizeremail)) { + if (isset($data->organizername)) { + $vevent->AddProperty("ORGANIZER", sprintf("MAILTO:%s", $data->organizeremail), array("CN" => $data->organizername)); + } + else { + $vevent->AddProperty("ORGANIZER", sprintf("MAILTO:%s", $data->organizeremail)); + } + } + if (isset($data->location)) { + $vevent->AddProperty("LOCATION", $data->location); + } + if (isset($data->endtime)) { + if ($data->alldayevent == 1) { + $vevent->AddProperty("DTEND", $this->_GetDateFromUTC("Ymd", $data->endtime, $data->timezone), array("VALUE" => "DATE")); + } + else { + $vevent->AddProperty("DTEND", gmdate("Ymd\THis\Z", $data->endtime)); + } + } + if (isset($data->recurrence)) { + $vevent->AddProperty("RRULE", $this->_GenerateRecurrence($data->recurrence)); + } + if (isset($data->sensitivity)) { + switch ($data->sensitivity) { + case "0": + $vevent->AddProperty("CLASS", "PUBLIC"); + break; + case "2": + $vevent->AddProperty("CLASS", "PRIVATE"); + break; + case "3": + $vevent->AddProperty("CLASS", "CONFIDENTIAL"); + break; + } + } + if (isset($data->busystatus)) { + switch ($data->busystatus) { + case "0": + case "1": + $vevent->AddProperty("TRANSP", "TRANSPARENT"); + break; + case "2": + case "3": + $vevent->AddProperty("TRANSP", "OPAQUE"); + break; + } + } + if (isset($data->reminder)) { + $valarm = new iCalComponent(); + $valarm->SetType("VALARM"); + $valarm->AddProperty("ACTION", "DISPLAY"); + $trigger = "-PT" . $data->reminder . "M"; + $valarm->AddProperty("TRIGGER", $trigger); + $vevent->AddComponent($valarm); + } + if (isset($data->rtf)) { + $rtfparser = new rtf(); + $rtfparser->loadrtf(base64_decode($data->rtf)); + $rtfparser->output("ascii"); + $rtfparser->parse(); + $vevent->AddProperty("DESCRIPTION", $rtfparser->out); + } + if (isset($data->meetingstatus)) { + switch ($data->meetingstatus) { + case "1": + $vevent->AddProperty("STATUS", "TENTATIVE"); + break; + case "3": + $vevent->AddProperty("STATUS", "CONFIRMED"); + break; + case "5": + case "7": + $vevent->AddProperty("STATUS", "CANCELLED"); + break; + } + } + if (isset($data->attendees) && is_array($data->attendees)) { + //If there are attendees, we need to set ORGANIZER + //Some phones doesn't send the organizeremail, so we gotto get it somewhere else. + //Lets use the login here ($username) + if (!isset($data->organizeremail)) { + $vevent->AddProperty("ORGANIZER", sprintf("MAILTO:%s", $this->_username)); + } + foreach ($data->attendees as $att) { + $att_str = sprintf("MAILTO:%s", $att->email); + $vevent->AddProperty("ATTENDEE", $att_str, array("CN" => $att->name)); + } + } + if (isset($data->body)) { + $vevent->AddProperty("DESCRIPTION", $data->body); + } + if (isset($data->asbody->data)) { + $vevent->AddProperty("DESCRIPTION", $data->asbody->data); + } + if (isset($data->categories) && is_array($data->categories)) { + $vevent->AddProperty("CATEGORIES", implode(",", $data->categories)); + } + + return $vevent; + } + + /** + * Generate Recurrence + * @param string $rec + */ + private function _GenerateRecurrence($rec) { + $rrule = array(); + if (isset($rec->type)) { + $freq = ""; + switch ($rec->type) { + case "0": + $freq = "DAILY"; + break; + case "1": + $freq = "WEEKLY"; + break; + case "2": + case "3": + $freq = "MONTHLY"; + break; + case "5": + $freq = "YEARLY"; + break; + } + $rrule[] = "FREQ=" . $freq; + } + if (isset($rec->until)) { + $rrule[] = "UNTIL=" . gmdate("Ymd\THis\Z", $rec->until); + } + if (isset($rec->occurrences)) { + $rrule[] = "COUNT=" . $rec->occurrences; + } + if (isset($rec->interval)) { + $rrule[] = "INTERVAL=" . $rec->interval; + } + if (isset($rec->dayofweek)) { + $week = ''; + if (isset($rec->weekofmonth)) { + $week = $rec->weekofmonth; + } + $days = array(); + if (($rec->dayofweek & 1) == 1) { + if (empty($week)) { + $days[] = "SU"; + } + else { + $days[] = $week . "SU"; + } + } + if (($rec->dayofweek & 2) == 2) { + if (empty($week)) { + $days[] = "MO"; + } + else { + $days[] = $week . "MO"; + } + } + if (($rec->dayofweek & 4) == 4) { + if (empty($week)) { + $days[] = "TU"; + } + else { + $days[] = $week . "TU"; + } + } + if (($rec->dayofweek & 8) == 8) { + if (empty($week)) { + $days[] = "WE"; + } + else { + $days[] = $week . "WE"; + } + } + if (($rec->dayofweek & 16) == 16) { + if (empty($week)) { + $days[] = "TH"; + } + else { + $days[] = $week . "TH"; + } + } + if (($rec->dayofweek & 32) == 32) { + if (empty($week)) { + $days[] = "FR"; + } + else { + $days[] = $week . "FR"; + } + } + if (($rec->dayofweek & 64) == 64) { + if (empty($week)) { + $days[] = "SA"; + } + else { + $days[] = $week . "SA"; + } + } + $rrule[] = "BYDAY=" . implode(",", $days); + } + if (isset($rec->dayofmonth)) { + $rrule[] = "BYMONTHDAY=" . $rec->dayofmonth; + } + if (isset($rec->monthofyear)) { + $rrule[] = "BYMONTH=" . $rec->monthofyear; + } + return implode(";", $rrule); + } + + /** + * Convert a iCAL VTodo to ActiveSync format + * @param string $data + * @param ContentParameters $contentparameters + */ + private function _ParseVTodoToAS($data, $contentparameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->_ParseVTodoToAS(): Parsing VTodo")); + $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation()); + + $message = new SyncTask(); + $ical = new iCalComponent($data); + + $vtodos = $ical->GetComponents("VTODO"); + //Should only loop once + foreach ($vtodos as $vtodo) { + $message = $this->_ParseVTodoToSyncObject($vtodo, $message, $truncsize); + } + return $message; + } + + /** + * Parse 1 VEvent + * @param ical_vtodo $vtodo + * @param SyncAppointment(Exception) $message + * @param int $truncsize + */ + private function _ParseVTodoToSyncObject($vtodo, $message, $truncsize) { + //Default + $message->reminderset = "0"; + $message->importance = "1"; + $message->complete = "0"; + + $properties = $vtodo->GetProperties(); + foreach ($properties as $property) { + switch ($property->Name()) { + case "SUMMARY": + $message->subject = $property->Value(); + break; + + case "STATUS": + switch ($property->Value()) { + case "NEEDS-ACTION": + case "IN-PROCESS": + $message->complete = "0"; + break; + case "COMPLETED": + case "CANCELLED": + $message->complete = "1"; + break; + } + break; + + case "COMPLETED": + $message->datecompleted = Utils::MakeUTCDate($property->Value()); + break; + + case "DUE": + $message->utcduedate = Utils::MakeUTCDate($property->Value()); + break; + + case "PRIORITY": + $priority = $property->Value(); + if ($priority <= 3) + $message->importance = "0"; + if ($priority <= 6) + $message->importance = "1"; + if ($priority > 6) + $message->importance = "2"; + break; + + case "RRULE": + $message->recurrence = $this->_ParseRecurrence($property->Value(), "vtodo"); + break; + + case "CLASS": + switch ($property->Value()) { + case "PUBLIC": + $message->sensitivity = "0"; + break; + case "PRIVATE": + $message->sensitivity = "2"; + break; + case "CONFIDENTIAL": + $message->sensitivity = "3"; + break; + } + break; + + case "DTSTART": + $message->utcstartdate = Utils::MakeUTCDate($property->Value()); + break; + + case "SUMMARY": + $message->subject = $property->Value(); + break; + + case "CATEGORIES": + $categories = explode(",", $property->Value()); + $message->categories = $categories; + break; + } + } + + if (isset($message->recurrence)) { + $message->recurrence->start = $message->utcstartdate; + } + + $valarm = current($vtodo->GetComponents("VALARM")); + if ($valarm) { + $properties = $valarm->GetProperties(); + foreach ($properties as $property) { + if ($property->Name() == "TRIGGER") { + $parameters = $property->Parameters(); + if (array_key_exists("VALUE", $parameters) && $parameters["VALUE"] == "DATE-TIME") { + $message->remindertime = Utils::MakeUTCDate($property->Value()); + $message->reminderset = "1"; + } + elseif (!array_key_exists("VALUE", $parameters) || $parameters["VALUE"] == "DURATION") { + $val = str_replace("-", "", $property->Value()); + $interval = new DateInterval($val); + $start = date_create("@" . $message->utcstartdate); + $message->remindertime = date_timestamp_get(date_sub($start, $interval)); + $message->reminderset = "1"; + } + } + } + } + return $message; + } + + /** + * Generate a VTODO from a SyncAppointment(Exception) + * @param string $data + * @param string $id + * @return iCalComponent + */ + private function _ParseASTaskToVTodo($data, $id) { + $vtodo = new iCalComponent(); + $vtodo->SetType("VTODO"); + + if (isset($data->body)) { + $vtodo->AddProperty("DESCRIPTION", $data->body); + } + if (isset($data->asbody->data)) { + if (isset($data->nativebodytype) && $data->nativebodytype == SYNC_BODYPREFERENCE_RTF) { + $rtfparser = new rtf(); + $rtfparser->loadrtf(base64_decode($data->asbody->data)); + $rtfparser->output("ascii"); + $rtfparser->parse(); + $vtodo->AddProperty("DESCRIPTION", $rtfparser->out); + } + else { + $vtodo->AddProperty("DESCRIPTION", $data->asbody->data); + } + } + if (isset($data->complete)) { + if ($data->complete == "0") { + $vtodo->AddProperty("STATUS", "NEEDS-ACTION"); + } + else { + $vtodo->AddProperty("STATUS", "COMPLETED"); + } + } + if (isset($data->datecompleted)) { + $vtodo->AddProperty("COMPLETED", gmdate("Ymd\THis\Z", $data->datecompleted)); + } + if ($data->utcduedate) { + $vtodo->AddProperty("DUE", gmdate("Ymd\THis\Z", $data->utcduedate)); + } + if (isset($data->importance)) { + if ($data->importance == "1") { + $vtodo->AddProperty("PRIORITY", 6); + } + elseif ($data->importance == "2") { + $vtodo->AddProperty("PRIORITY", 9); + } + else { + $vtodo->AddProperty("PRIORITY", 1); + } + } + if (isset($data->recurrence)) { + $vtodo->AddProperty("RRULE", $this->_GenerateRecurrence($data->recurrence)); + } + if ($data->reminderset && $data->remindertime) { + $valarm = new iCalComponent(); + $valarm->SetType("VALARM"); + $valarm->AddProperty("ACTION", "DISPLAY"); + $valarm->AddProperty("TRIGGER;VALUE=DATE-TIME", gmdate("Ymd\THis\Z", $data->remindertime)); + $vtodo->AddComponent($valarm); + } + if (isset($data->sensitivity)) { + switch ($data->sensitivity) { + case "0": + $vtodo->AddProperty("CLASS", "PUBLIC"); + break; + + case "2": + $vtodo->AddProperty("CLASS", "PRIVATE"); + break; + + case "3": + $vtodo->AddProperty("CLASS", "CONFIDENTIAL"); + break; + } + } + if (isset($data->utcstartdate)) { + $vtodo->AddProperty("DTSTART", gmdate("Ymd\THis\Z", $data->utcstartdate)); + } + if (isset($data->subject)) { + $vtodo->AddProperty("SUMMARY", $data->subject); + } + if (isset($data->rtf)) { + $rtfparser = new rtf(); + $rtfparser->loadrtf(base64_decode($data->rtf)); + $rtfparser->output("ascii"); + $rtfparser->parse(); + $vtodo->AddProperty("DESCRIPTION", $rtfparser->out); + } + if (isset($data->categories) && is_array($data->categories)) { + $vtodo->AddProperty("CATEGORIES", implode(",", $data->categories)); + } + + return $vtodo; + } + + private function _GetDateFromUTC($format, $date, $tz_str) { + $timezone = $this->_GetTimezoneFromString($tz_str); + $dt = date_create('@' . $date); + date_timezone_set($dt, timezone_open($timezone)); + return date_format($dt, $format); + } + + //This returns a timezone that matches the timezonestring. + //We can't be sure this is the one you chose, as multiple timezones have same timezonestring + private function _GetTimezoneFromString($tz_string) { + //Get a list of all timezones + $identifiers = DateTimeZone::listIdentifiers(); + //Try the default timezone first + array_unshift($identifiers, date_default_timezone_get()); + foreach ($identifiers as $tz) { + $str = $this->_GetTimezoneString($tz, false); + if ($str == $tz_string) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->_GetTimezoneFromString(): Found timezone: '%s'.", $tz)); + return $tz; + } + } + return date_default_timezone_get(); + } + + /** + * Generate ActiveSync Timezone Packed String. + * @param string $timezone + * @param string $with_names + * @throws Exception + */ + private function _GetTimezoneString($timezone, $with_names = true) { + // UTC needs special handling + if ($timezone == "UTC") + return base64_encode(pack('la64vvvvvvvvla64vvvvvvvvl', 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0)); + try { + //Generate a timezone string (PHP 5.3 needed for this) + $timezone = new DateTimeZone($timezone); + $trans = $timezone->getTransitions(time()); + $stdTime = null; + $dstTime = null; + if (count($trans) < 3) { + throw new Exception(); + } + if ($trans[1]['isdst'] == 1) { + $dstTime = $trans[1]; + $stdTime = $trans[2]; + } + else { + $dstTime = $trans[2]; + $stdTime = $trans[1]; + } + $stdTimeO = new DateTime($stdTime['time']); + $stdFirst = new DateTime(sprintf("first sun of %s %s", $stdTimeO->format('F'), $stdTimeO->format('Y')), timezone_open("UTC")); + $stdBias = $stdTime['offset'] / -60; + $stdName = $stdTime['abbr']; + $stdYear = 0; + $stdMonth = $stdTimeO->format('n'); + $stdWeek = floor(($stdTimeO->format("j")-$stdFirst->format("j"))/7)+1; + $stdDay = $stdTimeO->format('w'); + $stdHour = $stdTimeO->format('H'); + $stdMinute = $stdTimeO->format('i'); + $stdTimeO->add(new DateInterval('P7D')); + if ($stdTimeO->format('n') != $stdMonth) { + $stdWeek = 5; + } + $dstTimeO = new DateTime($dstTime['time']); + $dstFirst = new DateTime(sprintf("first sun of %s %s", $dstTimeO->format('F'), $dstTimeO->format('Y')), timezone_open("UTC")); + $dstName = $dstTime['abbr']; + $dstYear = 0; + $dstMonth = $dstTimeO->format('n'); + $dstWeek = floor(($dstTimeO->format("j")-$dstFirst->format("j"))/7)+1; + $dstDay = $dstTimeO->format('w'); + $dstHour = $dstTimeO->format('H'); + $dstMinute = $dstTimeO->format('i'); + $dstTimeO->add(new DateInterval('P7D')); + if ($dstTimeO->format('n') != $dstMonth) { + $dstWeek = 5; + } + $dstBias = ($dstTime['offset'] - $stdTime['offset']) / -60; + if ($with_names) { + return base64_encode(pack('la64vvvvvvvvla64vvvvvvvvl', $stdBias, $stdName, 0, $stdMonth, $stdDay, $stdWeek, $stdHour, $stdMinute, 0, 0, 0, $dstName, 0, $dstMonth, $dstDay, $dstWeek, $dstHour, $dstMinute, 0, 0, $dstBias)); + } + else { + return base64_encode(pack('la64vvvvvvvvla64vvvvvvvvl', $stdBias, '', 0, $stdMonth, $stdDay, $stdWeek, $stdHour, $stdMinute, 0, 0, 0, '', 0, $dstMonth, $dstDay, $dstWeek, $dstHour, $dstMinute, 0, 0, $dstBias)); + } + } + catch (Exception $e) { + // If invalid timezone is given, we return UTC + return base64_encode(pack('la64vvvvvvvvla64vvvvvvvvl', 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0)); + } + return base64_encode(pack('la64vvvvvvvvla64vvvvvvvvl', 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0)); + } +} + +?> diff --git a/sources/backend/caldav/config.php b/sources/backend/caldav/config.php new file mode 100644 index 0000000..3be2478 --- /dev/null +++ b/sources/backend/caldav/config.php @@ -0,0 +1,67 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// ************************ +// BackendCalDAV settings +// ************************ + +// Server address +define('CALDAV_SERVER', 'http://calendar.domain.com'); + +// Port +define('CALDAV_PORT', '80'); + +// Path +define('CALDAV_PATH', '/caldav.php/%u/'); + +// Default CalDAV folder (calendar folder/principal). This will be marked as the default calendar in the mobile +define('CALDAV_PERSONAL', 'home'); + +// If the CalDAV server supports the sync-collection operation +// DAViCal, SOGo and SabreDav support it +// SabreDav version must be at least 1.9.0, otherwise set this to false +// Setting this to false will work with most servers, but it will be slower +define('CALDAV_SUPPORTS_SYNC', false); + + +?> \ No newline at end of file diff --git a/sources/backend/carddav/README b/sources/backend/carddav/README new file mode 100644 index 0000000..e0228d4 --- /dev/null +++ b/sources/backend/carddav/README @@ -0,0 +1,12 @@ +This is a CardDAV backend based in the vcarddir backend. + +It supports DAViCal, Sogo, OwnCloud, SabreDav... and should works with any carddav server. So if it doesn't work with your server, please open a issue. + +It supports ChangesSink method that will detect and send faster changes to your device. + +DAViCal implements the SYNC operation, it's a very fast method to detect changes in your vcards. +The others servers don't implement it, so the code will fallback to a slower method (suggest your carddav server developers to implement it!!). + +This is controlled with a flag in the config.php file. + +Also, it can autodetect multiple addressbooks and will present them to the mobile device as an unique addressbook (only iOS supports multiple addressbook). diff --git a/sources/backend/carddav/REQUIREMENTS b/sources/backend/carddav/REQUIREMENTS new file mode 100644 index 0000000..ada7371 --- /dev/null +++ b/sources/backend/carddav/REQUIREMENTS @@ -0,0 +1,5 @@ +REQUIREMENTS: +php-curl +php-xsl + +CardDAV server (DAViCal, Sabredav, Sogo, Owncloud...) \ No newline at end of file diff --git a/sources/backend/carddav/THANKS b/sources/backend/carddav/THANKS new file mode 100644 index 0000000..f213642 --- /dev/null +++ b/sources/backend/carddav/THANKS @@ -0,0 +1,8 @@ +*Drenalina SRL (www.drenalina.com)* sponsored the development of the following features in the BackendCardDAV, any existing bug it's my fault not theirs ;-) +Thank you very much for helping to improve it!! + + + - Autodetecting addressbooks within a DAV principal. + - Merging multiple addressbooks so the device will see a unique one. Only iOS based devices support multiple addressbooks, so we will merge them for now. + - Selecting default addressbook to store new contacts created from the device. + - GAL addressbook and GAL search. \ No newline at end of file diff --git a/sources/backend/carddav/carddav.php b/sources/backend/carddav/carddav.php new file mode 100644 index 0000000..afe15a0 --- /dev/null +++ b/sources/backend/carddav/carddav.php @@ -0,0 +1,1452 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// config file +require_once("backend/carddav/config.php"); + +include_once('lib/default/diffbackend/diffbackend.php'); +include_once('include/z_carddav.php'); + +class BackendCardDAV extends BackendDiff implements ISearchProvider { + + private $domain = ''; + private $username = ''; + private $url = null; + private $server = null; + private $default_url = null; + private $gal_url = null; + + // Android only supports synchronizing 1 AddressBook per account, this is the foldername for Z-Push + private $foldername = "contacts"; + + // We can have multiple addressbooks, but the mobile device will only see one (all of them merged) + private $addressbooks; + + private $changessinkinit; + private $contactsetag; + private $sinkdata; + + /** + * Constructor + * + */ + public function BackendCardDAV() { + if (!function_exists("curl_init")) { + throw new FatalException("BackendCardDAV(): php-curl is not found", 0, null, LOGLEVEL_FATAL); + } + + $this->addressbooks = array(); + $this->changessinkinit = false; + $this->contactsetag = array(); + $this->sinkdata = array(); + } + + /** + * Authenticates the user - NOT EFFECTIVELY IMPLEMENTED + * Normally some kind of password check would be done here. + * Alternatively, the password could be ignored and an Apache + * authentication via mod_auth_* could be done + * + * @param string $username + * @param string $domain + * @param string $password + * + * @access public + * @return boolean + */ + public function Logon($username, $domain, $password) { + $this->url = CARDDAV_PROTOCOL . '://' . CARDDAV_SERVER . ':' . CARDDAV_PORT . str_replace("%d", $domain, str_replace("%u", $username, CARDDAV_PATH)); + $this->default_url = CARDDAV_PROTOCOL . '://' . CARDDAV_SERVER . ':' . CARDDAV_PORT . str_replace("%d", $domain, str_replace("%u", $username, CARDDAV_DEFAULT_PATH)); + if (defined('CARDDAV_GAL_PATH')) { + $this->gal_url = CARDDAV_PROTOCOL . '://' . CARDDAV_SERVER . ':' . CARDDAV_PORT . str_replace("%d", $domain, str_replace("%u", $username, CARDDAV_GAL_PATH)); + } + else { + $this->gal_url = false; + } + $this->server = new carddav_backend($this->url, CARDDAV_URL_VCARD_EXTENSION); + $this->server->set_auth($username, $password); + + if (($connected = $this->server->check_connection())) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->Logon(): User '%s' is authenticated on '%s'", $username, $this->url)); + $this->username = $username; + $this->domain = $domain; + + // Autodiscover all the addressbooks + $this->discoverAddressbooks(); + } + else { + //TODO: get error message + $error = ''; + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->Logon(): User '%s' failed to authenticate on '%s': %s", $username, $this->url, $error)); + $this->server = null; + } + + return $connected; + } + + /** + * Logs off + * + * @access public + * @return boolean + */ + public function Logoff() { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->Logoff()")); + $this->server = null; + + $this->SaveStorages(); + + unset($this->contactsetag); + unset($this->sinkdata); + unset($this->addressbooks); + + return true; + } + + /** + * Sends an e-mail + * Not implemented here + * + * @param SyncSendMail $sm SyncSendMail object + * + * @access public + * @return boolean + * @throws StatusException + */ + public function SendMail($sm) { + return false; + } + + /** + * Returns the waste basket + * Not implemented here + * + * @access public + * @return string + */ + public function GetWasteBasket() { + return false; + } + + /** + * Returns the content of the named attachment as stream + * Not implemented here + * + * @param string $attname + * + * @access public + * @return SyncItemOperationsAttachment + * @throws StatusException + */ + public function GetAttachmentData($attname) { + return false; + } + + /** + * Indicates if the backend has a ChangesSink. + * A sink is an active notification mechanism which does not need polling. + * The CardDAV backend simulates a sink by polling revision dates from the vcards + * + * @access public + * @return boolean + */ + public function HasChangesSink() { + return true; + } + + /** + * The folder should be considered by the sink. + * Folders which were not initialized should not result in a notification + * of IBackend->ChangesSink(). + * + * @param string $folderid + * + * @access public + * @return boolean false if found can not be found + */ + public function ChangesSinkInitialize($folderid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangesSinkInitialize(): folderid '%s'", $folderid)); + + + + // We don't need the actual cards, we only need to get the changes since this moment + $init_ok = true; + foreach ($this->addressbooks as $addressbook) { + try { + $this->server->set_url($addressbook); + $this->sinkdata[$addressbook] = $this->server->do_sync(true, false, CARDDAV_SUPPORTS_SYNC); + } + catch (Exception $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangesSinkInitialize - Error doing the initial sync for '%s': %s", $addressbook, $ex->getMessage())); + $init_ok = false; + } + + if ($this->sinkdata[$addressbook] === false) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangesSinkInitialize - Error initializing the sink for '%s'", $addressbook)); + $init_ok = false; + } + + if (CARDDAV_SUPPORTS_SYNC) { + // we don't need to store the sinkdata if the carddav server supports native sync + unset($this->sinkdata[$addressbook]); + } + } + + $this->changessinkinit = $init_ok; + + return $this->changessinkinit; + } + + /** + * The actual ChangesSink. + * For max. the $timeout value this method should block and if no changes + * are available return an empty array. + * If changes are available a list of folderids is expected. + * + * @param int $timeout max. amount of seconds to block + * + * @access public + * @return array + */ + public function ChangesSink($timeout = 30) { + $notifications = array(); + $stopat = time() + $timeout - 1; + $changed = false; + + //We can get here and the ChangesSink not be initialized yet + if (!$this->changessinkinit) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangesSink - Not initialized ChangesSink, sleep and exit")); + // We sleep and do nothing else + sleep($timeout); + return $notifications; + } + + while($stopat > time() && empty($notifications)) { + foreach ($this->addressbooks as $addressbook) { + $vcards = false; + try { + $this->server->set_url($addressbook); + $vcards = $this->server->do_sync(false, false, CARDDAV_SUPPORTS_SYNC); + } + catch (Exception $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangesSink - Error resyncing vcards: %s", $ex->getMessage())); + } + + if ($vcards === false) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangesSink - Error getting the changes")); + return false; + } + else { + $xml_vcards = new SimpleXMLElement($vcards); + + if (CARDDAV_SUPPORTS_SYNC) { + if (count($xml_vcards->element) > 0) { + $changed = true; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangesSink - Changes detected")); + } + } + else { + $xml_sinkdata = new SimpleXMLElement($this->sinkdata[$addressbook]); + if (count($xml_vcards->element) != count($xml_sinkdata->element)) { + // If the number of cards is different, we know for sure, there are changes + $changed = true; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangesSink - Changes detected")); + } + else { + // If it's the same we need to check vcard to vcard, or the original strings + if (strcmp($this->sinkdata[$addressbook], $vcards) != 0) { + $changed = true; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangesSink - Changes detected")); + } + } + unset($xml_sinkdata); + } + + unset($vcards); + unset($xml_vcards); + } + + if ($changed) { + $notifications[] = $this->foldername; + } + } + + if (empty($notifications)) + sleep(5); + } + + return $notifications; + } + + /**---------------------------------------------------------------------------------------------------------- + * implemented DiffBackend methods + */ + + /** + * Returns a list (array) of folders. + * In simple implementations like this one, probably just one folder is returned. + * + * @access public + * @return array + */ + public function GetFolderList() { + ZLog::Write(LOGLEVEL_DEBUG, 'BackendCardDAV::GetFolderList()'); + + // The mobile will only see one + $addressbooks = array(); + $addressbook = $this->StatFolder($this->foldername); + $addressbooks[] = $addressbook; + + return $addressbooks; + } + + /** + * Returns an actual SyncFolder object + * + * @param string $id id of the folder + * + * @access public + * @return object SyncFolder with information + */ + public function GetFolder($id) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::GetFolder('%s')", $id)); + + $addressbook = false; + + if ($id == $this->foldername) { + $addressbook = new SyncFolder(); + $addressbook->serverid = $id; + $addressbook->parentid = "0"; + $addressbook->displayname = str_replace("%d", $this->domain, str_replace("%u", $this->username, CARDDAV_CONTACTS_FOLDER_NAME)); + $addressbook->type = SYNC_FOLDER_TYPE_CONTACT; + } + + return $addressbook; + } + + /** + * Returns folder stats. An associative array with properties is expected. + * + * @param string $id id of the folder + * + * @access public + * @return array + */ + public function StatFolder($id) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::StatFolder('%s')", $id)); + + $addressbook = $this->GetFolder($id); + + $stat = array(); + $stat["id"] = $id; + $stat["parent"] = $addressbook->parentid; + $stat["mod"] = $addressbook->displayname; + + return $stat; + } + + /** + * Creates or modifies a folder + * Not implemented here + * + * @param string $folderid id of the parent folder + * @param string $oldid if empty -> new folder created, else folder is to be renamed + * @param string $displayname new folder name (to be created, or to be renamed to) + * @param int $type folder type + * + * @access public + * @return boolean status + * @throws StatusException could throw specific SYNC_FSSTATUS_* exceptions + * + */ + public function ChangeFolder($folderid, $oldid, $displayname, $type) { + return false; + } + + /** + * Deletes a folder + * Not implemented here + * + * @param string $id + * @param string $parent is normally false + * + * @access public + * @return boolean status - false if e.g. does not exist + * @throws StatusException could throw specific SYNC_FSSTATUS_* exceptions + * + */ + public function DeleteFolder($id, $parentid) { + return false; + } + + /** + * Returns a list (array) of messages + * + * @param string $folderid id of the parent folder + * @param long $cutoffdate timestamp in the past from which on messages should be returned + * + * @access public + * @return array/false array with messages or false if folder is not available + */ + public function GetMessageList($folderid, $cutoffdate) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->GetMessageList('%s', '%s')", $folderid, $cutoffdate)); + + $messages = array(); + + foreach ($this->addressbooks as $addressbook) { + $addressbookId = $this->convertAddressbookUrl($addressbook); + + $vcards = false; + try { + // We don't need the actual vcards here, we only need a list of all them + // This petition is always "initial", and we don't "include_vcards" + $this->server->set_url($addressbook); + $vcards = $this->server->do_sync(true, false, CARDDAV_SUPPORTS_SYNC); + } + catch (Exception $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->GetMessageList - Error getting the vcards in '%s': %s", $addressbook, $ex->getMessage())); + } + + if ($vcards === false) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->GetMessageList - Error getting the vcards")); + } + else { + $xml_vcards = new SimpleXMLElement($vcards); + foreach ($xml_vcards->element as $vcard) { + $id = $addressbookId . "-" . $vcard->id->__toString(); + $this->contactsetag[$id] = $vcard->etag->__toString(); + $messages[] = $this->StatMessage($folderid, $id); + } + } + } + + return $messages; + } + + /** + * Returns the actual SyncXXX object type. + * + * @param string $folderid id of the parent folder + * @param string $id id of the message + * @param ContentParameters $contentparameters parameters of the requested message (truncation, mimesupport etc) + * + * @access public + * @return object/false false if the message could not be retrieved + */ + public function GetMessage($folderid, $id, $contentparameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->GetMessage('%s', '%s')", $folderid, $id)); + + $message = false; + $addressbookId = $this->getAddressbookIdFromVcard($id); + $vcardId = $this->getVcardId($id); + $addressbookUrl = $this->getAddressbookFromId($addressbookId); + + if ($addressbookUrl !== false) { + $xml_vcard = false; + try { + $this->server->set_url($addressbookUrl); + $xml_vcard = $this->server->get_xml_vcard($vcardId); + } + catch (Exception $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->GetMessage - Error getting vcard '%s' in '%s': %s", $vcardId, $addressbookId, $ex->getMessage())); + } + + if ($xml_vcard !== false) { + $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation()); + $xml_data = new SimpleXMLElement($xml_vcard); + $message = $this->ParseFromVCard($xml_data->element[0]->vcard->__toString(), $truncsize); + } + } + + if ($message === false) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->GetMessage(): vCard not found")); + } + + return $message; + } + + + /** + * Returns message stats, analogous to the folder stats from StatFolder(). + * + * @param string $folderid id of the folder + * @param string $id id of the message + * + * @access public + * @return array + */ + public function StatMessage($folderid, $id) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->StatMessage('%s', '%s')", $folderid, $id)); + + $message = array(); + if (!isset($this->contactsetag[$id])) { + $addressbookId = $this->getAddressbookIdFromVcard($id); + $vcardId = $this->getVcardId($id); + $addressbookUrl = $this->getAddressbookFromId($addressbookId); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->StatMessage - No contactsetag found, getting vcard '%s' in '%s'", $vcardId, $addressbookId)); + if ($addressbookUrl !== false) { + $xml_vcard = false; + try { + $this->server->set_url($addressbookUrl); + $xml_vcard = $this->server->get_xml_vcard($vcardId); + } + catch (Exception $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->StatMessage - Error getting vcard '%s' in '%s': %s", $vcardId, $addressbookId, $ex->getMessage())); + } + + if ($xml_vcard !== false) { + $vcard = new SimpleXMLElement($xml_vcard); + $this->contactsetag[$id] = $vcard->element[0]->etag->__toString(); + unset($vcard); + } + unset($xml_vcard); + } + } + $message["mod"] = $this->contactsetag[$id]; + $message["id"] = $id; + $message["flags"] = 1; + $message["star"] = 0; + + return $message; + } + + /** + * Called when a message has been changed on the mobile. + * This functionality is not available for emails. + * + * @param string $folderid id of the folder + * @param string $id id of the message + * @param SyncXXX $message the SyncObject containing a message + * @param ContentParameters $contentParameters + * + * @access public + * @return array same return value as StatMessage() + * @throws StatusException could throw specific SYNC_STATUS_* exceptions + */ + public function ChangeMessage($folderid, $id, $message, $contentParameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangeMessage('%s', '%s')", $folderid, $id)); + + $vcard_text = $this->ParseToVCard($message); + + if ($vcard_text === false) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangeMessage - Error converting message to vCard")); + } + else { + ZLog::Write(LOGLEVEL_WBXML, sprintf("BackendCardDAV->ChangeMessage - vCard\n%s\n", $vcard_text)); + + $updated = false; + if (strlen($id) == 0) { + //no id, new vcard + try { + $addressbookId = $this->getAddressbookFromUrl($this->default_url); + if ($addressbookId === false) { + $addressbookId = $this->getAddressbookFromUrl($this->addressbooks[0]); + $this->server->set_url($this->addressbooks[0]); + } + else { + $this->server->set_url($this->default_url); + } + + $updated = $this->server->add($vcard_text); + if ($updated !== false) { + $id = $addressbookId . "-" . $updated; + } + } + catch (Exception $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangeMessage - Error adding vcard '%s' : %s", $id, $ex->getMessage())); + } + } + else { + //id, update vcard + + $vcardId = $this->getVcardId($id); + $addressbookUrl = $this->getAddressbookFromId($this->getAddressbookIdFromVcard($id)); + + if ($addressbookUrl !== false) { + try { + $this->server->set_url($addressbookUrl); + $updated = $this->server->update($vcard_text, $vcardId); + } + catch (Exception $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangeMessage - Error updating vcard '%s' : %s", $id, $ex->getMessage())); + } + } + } + + if ($updated !== false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangeMessage - vCard updated")); + } + else { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangeMessage - vCard not updated")); + } + } + + return $this->StatMessage($folderid, $id); + } + + /** + * Changes the 'read' flag of a message on disk + * Not implemented here + * + * @param string $folderid id of the folder + * @param string $id id of the message + * @param int $flags read flag of the message + * @param ContentParameters $contentParameters + * + * @access public + * @return boolean status of the operation + * @throws StatusException could throw specific SYNC_STATUS_* exceptions + */ + public function SetReadFlag($folderid, $id, $flags, $contentParameters) { + return false; + } + + /** + * Changes the 'star' flag of a message on disk + * Not implemented here + * + * @param string $folderid id of the folder + * @param string $id id of the message + * @param int $flags star flag of the message + * @param ContentParameters $contentParameters + * + * @access public + * @return boolean status of the operation + * @throws StatusException could throw specific SYNC_STATUS_* exceptions + */ + public function SetStarFlag($folderid, $id, $flags, $contentParameters) { + return false; + } + + /** + * Called when the user has requested to delete (really delete) a message + * + * @param string $folderid id of the folder + * @param string $id id of the message + * @param ContentParameters $contentParameters + * + * @access public + * @return boolean status of the operation + * @throws StatusException could throw specific SYNC_STATUS_* exceptions + */ + public function DeleteMessage($folderid, $id, $contentParameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->DeleteMessage('%s', '%s')", $folderid, $id)); + + $deleted = false; + + $vcardId = $this->getVcardId($id); + $addressbookUrl = $this->getAddressbookFromId($this->getAddressbookIdFromVcard($id)); + + if ($addressbookUrl !== false) { + try { + $this->server->set_url($addressbookUrl); + $deleted = $this->server->delete($vcardId); + } + catch (Exception $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->DeleteMessage - Error deleting vcard: %s", $ex->getMessage())); + } + } + + if ($deleted) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->DeleteMessage - vCard deleted")); + } + else { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->DeleteMessage - cannot delete vCard")); + } + + return $deleted; + } + + /** + * Called when the user moves an item on the PDA from one folder to another + * Not implemented here + * + * @param string $folderid id of the source folder + * @param string $id id of the message + * @param string $newfolderid id of the destination folder + * @param ContentParameters $contentParameters + * + * @access public + * @return boolean status of the operation + * @throws StatusException could throw specific SYNC_MOVEITEMSSTATUS_* exceptions + */ + public function MoveMessage($folderid, $id, $newfolderid, $contentParameters) { + return false; + } + + + /** + * Indicates which AS version is supported by the backend. + * + * @access public + * @return string AS version constant + */ + public function GetSupportedASVersion() { + return ZPush::ASV_14; + } + + + /** + * Returns the BackendCardDAV as it implements the ISearchProvider interface + * This could be overwritten by the global configuration + * + * @access public + * @return object Implementation of ISearchProvider + */ + public function GetSearchProvider() { + return $this; + } + + + /**---------------------------------------------------------------------------------------------------------- + * public ISearchProvider methods + */ + + /** + * Indicates if a search type is supported by this SearchProvider + * Currently only the type ISearchProvider::SEARCH_GAL (Global Address List) is implemented + * + * @param string $searchtype + * + * @access public + * @return boolean + */ + public function SupportsType($searchtype) { + if ($this->gal_url !== false) { + return ($searchtype == ISearchProvider::SEARCH_GAL); + } + else { + return false; + } + } + + + /** + * Queries the CardDAV backend + * + * @param string $searchquery string to be searched for + * @param string $searchrange specified searchrange + * + * @access public + * @return array search results + */ + public function GetGALSearchResults($searchquery, $searchrange) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->GetGALSearchResults(%s, %s)", $searchquery, $searchrange)); + if ($this->gal_url !== false && $this->server !== false) { + // Don't search if the length is < 5, we are typing yet + if (strlen($searchquery) < CARDDAV_GAL_MIN_LENGTH) { + return false; + } + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->GetGALSearchResults searching: %s", $this->url)); + try { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->GetGALSearchResults server is null? %d", $this->server == null)); + $this->server->set_url($this->gal_url); + $vcards = $this->server->search_vcards(str_replace("<", "", str_replace(">", "", $searchquery)), 15, true, false, + defined('CARDDAV_SUPPORTS_FN_SEARCH') ? CARDDAV_SUPPORTS_FN_SEARCH : false); + } + catch (Exception $e) { + $vcards = false; + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->GetGALSearchResults : Error in search %s", $e->getMessage())); + } + if ($vcards === false) { + ZLog::Write(LOGLEVEL_ERROR, "BackendCardDAV->GetGALSearchResults : Error in search query. Search aborted"); + return false; + } + + $xml_vcards = new SimpleXMLElement($vcards); + unset($vcards); + + // range for the search results, default symbian range end is 50, wm 99, + // so we'll use that of nokia + $rangestart = 0; + $rangeend = 50; + + if ($searchrange != '0') { + $pos = strpos($searchrange, '-'); + $rangestart = substr($searchrange, 0, $pos); + $rangeend = substr($searchrange, ($pos + 1)); + } + $items = array(); + + // TODO the limiting of the searchresults could be refactored into Utils as it's probably used more than once + $querycnt = $xml_vcards->count(); + //do not return more results as requested in range + $querylimit = (($rangeend + 1) < $querycnt) ? ($rangeend + 1) : $querycnt == 0 ? 1 : $querycnt; + $items['range'] = $rangestart.'-'.($querylimit - 1); + $items['searchtotal'] = $querycnt; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->GetGALSearchResults : %s entries found, returning %s to %s", $querycnt, $rangestart, $querylimit)); + + $i = 0; + $rc = 0; + foreach ($xml_vcards->element as $xml_vcard) { + if ($i >= $rangestart && $i < $querylimit) { + $contact = $this->ParseFromVCard($xml_vcard->vcard->__toString()); + if ($contact === false) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->GetGALSearchResults : error converting vCard to AS contact\n%s\n", $xml_vcard->vcard->__toString())); + } + else { + $items[$rc][SYNC_GAL_EMAILADDRESS] = $contact->email1address; + if (isset($contact->fileas)) { + $items[$rc][SYNC_GAL_DISPLAYNAME] = $contact->fileas; + } + else if (isset($contact->firstname) || isset($contact->middlename) || isset($contact->lastname)) { + $items[$rc][SYNC_GAL_DISPLAYNAME] = $contact->firstname . (isset($contact->middlename) ? " " . $contact->middlename : "") . (isset($contact->lastname) ? " " . $contact->lastname : ""); + } + else { + $items[$rc][SYNC_GAL_DISPLAYNAME] = $contact->email1address; + } + if (isset($contact->firstname)) { + $items[$rc][SYNC_GAL_FIRSTNAME] = $contact->firstname; + } + else { + $items[$rc][SYNC_GAL_FIRSTNAME] = ""; + } + if (isset($contact->lastname)) { + $items[$rc][SYNC_GAL_LASTNAME] = $contact->lastname; + } + else { + $items[$rc][SYNC_GAL_LASTNAME] = ""; + } + if (isset($contact->business2phonenumber)) { + $items[$rc][SYNC_GAL_PHONE] = $contact->business2phonenumber; + } + if (isset($contact->home2phonenumber)) { + $items[$rc][SYNC_GAL_HOMEPHONE] = $contact->home2phonenumber; + } + if (isset($contact->mobilephonenumber)) { + $items[$rc][SYNC_GAL_MOBILEPHONE] = $contact->mobilephonenumber; + } + if (isset($contact->title)) { + $items[$rc][SYNC_GAL_TITLE] = $contact->title; + } + if (isset($contact->companyname)) { + $items[$rc][SYNC_GAL_COMPANY] = $contact->companyname; + } + if (isset($contact->department)) { + $items[$rc][SYNC_GAL_OFFICE] = $contact->department; + } + if (isset($contact->nickname)) { + $items[$rc][SYNC_GAL_ALIAS] = $contact->nickname; + } + unset($contact); + $rc++; + } + } + $i++; + } + + unset($xml_vcards); + return $items; + } + else { + unset($xml_vcards); + return false; + } + } + + /** + * Searches for the emails on the server + * + * @param ContentParameter $cpo + * + * @return array + */ + public function GetMailboxSearchResults($cpo) { + return false; + } + + /** + * Terminates a search for a given PID + * + * @param int $pid + * + * @return boolean + */ + public function TerminateSearch($pid) { + return true; + } + + /** + * Disconnects from CardDAV + * + * @access public + * @return boolean + */ + public function Disconnect() { + return true; + } + + + /**---------------------------------------------------------------------------------------------------------- + * private vcard-specific internals + */ + + + /** + * Escapes a string + * + * @param string $data string to be escaped + * + * @access private + * @return string + */ + private function escape($data) { + if (is_array($data)) { + foreach ($data as $key => $val) { + $data[$key] = $this->escape($val); + } + return $data; + } + $data = str_replace("\r\n", "\n", $data); + $data = str_replace("\r", "\n", $data); + $data = str_replace(array('\\', ';', ',', "\n"), array('\\\\', '\\;', '\\,', '\\n'), $data); + return $data; + } + + /** + * Un-escapes a string + * + * @param string $data string to be un-escaped + * + * @access private + * @return string + */ + private function unescape($data) { + $data = str_replace(array('\\\\', '\\;', '\\,', '\\n','\\N'),array('\\', ';', ',', "\n", "\n"),$data); + return $data; + } + + /** + * Converts the vCard into SyncContact + * + * @param string $data string with the vcard + * @param int $truncsize truncate size requested + * @return SyncContact + */ + private function ParseFromVCard($data, $truncsize = -1) { + ZLog::Write(LOGLEVEL_WBXML, sprintf("BackendCardDAV->ParseFromVCard : vCard\n%s\n", $data)); + + $types = array ('dom' => 'type', 'intl' => 'type', 'postal' => 'type', 'parcel' => 'type', 'home' => 'type', 'work' => 'type', + 'pref' => 'type', 'voice' => 'type', 'fax' => 'type', 'msg' => 'type', 'cell' => 'type', 'pager' => 'type', + 'bbs' => 'type', 'modem' => 'type', 'car' => 'type', 'isdn' => 'type', 'video' => 'type', + 'aol' => 'type', 'applelink' => 'type', 'attmail' => 'type', 'cis' => 'type', 'eworld' => 'type', + 'internet' => 'type', 'ibmmail' => 'type', 'mcimail' => 'type', + 'powershare' => 'type', 'prodigy' => 'type', 'tlx' => 'type', 'x400' => 'type', + 'gif' => 'type', 'cgm' => 'type', 'wmf' => 'type', 'bmp' => 'type', 'met' => 'type', 'pmb' => 'type', 'dib' => 'type', + 'pict' => 'type', 'tiff' => 'type', 'pdf' => 'type', 'ps' => 'type', 'jpeg' => 'type', 'qtime' => 'type', + 'mpeg' => 'type', 'mpeg2' => 'type', 'avi' => 'type', + 'wave' => 'type', 'aiff' => 'type', 'pcm' => 'type', + 'x509' => 'type', 'pgp' => 'type', 'text' => 'value', 'inline' => 'value', 'url' => 'value', 'cid' => 'value', 'content-id' => 'value', + '7bit' => 'encoding', '8bit' => 'encoding', 'quoted-printable' => 'encoding', 'base64' => 'encoding', + ); + + // Parse the vcard + $message = new SyncContact(); + + $data = str_replace("\x00", '', $data); + $data = str_replace("\r\n", "\n", $data); + $data = str_replace("\r", "\n", $data); + $data = preg_replace('/(\n)([ \t])/i', '', $data); + + $lines = explode("\n", $data); + + $vcard = array(); + foreach ($lines as $line) { + if (trim($line) == '') + continue; + $pos = strpos($line, ':'); + if ($pos === false) + continue; + + $field = trim(substr($line, 0, $pos)); + $value = trim(substr($line, $pos+1)); + + $fieldparts = preg_split('/(? $v) { + $val[$i] = quoted_printable_decode($v); + } + break; + case 'b': + case 'base64': + foreach ($val as $i => $v) { + $val[$i] = base64_decode($v); + } + break; + } + } + else { + foreach ($val as $i => $v) { + $val[$i] = $this->unescape($v); + } + } + $fieldvalue['val'] = $val; + $vcard[$type][] = $fieldvalue; + } + + if (isset($vcard['email'][0]['val'][0])) + $message->email1address = $vcard['email'][0]['val'][0]; + if (isset($vcard['email'][1]['val'][0])) + $message->email2address = $vcard['email'][1]['val'][0]; + if (isset($vcard['email'][2]['val'][0])) + $message->email3address = $vcard['email'][2]['val'][0]; + + if (isset($vcard['tel'])) { + foreach ($vcard['tel'] as $tel) { + if (!isset($tel['type'])) { + $tel['type'] = array(); + } + if (in_array('car', $tel['type'])) { + $message->carphonenumber = $tel['val'][0]; + } + elseif (in_array('pager', $tel['type'])) { + $message->pagernumber = $tel['val'][0]; + } + elseif (in_array('cell', $tel['type'])) { + $message->mobilephonenumber = $tel['val'][0]; + } + elseif (in_array('home', $tel['type'])) { + if (in_array('fax', $tel['type'])) { + $message->homefaxnumber = $tel['val'][0]; + } + elseif (empty($message->homephonenumber)) { + $message->homephonenumber = $tel['val'][0]; + } + else { + $message->home2phonenumber = $tel['val'][0]; + } + } + elseif (in_array('work', $tel['type'])) { + if (in_array('fax', $tel['type'])) { + $message->businessfaxnumber = $tel['val'][0]; + } + elseif (empty($message->businessphonenumber)) { + $message->businessphonenumber = $tel['val'][0]; + } + else { + $message->business2phonenumber = $tel['val'][0]; + } + } + elseif (empty($message->homephonenumber)) { + $message->homephonenumber = $tel['val'][0]; + } + elseif (empty($message->home2phonenumber)) { + $message->home2phonenumber = $tel['val'][0]; + } + else { + $message->radiophonenumber = $tel['val'][0]; + } + } + } + //;;street;city;state;postalcode;country + if (isset($vcard['adr'])) { + foreach ($vcard['adr'] as $adr) { + if (empty($adr['type'])) { + $a = 'other'; + } + elseif (in_array('home', $adr['type'])) { + $a = 'home'; + } + elseif (in_array('work', $adr['type'])) { + $a = 'business'; + } + else { + $a = 'other'; + } + if (!empty($adr['val'][2])) { + $b=$a.'street'; + $message->$b = $adr['val'][2]; + } + if (!empty($adr['val'][3])) { + $b=$a.'city'; + $message->$b = $adr['val'][3]; + } + if (!empty($adr['val'][4])) { + $b=$a.'state'; + $message->$b = $adr['val'][4]; + } + if (!empty($adr['val'][5])) { + $b=$a.'postalcode'; + $message->$b = $adr['val'][5]; + } + if (!empty($adr['val'][6])) { + $b=$a.'country'; + $message->$b = $adr['val'][6]; + } + } + } + + if (!empty($vcard['fn'][0]['val'][0])) + $message->fileas = $vcard['fn'][0]['val'][0]; + if (!empty($vcard['n'][0]['val'][0])) + $message->lastname = $vcard['n'][0]['val'][0]; + if (!empty($vcard['n'][0]['val'][1])) + $message->firstname = $vcard['n'][0]['val'][1]; + if (!empty($vcard['n'][0]['val'][2])) + $message->middlename = $vcard['n'][0]['val'][2]; + if (!empty($vcard['n'][0]['val'][3])) + $message->title = $vcard['n'][0]['val'][3]; + if (!empty($vcard['n'][0]['val'][4])) + $message->suffix = $vcard['n'][0]['val'][4]; + if (!empty($vcard['nickname'][0]['val'][0])) + $message->nickname = $vcard['nickname'][0]['val'][0]; + if (!empty($vcard['bday'][0]['val'][0])) { + $tz = date_default_timezone_get(); + date_default_timezone_set('UTC'); + $message->birthday = strtotime($vcard['bday'][0]['val'][0]); + date_default_timezone_set($tz); + } + if (!empty($vcard['org'][0]['val'][0])) + $message->companyname = $vcard['org'][0]['val'][0]; + if (!empty($vcard['note'][0]['val'][0])) { + if (Request::GetProtocolVersion() >= 12.0) { + $message->asbody = new SyncBaseBody(); + $message->asbody->type = SYNC_BODYPREFERENCE_PLAIN; + $message->asbody->data = $vcard['note'][0]['val'][0]; + if ($truncsize > 0 && $truncsize < strlen($message->asbody->data)) { + $message->asbody->truncated = 1; + $message->asbody->data = Utils::Utf8_truncate($message->asbody->data, $truncsize); + } + else { + $message->asbody->truncated = 0; + } + + $message->asbody->estimatedDataSize = strlen($message->asbody->data); + } + else { + $message->body = $vcard['note'][0]['val'][0]; + if ($truncsize > 0 && $truncsize < strlen($message->body)) { + $message->bodytruncated = 1; + $message->body = Utils::Utf8_truncate($message->body, $truncsize); + } + else { + $message->bodytruncated = 0; + } + $message->bodysize = strlen($message->body); + } + } + if (!empty($vcard['role'][0]['val'][0])) + $message->jobtitle = $vcard['role'][0]['val'][0];//$vcard['title'][0]['val'][0] + if (!empty($vcard['url'][0]['val'][0])) + $message->webpage = $vcard['url'][0]['val'][0]; + if (!empty($vcard['categories'][0]['val'])) + $message->categories = $vcard['categories'][0]['val']; + + if (!empty($vcard['photo'][0]['val'][0])) + $message->picture = base64_encode($vcard['photo'][0]['val'][0]); + + return $message; + } + + /** + * Convert a SyncObject into vCard. + * + * @param SyncContact $message AS Contact + * @return string vcard text + */ + private function ParseToVCard($message) { + // http://tools.ietf.org/html/rfc6350 + $mapping = array( + 'fileas' => 'FN', + 'lastname;firstname;middlename;title;suffix' => 'N', + 'email1address' => 'EMAIL;PREF=1', + 'email2address' => 'EMAIL;PREF=2', + 'email3address' => 'EMAIL;PREF=3', + 'businessphonenumber' => 'TEL;TYPE=WORK,VOICE', + 'business2phonenumber' => 'TEL;TYPE=WORK,VOICE', + 'businessfaxnumber' => 'TEL;TYPE=WORK,FAX', + 'homephonenumber' => 'TEL;TYPE=HOME,VOICE', + 'home2phonenumber' => 'TEL;TYPE=HOME,VOICE', + 'homefaxnumber' => 'TEL;TYPE=HOME,FAX', + 'mobilephonenumber' => 'TEL;TYPE=CELL', + 'carphonenumber' => 'TEL;TYPE=VOICE', + 'pagernumber' => 'TEL;TYPE=PAGER', + ';;businessstreet;businesscity;businessstate;businesspostalcode;businesscountry' => 'ADR;TYPE=WORK', + ';;homestreet;homecity;homestate;homepostalcode;homecountry' => 'ADR;TYPE=HOME', + ';;otherstreet;othercity;otherstate;otherpostalcode;othercountry' => 'ADR', + 'companyname' => 'ORG', + 'body' => 'NOTE', + 'jobtitle' => 'ROLE', + 'webpage' => 'URL', + 'nickname' => 'NICKNAME' + ); + + $data = "BEGIN:VCARD\nVERSION:3.0\nPRODID:Z-Push\n"; + foreach ($mapping as $k => $v) { + $val = ''; + $ks = explode(';', $k); + foreach ($ks as $i) { + if (!empty($message->$i)) + $val .= $this->escape($message->$i); + $val.=';'; + } + if ($k == 'body' && isset($message->asbody)) { + $val = $message->asbody->data; + } + if (empty($val) || preg_match('/^(\;)+$/', $val) == 1) + continue; + // Remove trailing ; + $val = substr($val, 0, -1); + if (strlen($val) > 50) { + $data .= $v.":\n\t".substr(chunk_split($val, 50, "\n\t"), 0, -1); + } + else { + $data .= $v.':'.$val."\n"; + } + } + if (!empty($message->categories)) + $data .= 'CATEGORIES:'.implode(',', $message->categories)."\n"; + if (!empty($message->picture)) + $data .= 'PHOTO;ENCODING=BASE64;TYPE=JPEG:'."\n\t".substr(chunk_split($message->picture, 50, "\n\t"), 0, -1); + if (isset($message->birthday)) + $data .= 'BDAY:'.date('Y-m-d', $message->birthday)."\n"; + $data .= "END:VCARD"; + + // http://en.wikipedia.org/wiki/VCard + // TODO: add support for v4.0 + // not supported: anniversary, assistantname, assistnamephonenumber, children, department, officelocation, radiophonenumber, spouse, rtf + + return $data; + } + + + /** + * Discover all the addressbooks collections for a user under a root. + * + */ + private function discoverAddressbooks() { + unset($this->addressbooks); + $this->addressbooks = array(); + $raw = $this->server->get(false, false, true); + if ($raw !== false) { + $xml = new SimpleXMLElement($raw); + foreach ($xml->addressbook_element as $response) { + if ($this->gal_url !== false) { + if (strcmp(urldecode($response->url), $this->gal_url) == 0) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::discoverAddressbooks() Ignoring GAL addressbook '%s'", $this->gal_url)); + continue; + } + } + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::discoverAddressbooks() Found addressbook '%s'", urldecode($response->url))); + $this->addressbooks[] = urldecode($response->url); + } + unset($xml); + } + } + + /** + * Returns de addressbookId of a vcard. + * The vcardId sent to the device is formed as [addressbookId]-[vcardId] + * + * @param string $vcardId vcard ID in device. + * @return addressbookId + */ + private function getAddressbookIdFromVcard($vcardId) { + $parts = explode("-", $vcardId); + + return $parts[0]; + } + + /** + * Returns de vcard id stored in the carddav server. + * + * @param string $vcardId vcard ID in device + * @return vcard id in carddav server + */ + private function getVcardId($vcardId) { + $parts = explode("-", $vcardId); + + $id = ""; + for ($i = 1; $i < count($parts); $i++) { + if ($i > 1) { + $id .= "-"; + } + $id .= $parts[$i]; + } + + return $id; + } + + /** + * Convert an addressbook url into a zpush id. + * + * @param string $addressbookUrl AddressBook URL + * @return id or false + */ + private function convertAddressbookUrl($addressbookUrl) { + $this->InitializePermanentStorage(); + + // check if this addressbookUrl was converted before + $addressbookId = $this->getAddressbookFromUrl($addressbookUrl); + + // nothing found, so generate a new id and put it in the cache + if ($addressbookId === false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::convertAddressbookUrl('%s') New addressbook", $addressbookUrl)); + // generate addressbookId and add it to the mapping + $addressbookId = sprintf('%04x%04x', mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )); + + // addressbookId to addressbookUrl mapping + if (!isset($this->permanentStorage->fmAidAurl)) + $this->permanentStorage->fmAidAurl = array(); + + $a = $this->permanentStorage->fmAidAurl; + $a[$addressbookId] = $addressbookUrl; + $this->permanentStorage->fmAidAurl = $a; + + // addressbookUrl to addressbookId mapping + if (!isset($this->permanentStorage->fmAurlAid)) + $this->permanentStorage->fmAurlAid = array(); + + $b = $this->permanentStorage->fmAurlAid; + $b[$addressbookUrl] = $addressbookId; + $this->permanentStorage->fmAurlAid = $b; + } + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::convertAddressbookUrl('%s') = %s", $addressbookUrl, $addressbookId)); + + return $addressbookId; + } + + /** + * Get the URL of an addressbook zpush id. + * + * @param string $addressbookId AddressBook Z-Push based ID + * @return url or false + */ + private function getAddressbookFromId($addressbookId) { + $this->InitializePermanentStorage(); + + $addressbookUrl = false; + + if (isset($this->permanentStorage->fmAidAurl)) { + if (isset($this->permanentStorage->fmAidAurl[$addressbookId])) { + $addressbookUrl = $this->permanentStorage->fmAidAurl[$addressbookId]; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::getAddressbookFromId('%s') = %s", $addressbookId, $addressbookUrl)); + } + else { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendCardDAV::getAddressbookFromId('%s') = %s", $addressbookId, 'not found')); + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::getAddressbookFromId('%s') = %s", $addressbookId, 'not initialized!')); + } + + return $addressbookUrl; + } + + /** + * Get the zpush id of an addressbook. + * + * @param string $addressbookUrl AddressBook URL + * @return id or false + */ + private function getAddressbookFromUrl($addressbookUrl) { + $this->InitializePermanentStorage(); + + $addressbookId = false; + + if (isset($this->permanentStorage->fmAurlAid)) { + if (isset($this->permanentStorage->fmAurlAid[$addressbookUrl])) { + $addressbookId = $this->permanentStorage->fmAurlAid[$addressbookUrl]; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::getAddressbookFromUrl('%s') = %s", $addressbookUrl, $addressbookId)); + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::getAddressbookFromUrl('%s') = %s", $addressbookUrl, 'not found')); + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::getAddressbookFromUrl('%s') = %s", $addressbookUrl, 'not initialized!')); + } + + return $addressbookId; + } + +}; +?> \ No newline at end of file diff --git a/sources/backend/carddav/config.php b/sources/backend/carddav/config.php new file mode 100644 index 0000000..2d24cc3 --- /dev/null +++ b/sources/backend/carddav/config.php @@ -0,0 +1,109 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// ************************ +// BackendCardDAV settings +// ************************ + +// Server protocol: http or https +define('CARDDAV_PROTOCOL', 'https'); + +// Server name +define('CARDDAV_SERVER', 'localhost'); + +// Server port +define('CARDDAV_PORT', '443'); + +// Server path to the addressbook, or the principal with the addressbooks +// If your user has more than 1 addressbook point it to the principal. +// Example: user test@domain.com will have 2 addressbooks +// http://localhost/caldav.php/test@domain.com/addresses/personal +// http://localhost/caldav.php/test@domain.com/addresses/work +// You set the CARDDAV_PATH to '/caldav.php/%u/addresses/' and personal and work will be autodiscovered +// %u: replaced with the username +// %d: replaced with the domain +// Add the trailing / +define('CARDDAV_PATH', '/caldav.php/%u/'); + + +// Server path to the default addressbook +// Mobile device will create new contacts here. It must be under CARDDAV_PATH +// %u: replaced with the username +// %d: replaced with the domain +// Add the trailing / +define('CARDDAV_DEFAULT_PATH', '/caldav.php/%u/addresses/'); + +// Server path to the GAL addressbook. This addressbook is readonly and searchable by the user, but it will NOT be synced. +// If you don't want GAL, comment it +// %u: replaced with the username +// %d: replaced with the domain +// Add the trailing / +define('CARDDAV_GAL_PATH', '/caldav.php/%d/GAL/'); + +// Minimal length for the search pattern to do the real search. +define('CARDDAV_GAL_MIN_LENGTH', 5); + +// Addressbook display name, the name showed in the mobile device +// %u: replaced with the username +// %d: replaced with the domain +define('CARDDAV_CONTACTS_FOLDER_NAME', '%u Addressbook'); + + +// If the CardDAV server supports the sync-collection operation +// DAViCal and SabreDav support it, but Owncloud, SOGo don't +// SabreDav version must be at least 1.9.0, otherwise set this to false +// Setting this to false will work with most servers, but it will be slower: 1 petition for the href of vcards, and 1 petition for each vcard +define('CARDDAV_SUPPORTS_SYNC', false); + + +// If the CardDAV server supports the FN attribute for searches +// DAViCal supports it, but SabreDav, Owncloud and SOGo don't +// Setting this to true will search by FN. If false will search by sn, givenName and email +// It's safe to leave it as false +define('CARDDAV_SUPPORTS_FN_SEARCH', false); + + +// If your carddav server needs to use file extension to recover a vcard. +// Davical needs it +// SOGo official demo online needs it, but some SOGo installation don't need it, so test it +define('CARDDAV_URL_VCARD_EXTENSION', '.vcf'); \ No newline at end of file diff --git a/sources/backend/combined/combined.php b/sources/backend/combined/combined.php index 367e4e9..b0bf385 100644 --- a/sources/backend/combined/combined.php +++ b/sources/backend/combined/combined.php @@ -57,11 +57,12 @@ require_once("backend/combined/config.php"); require_once("backend/combined/importer.php"); require_once("backend/combined/exporter.php"); -class BackendCombined extends Backend { +class BackendCombined extends Backend implements ISearchProvider { public $config; public $backends; private $activeBackend; private $activeBackendID; + private $numberChangesSink; /** * Constructor of the combined backend @@ -72,12 +73,12 @@ class BackendCombined extends Backend { parent::Backend(); $this->config = BackendCombinedConfig::GetBackendCombinedConfig(); - foreach ($this->config['backends'] as $i => $b){ - // load and instatiate backend - ZPush::IncludeBackend($b['name']); - $this->backends[$i] = new $b['name'](); + $backend_values = array_unique(array_values($this->config['folderbackend'])); + foreach ($backend_values as $i) { + ZPush::IncludeBackend($this->config['backends'][$i]['name']); + $this->backends[$i] = new $this->config['backends'][$i]['name'](); } - ZLog::Write(LOGLEVEL_INFO, sprintf("Combined %d backends loaded.", count($this->backends))); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined %d backends loaded.", count($this->backends))); } /** @@ -116,7 +117,8 @@ class BackendCombined extends Backend { return false; } } - ZLog::Write(LOGLEVEL_INFO, "Combined->Logon() success"); + + ZLog::Write(LOGLEVEL_DEBUG, "Combined->Logon() success"); return true; } @@ -154,7 +156,7 @@ class BackendCombined extends Backend { return false; } } - ZLog::Write(LOGLEVEL_INFO, "Combined->Setup() success"); + ZLog::Write(LOGLEVEL_DEBUG, "Combined->Setup() success"); return true; } @@ -274,6 +276,10 @@ class BackendCombined extends Backend { */ public function SendMail($sm) { ZLog::Write(LOGLEVEL_DEBUG, "Combined->SendMail()"); + // Convert source folderid + if (isset($sm->source->folderid)) { + $sm->source->folderid = $this->GetBackendFolder($sm->source->folderid); + } foreach ($this->backends as $i => $b){ if($this->backends[$i]->SendMail($sm) == true){ return true; @@ -358,11 +364,125 @@ class BackendCombined extends Backend { * @return string id of the created/updated calendar obj * @throws StatusException */ - public function MeetingResponse($requestid, $folderid, $error) { + public function MeetingResponse($requestid, $folderid, $response) { $backend = $this->GetBackend($folderid); if($backend === false) return false; - return $backend->MeetingResponse($requestid, $this->GetBackendFolder($folderid), $error); + return $backend->MeetingResponse($requestid, $this->GetBackendFolder($folderid), $response); + } + + + /** + * Deletes all contents of the specified folder. + * This is generally used to empty the trash (wastebasked), but could also be used on any + * other folder. + * + * @param string $folderid + * @param boolean $includeSubfolders (opt) also delete sub folders, default true + * + * @access public + * @return boolean + * @throws StatusException + */ + public function EmptyFolder($folderid, $includeSubfolders = true) { + $backend = $this->GetBackend($folderid); + if($backend === false) + return false; + return $backend->EmptyFolder($this->GetBackendFolder($folderid), $includeSubfolders); + } + + /** + * Indicates if the backend has a ChangesSink. + * A sink is an active notification mechanism which does not need polling. + * + * @access public + * @return boolean + */ + public function HasChangesSink() { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCombined->HasChangesSink()")); + + $this->numberChangesSink = 0; + + foreach ($this->backends as $i => $b) { + if ($this->backends[$i]->HasChangesSink()) { + $this->numberChangesSink++; + } + } + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCombined->HasChangesSink - Number ChangesSink found: %d", $this->numberChangesSink)); + + return true; + } + + /** + * The folder should be considered by the sink. + * Folders which were not initialized should not result in a notification + * of IBacken->ChangesSink(). + * + * @param string $folderid + * + * @access public + * @return boolean false if there is any problem with that folder + */ + public function ChangesSinkInitialize($folderid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCombined->ChangesSinkInitialize('%s')", $folderid)); + + $backend = $this->GetBackend($folderid); + if($backend === false) { + // if not backend is found we return true, we don't want this to never cause an error + return true; + } + + if ($backend->HasChangesSink()) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCombined->ChangesSinkInitialize('%s') is supported, initializing", $folderid)); + return $backend->ChangesSinkInitialize($this->GetBackendFolder($folderid)); + } + else { + // if the backend doesn't support ChangesSink, we also return true so we don't get an error + return true; + } + } + + /** + * The actual ChangesSink. + * For max. the $timeout value this method should block and if no changes + * are available return an empty array. + * If changes are available a list of folderids is expected. + * + * @param int $timeout max. amount of seconds to block + * + * @access public + * @return array + */ + public function ChangesSink($timeout = 30) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCombined->ChangesSink(%d)", $timeout)); + + $notifications = array(); + if ($this->numberChangesSink == 0) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCombined doesn't include any Sinkable backends")); + } else { + $stopat = time() + $timeout - 1; + //we will spend 2 seconds at least in each backend that support changessink + // why 2 seconds? because it's the minimum to ensure we run at least once the changessink + // I think it's fairer than run for 10 continuos seconds the same backend (run backend1, run backend2, run backend1, run backend2... vs run backend1, run backend1, run backend2, run backend2) + do { + foreach ($this->backends as $i => $b) { + if ($this->backends[$i]->HasChangesSink()) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCombined->ChangesSink - Calling in '%s' with %d", get_class($b), 2)); + + // 2 seconds hardcoded timeout!!! + $notifications_backend = $this->backends[$i]->ChangesSink(2); + //preppend backend delimiter + for ($c = 0; $c < count($notifications_backend); $c++) { + $notifications_backend[$c] = $i . $this->config['delimiter'] . $notifications_backend[$c]; + } + $notifications = array_merge($notifications, $notifications_backend); + } + } + } while($stopat > time() && empty($notifications)); + } + + return $notifications; } /** @@ -413,7 +533,159 @@ class BackendCombined extends Backend { $pos = strpos($folderid, $this->config['delimiter']); if($pos === false) return false; - return substr($folderid,0,$pos); + return substr($folderid, 0, $pos); + } + + /** + * Indicates which AS version is supported by the backend. + * Return the lowest version supported by the backends used. + * + * @access public + * @return string AS version constant + */ + public function GetSupportedASVersion() { + $version = ZPush::ASV_14; + foreach ($this->backends as $i => $b) { + $subversion = $this->backends[$i]->GetSupportedASVersion(); + if ($subversion < $version) { + $version = $subversion; + } + } + return $version; + } + + /** + * Returns the BackendCombined as it implements the ISearchProvider interface + * This could be overwritten by the global configuration + * + * @access public + * @return object Implementation of ISearchProvider + */ + public function GetSearchProvider() { + return $this; + } + + + /*----------------------------------------------------------------------------------------- + -- ISearchProvider + ------------------------------------------------------------------------------------------*/ + /** + * Indicates if a search type is supported by this SearchProvider + * It supports all the search types, searches are delegated. + * + * @param string $searchtype + * + * @access public + * @return boolean + */ + public function SupportsType($searchtype) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->SupportsType('%s')", $searchtype)); + $i = $this->getSearchBackend($searchtype); + + return $i !== false; + } + + + /** + * Queries the LDAP backend + * + * @param string $searchquery string to be searched for + * @param string $searchrange specified searchrange + * + * @access public + * @return array search results + */ + public function GetGALSearchResults($searchquery, $searchrange) { + ZLog::Write(LOGLEVEL_DEBUG, "Combined->GetGALSearchResults()"); + $i = $this->getSearchBackend(ISearchProvider::SEARCH_GAL); + + $result = false; + if ($i !== false) { + $result = $this->backends[$i]->GetGALSearchResults($searchquery, $searchrange); + } + + return $result; + } + + + /** + * Searches for the emails on the server + * + * @param ContentParameter $cpo + * + * @return array + */ + public function GetMailboxSearchResults($cpo) { + ZLog::Write(LOGLEVEL_DEBUG, "Combined->GetMailboxSearchResults()"); + $i = $this->getSearchBackend(ISearchProvider::SEARCH_MAILBOX); + + $result = false; + if ($i !== false) { + //Convert $cpo GetSearchFolderid + $cpo->SetSearchFolderid($this->GetBackendFolder($cpo->GetSearchFolderid())); + $result = $this->backends[$i]->GetMailboxSearchResults($cpo, $i . $this->config['delimiter']); + } + + return $result; + } + + + /** + * Terminates a search for a given PID + * + * @param int $pid + * + * @return boolean + */ + public function TerminateSearch($pid) { + ZLog::Write(LOGLEVEL_DEBUG, "Combined->TerminateSearch()"); + foreach ($this->backends as $i => $b) { + if ($this->backends[$i] instanceof ISearchProvider) { + $this->backends[$i]->TerminateSearch($pid); + } + } + + return true; + } + + + /** + * Disconnects backends + * + * @access public + * @return boolean + */ + public function Disconnect() { + ZLog::Write(LOGLEVEL_DEBUG, "Combined->Disconnect()"); + foreach ($this->backends as $i => $b) { + if ($this->backends[$i] instanceof ISearchProvider) { + $this->backends[$i]->Disconnect(); + } + } + + return true; + } + + + /** + * Returns the first backend that support a search type + * + * @param string $searchtype + * + * @access private + * @return string + */ + private function getSearchBackend($searchtype) { + foreach ($this->backends as $i => $b) { + if ($this->backends[$i] instanceof ISearchProvider) { + if ($this->backends[$i]->SupportsType($searchtype)) { + return $i; + } + } + } + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->getSearchBackend('%s') No support found!", $searchtype)); + + return false; } } ?> \ No newline at end of file diff --git a/sources/backend/combined/config.php b/sources/backend/combined/config.php index 452cb5a..32e61f1 100644 --- a/sources/backend/combined/config.php +++ b/sources/backend/combined/config.php @@ -74,6 +74,15 @@ class BackendCombinedConfig { 'v' => array( 'name' => 'BackendVCardDir', ), + 'c' => array( + 'name' => 'BackendCalDAV', + ), + 'l' => array( + 'name' => 'BackendLDAP', + ), + 'd' => array( + 'name' => 'BackendCardDAV', + ), ), 'delimiter' => '/', //force one type of folder to one backend diff --git a/sources/backend/combined/exporter.php b/sources/backend/combined/exporter.php index 43ac379..1ddbdad 100644 --- a/sources/backend/combined/exporter.php +++ b/sources/backend/combined/exporter.php @@ -173,7 +173,7 @@ class ExportChangesCombined implements IExportChanges { public function InitializeExporter(&$importer) { ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined->InitializeExporter(...)"); foreach ($this->exporters as $i => $e) { - if(!isset($this->_importwraps[$i])){ + if(!isset($this->importwraps[$i])){ $this->importwraps[$i] = new ImportHierarchyChangesCombinedWrap($i, $this->backend, $importer); } $e->InitializeExporter($this->importwraps[$i]); diff --git a/sources/backend/combined/importer.php b/sources/backend/combined/importer.php index 2f2f8f9..24497e7 100644 --- a/sources/backend/combined/importer.php +++ b/sources/backend/combined/importer.php @@ -131,6 +131,24 @@ class ImportChangesCombined implements IImportChanges { return $this->icc->ImportMessageReadFlag($id, $flags); } + /** + * Imports a change in 'star' flag + * This can never conflict + * + * @param string $id + * @param int $flags + * + * @access public + * @return boolean + */ + public function ImportMessageStarFlag($id, $flags) { + if (!$this->icc) { + ZLog::Write(LOGLEVEL_ERROR, "ImportChangesCombined->ImportMessageReadFlag() icc not configured"); + return false; + } + return $this->icc->ImportMessageStarFlag($id, $flags); + } + /** * Imports a move of a message. This occurs when a user moves an item to another folder * diff --git a/sources/backend/imap/README b/sources/backend/imap/README new file mode 100644 index 0000000..ab6c342 --- /dev/null +++ b/sources/backend/imap/README @@ -0,0 +1,28 @@ +BackendIMAP - NOTES +=================== + +This backend support the Search operation in the mailbox. +Since the IMAP search operation is pretty slow, with a medium/big mailbox, or with a lots of folders, +the mobile device will timeout the operation before this is completed on server. + +I'm using Dovecot + FTS-SOLR plugin so the real search is done against an Apache SOLR server. +It reduces a 1-2 minutes search to 1-5 seconds, and the response is given to the mobile device in time. + + +CHANGESSINK +=========== +It supports ChangesSink method that will detect and send faster changes to your device. + + +SMTP +==== +You can choice between 3 methods for send mails: mail (php mail), sendmail (native binary), smtp (php smtp direct connection). +Remember to configure it in the config.php + +"mail" is a sendmail wrapper in Linux. + + +MBCONVERT +========= +A lot of messages come with wrong encoding, to them to look better with any device you can pre-convert them to UTF-8. +You will need to install the php-mbstring module \ No newline at end of file diff --git a/sources/backend/imap/REQUIREMENTS b/sources/backend/imap/REQUIREMENTS new file mode 100644 index 0000000..740bdae --- /dev/null +++ b/sources/backend/imap/REQUIREMENTS @@ -0,0 +1,7 @@ +REQUIREMENTS: +php-imap +php-mbstring (optional but recommended) +libawl-php + + +IMAP server (Dovecot, Courier...) \ No newline at end of file diff --git a/sources/backend/imap/config.php b/sources/backend/imap/config.php index fe4f789..17975ce 100644 --- a/sources/backend/imap/config.php +++ b/sources/backend/imap/config.php @@ -54,25 +54,107 @@ define('IMAP_PORT', 143); // best cross-platform compatibility (see http://php.net/imap_open for options) define('IMAP_OPTIONS', '/notls/norsh'); -// overwrite the "from" header if it isn't set when sending emails -// options: 'username' - the username will be set (usefull if your login is equal to your emailaddress) -// 'domain' - the value of the "domain" field is used +// overwrite the "from" header with some value +// options: +// '' - do nothing, use the From header +// 'username' - the username will be set (usefull if your login is equal to your emailaddress) +// 'domain' - the value of the "domain" field is used +// 'sql' - the username will be the result of a sql query. REMEMBER TO INSTALL PHP-PDO AND PHP-DATABASE +// 'ldap' - the username will be the result of a ldap query. REMEMBER TO INSTALL PHP-LDAP!! // '@mydomain.com' - the username is used and the given string will be appended define('IMAP_DEFAULTFROM', ''); +// DSN: formatted PDO connection string +// mysql:host=xxx;port=xxx;dbname=xxx +// USER: username to DB +// PASSWORD: password to DB +// OPTIONS: array with options needed +// QUERY: query to execute +// FIELDS: columns in the query +// FROM: string that will be the from, replacing the column names with the values +define('IMAP_FROM_SQL_DSN', ''); +define('IMAP_FROM_SQL_USER', ''); +define('IMAP_FROM_SQL_PASSWORD', ''); +define('IMAP_FROM_SQL_OPTIONS', serialize(array(PDO::ATTR_PERSISTENT => true))); +define('IMAP_FROM_SQL_QUERY', "select first_name, last_name, mail_address from users where mail_address = '#username@#domain'"); +define('IMAP_FROM_SQL_FIELDS', serialize(array('first_name', 'last_name', 'mail_address'))); +define('IMAP_FROM_SQL_FROM', '#first_name #last_name <#mail_address>'); +define('IMAP_FROM_SQL_FULLNAME', '#first_name #last_name'); + +// SERVER: ldap server +// SERVER_PORT: ldap port +// USER: dn to use for connecting +// PASSWORD: password +// QUERY: query to execute +// FIELDS: columns in the query +// FROM: string that will be the from, replacing the field names with the values +define('IMAP_FROM_LDAP_SERVER', 'localhost'); +define('IMAP_FROM_LDAP_SERVER_PORT', '389'); +define('IMAP_FROM_LDAP_USER', 'cn=zpush,ou=servers,dc=zpush,dc=org'); +define('IMAP_FROM_LDAP_PASSWORD', 'password'); +define('IMAP_FROM_LDAP_BASE', 'dc=zpush,dc=org'); +define('IMAP_FROM_LDAP_QUERY', '(mail=#username@#domain)'); +define('IMAP_FROM_LDAP_FIELDS', serialize(array('givenname', 'sn', 'mail'))); +define('IMAP_FROM_LDAP_FROM', '#givenname #sn <#mail>'); +define('IMAP_FROM_LDAP_FULLNAME', '#givenname #sn'); + + +// Root folder or prefix in your IMAP server (without the separator). For example, with courier it will be INBOX, and your folder will be INBOX.Sent +// You can use the real case +define('IMAP_FOLDER_ROOT', 'INBOX'); + // copy outgoing mail to this folder. If not set z-push will try the default folders -define('IMAP_SENTFOLDER', ''); +// You can use the real case and the full path (INBOX.Sent) +define('IMAP_FOLDER_SENT', ''); -// forward messages inline (default false - as attachment) -define('IMAP_INLINE_FORWARD', false); +// Draft folder +// You can use the real case and the full path (INBOX.Draft) +define('IMAP_FOLDER_DRAFT', ''); -// use imap_mail() to send emails (default) - if false mail() is used -define('IMAP_USE_IMAPMAIL', true); +// Trash folder +// You can use the real case and the full path (INBOX.Trash) +define('IMAP_FOLDER_TRASH', ''); + +// forward messages inline (default true - inlined) +define('IMAP_INLINE_FORWARD', true); -/* BEGIN fmbiete's contribution r1527, ZP-319 */ // list of folders we want to exclude from sync. Names, or part of it, separated by | // example: dovecot.sieve|archive|spam define('IMAP_EXCLUDED_FOLDERS', ''); -/* END fmbiete's contribution r1527, ZP-319 */ -?> + +// Method used for sending mail +// mail => mail() php function +// sendmail => sendmail executable +// smtp => direct connection against SMTP +define('IMAP_SMTP_METHOD', 'mail'); + +global $imap_smtp_params; +// SMTP Parameters +// mail : no params +$imap_smtp_params = array(); +// sendmail +//$imap_smtp_params = array('sendmail_path' => '/usr/bin/sendmail', 'sendmail_args' => '-i'); +// smtp +// "host" - The server to connect. Default is localhost. +// "port" - The port to connect. Default is 25. +// "auth" - Whether or not to use SMTP authentication. Default is FALSE. +// "username" - The username to use for SMTP authentication. "imap_username" for using the same username as the imap server +// "password" - The password to use for SMTP authentication. "imap_password" for using the same password as the imap server +// "localhost" - The value to give when sending EHLO or HELO. Default is localhost +// "timeout" - The SMTP connection timeout. Default is NULL (no timeout). +// "verp" - Whether to use VERP or not. Default is FALSE. +// "debug" - Whether to enable SMTP debug mode or not. Default is FALSE. +// "persist" - Indicates whether or not the SMTP connection should persist over multiple calls to the send() method. +// "pipelining" - Indicates whether or not the SMTP commands pipelining should be used. +//$imap_smtp_params = array('host' => 'localhost', 'port' => 25, 'auth' => false); +// If you want to use SSL with port 25 or port 465 you must preppend "ssl://" before the hostname or IP of your SMTP server +// IMPORTANT: To use SSL you must use PHP 5.1 or later, install openssl libs and use ssl:// within the host variable +//$imap_smtp_params = array('host' => 'ssl://localhost', 'port' => 465, 'auth' => true, 'username' => 'imap_username', 'password' => 'imap_password'); + + +// If you are using IMAP_SMTP_METHOD = mail or sendmail and your sent messages are not correctly displayed you can change this to "\n". +// BUT, it doesn't comply with RFC 2822 and will break if using smtp method +define('MAIL_MIMEPART_CRLF', "\r\n"); + +?> \ No newline at end of file diff --git a/sources/backend/imap/imap.php b/sources/backend/imap/imap.php index d9b8160..bfa68f9 100644 --- a/sources/backend/imap/imap.php +++ b/sources/backend/imap/imap.php @@ -45,24 +45,48 @@ // config file require_once("backend/imap/config.php"); +require_once("backend/imap/mime_encode.php"); include_once('lib/default/diffbackend/diffbackend.php'); +include_once('include/Mail.php'); include_once('include/mimeDecode.php'); -require_once('include/z_RFC822.php'); +include_once('include/mimePart.php'); +include_once('include/z_RFC822.php'); +include_once('include/iCalendar.php'); -class BackendIMAP extends BackendDiff { - protected $wasteID; - protected $sentID; - protected $server; - protected $mbox; - protected $mboxFolder; - protected $username; - protected $domain; - protected $serverdelimiter; - protected $sinkfolders; - protected $sinkstates; - protected $excludedFolders; /* fmbiete's contribution r1527, ZP-319 */ +class BackendIMAP extends BackendDiff implements ISearchProvider { + private $wasteID; + private $sentID; + private $server; + private $mbox; + private $mboxFolder; + private $username; + private $password; + private $domain; + private $serverdelimiter; + private $sinkfolders = array(); + private $sinkstates = array(); + private $changessinkinit = false; + private $excludedFolders; + private static $mimeTypes = false; + + + public function BackendIMAP() { + if (BackendIMAP::$mimeTypes === false) { + BackendIMAP::$mimeTypes = $this->SystemExtensionMimeTypes(); + } + $this->wasteID = false; + $this->sentID = false; + $this->mboxFolder = ""; + + if (!function_exists("imap_open")) + throw new FatalException("BackendIMAP(): php-imap module is not installed", 0, null, LOGLEVEL_FATAL); + + if (!function_exists("mb_detect_order")) { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP(): php-mbstring module is not installed, you should install it for better encoding conversions")); + } + } /**---------------------------------------------------------------------------------------------------------- * default backend methods @@ -100,15 +124,16 @@ class BackendIMAP extends BackendDiff { $this->mboxFolder = ""; if ($this->mbox) { - ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->Logon(): User '%s' is authenticated on IMAP",$username)); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->Logon(): User '%s' is authenticated on '%s'", $username, $this->server)); $this->username = $username; + $this->password = $password; $this->domain = $domain; // set serverdelimiter $this->serverdelimiter = $this->getServerDelimiter(); return true; } else { - ZLog::Write(LOGLEVEL_ERROR, "BackendIMAP->Logon(): can't connect: " . imap_last_error()); + ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendIMAP->Logon(): can't connect as user '%s' on '%s': %s", $username, $this->server, imap_last_error())); return false; } } @@ -152,377 +177,287 @@ class BackendIMAP extends BackendDiff { * @return boolean * @throws StatusException */ - // TODO implement , $saveInSent = true public function SendMail($sm) { - $forward = $reply = (isset($sm->source->itemid) && $sm->source->itemid) ? $sm->source->itemid : false; - $parent = false; - - ZLog::Write(LOGLEVEL_DEBUG, sprintf("IMAPBackend->SendMail(): RFC822: %d bytes forward-id: '%s' reply-id: '%s' parent-id: '%s' SaveInSent: '%s' ReplaceMIME: '%s'", - strlen($sm->mime), Utils::PrintAsString($sm->forwardflag), Utils::PrintAsString($sm->replyflag), + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): RFC822: %d bytes forward-id: '%s' reply-id: '%s' parent-id: '%s' SaveInSent: '%s' ReplaceMIME: '%s'", + strlen($sm->mime), + Utils::PrintAsString($sm->forwardflag ? (isset($sm->source->itemid) ? $sm->source->itemid : "error no itemid") : false), + Utils::PrintAsString($sm->replyflag ? (isset($sm->source->itemid) ? $sm->source->itemid : "error no itemid") : false), Utils::PrintAsString((isset($sm->source->folderid) ? $sm->source->folderid : false)), - Utils::PrintAsString(($sm->saveinsent)), Utils::PrintAsString(isset($sm->replacemime)) )); - - if (isset($sm->source->folderid) && $sm->source->folderid) - // convert parent folder id back to work on an imap-id - $parent = $this->getImapIdFromFolderId($sm->source->folderid); - + Utils::PrintAsString(($sm->saveinsent)), Utils::PrintAsString(isset($sm->replacemime)))); // by splitting the message in several lines we can easily grep later foreach(preg_split("/((\r)?\n)/", $sm->mime) as $rfc822line) ZLog::Write(LOGLEVEL_WBXML, "RFC822: ". $rfc822line); - $mobj = new Mail_mimeDecode($sm->mime); - $message = $mobj->decode(array('decode_headers' => false, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')); - - $Mail_RFC822 = new Mail_RFC822(); - $toaddr = $ccaddr = $bccaddr = ""; - if(isset($message->headers["to"])) - $toaddr = $this->parseAddr($Mail_RFC822->parseAddressList($message->headers["to"])); - if(isset($message->headers["cc"])) - $ccaddr = $this->parseAddr($Mail_RFC822->parseAddressList($message->headers["cc"])); - if(isset($message->headers["bcc"])) - $bccaddr = $this->parseAddr($Mail_RFC822->parseAddressList($message->headers["bcc"])); - - // save some headers when forwarding mails (content type & transfer-encoding) - $headers = ""; - $forward_h_ct = ""; - $forward_h_cte = ""; - $envelopefrom = ""; - - $use_orgbody = false; - - // clean up the transmitted headers - // remove default headers because we are using imap_mail - $changedfrom = false; - $returnPathSet = false; - $body_base64 = false; - $org_charset = ""; - $org_boundary = false; - $multipartmixed = false; - foreach($message->headers as $k => $v) { - if ($k == "subject" || $k == "to" || $k == "cc" || $k == "bcc") - continue; - - if ($k == "content-type") { - // if the message is a multipart message, then we should use the sent body - if (preg_match("/multipart/i", $v)) { - $use_orgbody = true; - $org_boundary = $message->ctype_parameters["boundary"]; - } - - // save the original content-type header for the body part when forwarding - if ($sm->forwardflag && !$use_orgbody) { - $forward_h_ct = $v; - continue; - } - - // set charset always to utf-8 - $org_charset = $v; - $v = preg_replace("/charset=([A-Za-z0-9-\"']+)/", "charset=\"utf-8\"", $v); - } - - if ($k == "content-transfer-encoding") { - // if the content was base64 encoded, encode the body again when sending - if (trim($v) == "base64") $body_base64 = true; - - // save the original encoding header for the body part when forwarding - if ($sm->forwardflag) { - $forward_h_cte = $v; - continue; - } - } - - // check if "from"-header is set, do nothing if it's set - // else set it to IMAP_DEFAULTFROM - if ($k == "from") { - if (trim($v)) { - $changedfrom = true; - } elseif (! trim($v) && IMAP_DEFAULTFROM) { - $changedfrom = true; - if (IMAP_DEFAULTFROM == 'username') $v = $this->username; - else if (IMAP_DEFAULTFROM == 'domain') $v = $this->domain; - else $v = $this->username . IMAP_DEFAULTFROM; - $envelopefrom = "-f$v"; - } - } - - // check if "Return-Path"-header is set - if ($k == "return-path") { - $returnPathSet = true; - if (! trim($v) && IMAP_DEFAULTFROM) { - if (IMAP_DEFAULTFROM == 'username') $v = $this->username; - else if (IMAP_DEFAULTFROM == 'domain') $v = $this->domain; - else $v = $this->username . IMAP_DEFAULTFROM; - } - } - - // all other headers stay - if ($headers) $headers .= "\n"; - $headers .= ucfirst($k) . ": ". $v; - } - - // set "From" header if not set on the device - if(IMAP_DEFAULTFROM && !$changedfrom){ - if (IMAP_DEFAULTFROM == 'username') $v = $this->username; - else if (IMAP_DEFAULTFROM == 'domain') $v = $this->domain; - else $v = $this->username . IMAP_DEFAULTFROM; - if ($headers) $headers .= "\n"; - $headers .= 'From: '.$v; - $envelopefrom = "-f$v"; - } - - // set "Return-Path" header if not set on the device - if(IMAP_DEFAULTFROM && !$returnPathSet){ - if (IMAP_DEFAULTFROM == 'username') $v = $this->username; - else if (IMAP_DEFAULTFROM == 'domain') $v = $this->domain; - else $v = $this->username . IMAP_DEFAULTFROM; - if ($headers) $headers .= "\n"; - $headers .= 'Return-Path: '.$v; - } - - // if this is a multipart message with a boundary, we must use the original body - if ($use_orgbody) { - list(,$body) = $mobj->_splitBodyHeader($sm->mime); - $repl_body = $this->getBody($message); - } - else - $body = $this->getBody($message); - - // reply - if ($sm->replyflag && $parent) { - $this->imap_reopenFolder($parent); - // receive entire mail (header + body) to decode body correctly - $origmail = @imap_fetchheader($this->mbox, $reply, FT_UID) . @imap_body($this->mbox, $reply, FT_PEEK | FT_UID); - if (!$origmail) - throw new StatusException(sprintf("BackendIMAP->SendMail(): Could not open message id '%s' in folder id '%s' to be replied: %s", $reply, $parent, imap_last_error()), SYNC_COMMONSTATUS_ITEMNOTFOUND); - - $mobj2 = new Mail_mimeDecode($origmail); - // receive only body - $body .= $this->getBody($mobj2->decode(array('decode_headers' => false, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8'))); - // unset mimedecoder & origmail - free memory - unset($mobj2); - unset($origmail); - } - - // encode the body to base64 if it was sent originally in base64 by the pda - // contrib - chunk base64 encoded body - if ($body_base64 && !$sm->forwardflag) $body = chunk_split(base64_encode($body)); - - - // forward - if ($sm->forwardflag && $parent) { - $this->imap_reopenFolder($parent); - // receive entire mail (header + body) - $origmail = @imap_fetchheader($this->mbox, $forward, FT_UID) . @imap_body($this->mbox, $forward, FT_PEEK | FT_UID); - - if (!$origmail) - throw new StatusException(sprintf("BackendIMAP->SendMail(): Could not open message id '%s' in folder id '%s' to be forwarded: %s", $forward, $parent, imap_last_error()), SYNC_COMMONSTATUS_ITEMNOTFOUND); - - if (!defined('IMAP_INLINE_FORWARD') || IMAP_INLINE_FORWARD === false) { - // contrib - chunk base64 encoded body - if ($body_base64) $body = chunk_split(base64_encode($body)); - //use original boundary if it's set - $boundary = ($org_boundary) ? $org_boundary : false; - // build a new mime message, forward entire old mail as file - list($aheader, $body) = $this->mail_attach("forwarded_message.eml",strlen($origmail),$origmail, $body, $forward_h_ct, $forward_h_cte,$boundary); - // add boundary headers - $headers .= "\n" . $aheader; - + $sourceMessage = $sourceMail = false; + // If we have a reference to a source message and we are not replacing mime (since we wouldn't use it) + if (isset($sm->source->folderid) && isset($sm->source->itemid) && (!isset($sm->replacemime) || $sm->replacemime === false)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): We have a source message and we try to fetch it")); + $parent = $this->getImapIdFromFolderId($sm->source->folderid); + if ($parent === false) { + throw new StatusException(sprintf("BackendIMAP->SendMail(): Could not get imapid from source folderid '%'", $sm->source->folderid), SYNC_COMMONSTATUS_ITEMNOTFOUND); } else { - $mobj2 = new Mail_mimeDecode($origmail); - $mess2 = $mobj2->decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')); + $this->imap_reopen_folder($parent); + $sourceMail = @imap_fetchheader($this->mbox, $sm->source->itemid, FT_UID) . @imap_body($this->mbox, $sm->source->itemid, FT_PEEK | FT_UID); + $mobj = new Mail_mimeDecode($sourceMail); + $sourceMessage = $mobj->decode(array('decode_headers' => false, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')); + unset($mobj); + //We will need $sourceMail if the message is forwarded and not inlined - if (!$use_orgbody) - $nbody = $body; - else - $nbody = $repl_body; - - $nbody .= "\r\n\r\n"; - $nbody .= "-----Original Message-----\r\n"; - if(isset($mess2->headers['from'])) - $nbody .= "From: " . $mess2->headers['from'] . "\r\n"; - if(isset($mess2->headers['to']) && strlen($mess2->headers['to']) > 0) - $nbody .= "To: " . $mess2->headers['to'] . "\r\n"; - if(isset($mess2->headers['cc']) && strlen($mess2->headers['cc']) > 0) - $nbody .= "Cc: " . $mess2->headers['cc'] . "\r\n"; - if(isset($mess2->headers['date'])) - $nbody .= "Sent: " . $mess2->headers['date'] . "\r\n"; - if(isset($mess2->headers['subject'])) - $nbody .= "Subject: " . $mess2->headers['subject'] . "\r\n"; - $nbody .= "\r\n"; - $nbody .= $this->getBody($mess2); - - if ($body_base64) { - // contrib - chunk base64 encoded body - $nbody = chunk_split(base64_encode($nbody)); - if ($use_orgbody) - // contrib - chunk base64 encoded body - $repl_body = chunk_split(base64_encode($repl_body)); + // If it's a reply, we mark the original message as answered + if ($sm->replyflag) { + if (!@imap_setflag_full($this->mbox, $sm->source->itemid, "\\Answered", ST_UID)) { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->SendMail(): Unable to mark the message as Answered")); + } } - if ($use_orgbody) { - ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): -------------------"); - ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): old:\n'$repl_body'\nnew:\n'$nbody'\nund der body:\n'$body'"); - //$body is quoted-printable encoded while $repl_body and $nbody are plain text, - //so we need to decode $body in order replace to take place - $body = str_replace($repl_body, $nbody, quoted_printable_decode($body)); + // If it's a forward, we mark the original message as forwarded + if ($sm->forwardflag) { + if (!@imap_setflag_full($this->mbox, $sm->source->itemid, "\\Forwarded", ST_UID)) { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->SendMail(): Unable to mark the message as Forwarded")); + } } - else - $body = $nbody; + } + } + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): We get the new message")); + $mobj = new Mail_mimeDecode($sm->mime); + $message = $mobj->decode(array('decode_headers' => false, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')); + unset($mobj); - if(isset($mess2->parts)) { - $attached = false; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): We get the From and To")); + $Mail_RFC822 = new Mail_RFC822(); - if ($org_boundary) { - $att_boundary = $org_boundary; - // cut end boundary from body - $body = substr($body, 0, strrpos($body, "--$att_boundary--")); - } - else { - $att_boundary = strtoupper(md5(uniqid(time()))); - // add boundary headers - $headers .= "\n" . "Content-Type: multipart/mixed; boundary=$att_boundary"; - $multipartmixed = true; - } + $toaddr = ""; + $this->setFromHeaderValue($message->headers); + $fromaddr = $this->parseAddr($Mail_RFC822->parseAddressList($message->headers["from"])); - foreach($mess2->parts as $part) { - if(isset($part->disposition) && ($part->disposition == "attachment" || $part->disposition == "inline")) { + if (isset($message->headers["to"])) { + $toaddr = $this->parseAddr($Mail_RFC822->parseAddressList($message->headers["to"])); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): To defined: %s", $toaddr)); + } + unset($Mail_RFC822); - if(isset($part->d_parameters['filename'])) - $attname = $part->d_parameters['filename']; - else if(isset($part->ctype_parameters['name'])) - $attname = $part->ctype_parameters['name']; - else if(isset($part->headers['content-description'])) - $attname = $part->headers['content-description']; - else $attname = "unknown attachment"; + $this->setReturnPathValue($message->headers, $fromaddr); - // ignore html content - if ($part->ctype_primary == "text" && $part->ctype_secondary == "html") { - continue; - } - // - if ($use_orgbody || $attached) { - $body .= $this->enc_attach_file($att_boundary, $attname, strlen($part->body),$part->body, $part->ctype_primary ."/". $part->ctype_secondary); - } - // first attachment - else { - $encmail = $body; - $attached = true; - $body = $this->enc_multipart($att_boundary, $body, $forward_h_ct, $forward_h_cte); - $body .= $this->enc_attach_file($att_boundary, $attname, strlen($part->body),$part->body, $part->ctype_primary ."/". $part->ctype_secondary); - } - } - } - if ($multipartmixed && strpos(strtolower($mess2->headers['content-type']), "alternative") !== false) { - //this happens if a multipart/alternative message is forwarded - //then it's a multipart/mixed message which consists of: - //1. text/plain part which was written on the mobile - //2. multipart/alternative part which is the original message - $body = "This is a message with multiple parts in MIME format.\n--". - $att_boundary. - "\nContent-Type: $forward_h_ct\nContent-Transfer-Encoding: $forward_h_cte\n\n". - (($body_base64) ? chunk_split(base64_encode($message->body)) : rtrim($message->body)). - "\n--".$att_boundary. - "\nContent-Type: {$mess2->headers['content-type']}\n\n". - @imap_body($this->mbox, $forward, FT_PEEK | FT_UID)."\n\n"; - } - $body .= "--$att_boundary--\n\n"; + $finalBody = ""; + $finalHeaders = array(); + + // if it's a S/MIME message I don't do anything with it + if (is_smime($message)) { + $mobj = new Mail_mimeDecode($sm->mime); + $parts = $mobj->getSendArray(); + unset($mobj); + if ($parts === false) { + throw new StatusException(sprintf("BackendIMAP->SendMail(): Could not getSendArray for SMIME messages"), SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED); + } + else { + list($recipents, $finalHeaders, $finalBody) = $parts; + + $this->setFromHeaderValue($finalHeaders); + $this->setReturnPathValue($finalHeaders, $fromaddr); + } + } + else { + //http://pear.php.net/manual/en/package.mail.mail-mime.example.php + //http://pear.php.net/manual/en/package.mail.mail-mimedecode.decode.php + //http://pear.php.net/manual/en/package.mail.mail-mimepart.addsubpart.php + + // I don't mind if the new message is multipart or not, I always will create a multipart. It's simpler + $finalEmail = new Mail_mimePart('', array('content_type' => 'multipart/mixed')); + + if ($sm->replyflag && (!isset($sm->replacemime) || $sm->replacemime === false)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): Replying message")); + $this->addTextParts($finalEmail, $message, $sourceMessage, true); + + if (isset($message->parts)) { + // We add extra parts from the replying message + add_extra_sub_parts($finalEmail, $message->parts); } + // A replied message doesn't include the original attachments + } + else if ($sm->forwardflag && (!isset($sm->replacemime) || $sm->replacemime === false)) { + if (!defined('IMAP_INLINE_FORWARD') || IMAP_INLINE_FORWARD === false) { + ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): Forwarding message as attached file - eml"); + $finalEmail->addSubPart($sourceMail, array('content_type' => 'message/rfc822', 'encoding' => 'base64', 'disposition' => 'attachment', 'dfilename' => 'forwarded_message.eml')); + } + else { + ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): Forwarding inlined message"); + $this->addTextParts($finalEmail, $message, $sourceMessage, false); - unset($mobj2); + if (isset($message->parts)) { + // We add extra parts from the forwarding message + add_extra_sub_parts($finalEmail, $message->parts); + } + if (isset($sourceMessage->parts)) { + // We add extra parts from the forwarded message + add_extra_sub_parts($finalEmail, $sourceMessage->parts); + } + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): is a new message or we are replacing mime")); + $this->addTextPartsMessage($finalEmail, $message); + if (isset($message->parts)) { + // We add extra parts from the new message + add_extra_sub_parts($finalEmail, $message->parts); + } } - // unset origmail - free memory - unset($origmail); + // We encode the final message + $boundary = '=_' . md5(rand() . microtime()); + $finalEmail = $finalEmail->encode($boundary); - } - - // remove carriage-returns from body - $body = str_replace("\r\n", "\n", $body); - - if (!$multipartmixed) { - if (!empty($forward_h_ct)) $headers .= "\nContent-Type: $forward_h_ct"; - if (!empty($forward_h_cte)) $headers .= "\nContent-Transfer-Encoding: $forward_h_cte"; - // if body was quoted-printable, convert it again - if (isset($message->headers["content-transfer-encoding"]) && strtolower($message->headers["content-transfer-encoding"]) == "quoted-printable") { - $body = quoted_printable_encode($body); + $finalHeaders = array('Mime-Version' => '1.0'); + // We copy all the non-existent headers, minus content_type + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): Copying new headers")); + foreach ($message->headers as $k => $v) { + if (strcasecmp($k, 'content-type') != 0 && strcasecmp($k, 'content-transfer-encoding') != 0 && strcasecmp($k, 'mime-version') != 0) { + if (!isset($finalHeaders[$k])) + $finalHeaders[ucwords($k)] = $v; + } } + foreach ($finalEmail['headers'] as $k => $v) { + if (!isset($finalHeaders[$k])) + $finalHeaders[$k] = $v; + } + + $finalBody = "This is a multi-part message in MIME format.\n" . $finalEmail['body']; + + unset($finalEmail); } - // more debugging - ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): parsed message: ". print_r($message,1)); - ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): headers: $headers"); - /* BEGIN fmbiete's contribution r1528, ZP-320 */ - if (isset($message->headers["subject"])) { - ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): subject: {$message->headers["subject"]}"); + unset($sourceMail); + unset($message); + unset($sourceMessage); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): Final mail to send:")); + foreach ($finalHeaders as $k => $v) + ZLog::Write(LOGLEVEL_WBXML, sprintf("%s: %s", $k, $v)); + foreach (preg_split("/((\r)?\n)/", $finalBody) as $bodyline) + ZLog::Write(LOGLEVEL_WBXML, sprintf("Body: %s", $bodyline)); + + $send = $this->sendMessage($fromaddr, $toaddr, $finalHeaders, $finalBody); + + if (isset($sm->saveinsent)) { + $this->saveSentMessage($finalHeaders, $finalBody); } else { - ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): subject: no subject set. Set to empty."); - $message->headers["subject"] = ""; // added by mku ZP-330 - } - /* END fmbiete's contribution r1528, ZP-320 */ - ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): body: $body"); - - if (!defined('IMAP_USE_IMAPMAIL') || IMAP_USE_IMAPMAIL == true) { - // changed by mku ZP-330 - $send = @imap_mail ( $toaddr, $message->headers["subject"], $body, $headers, $ccaddr, $bccaddr); - } - else { - if (!empty($ccaddr)) $headers .= "\nCc: $ccaddr"; - if (!empty($bccaddr)) $headers .= "\nBcc: $bccaddr"; - // changed by mku ZP-330 - $send = @mail ( $toaddr, $message->headers["subject"], $body, $headers, $envelopefrom ); + ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): Not saving in SentFolder"); } - // email sent? - if (!$send) - throw new StatusException(sprintf("BackendIMAP->SendMail(): The email could not be sent. Last IMAP-error: %s", imap_last_error()), SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED); - - // add message to the sent folder - // build complete headers - $headers .= "\nTo: $toaddr"; - $headers .= "\nSubject: " . $message->headers["subject"]; // changed by mku ZP-330 - - if (!defined('IMAP_USE_IMAPMAIL') || IMAP_USE_IMAPMAIL == true) { - if (!empty($ccaddr)) $headers .= "\nCc: $ccaddr"; - if (!empty($bccaddr)) $headers .= "\nBcc: $bccaddr"; - } - ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): complete headers: $headers"); - - $asf = false; - if ($this->sentID) { - $asf = $this->addSentMessage($this->sentID, $headers, $body); - } - else if (IMAP_SENTFOLDER) { - $asf = $this->addSentMessage(IMAP_SENTFOLDER, $headers, $body); - ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): Outgoing mail saved in configured 'Sent' folder '%s': %s", IMAP_SENTFOLDER, Utils::PrintAsString($asf))); - } - // No Sent folder set, try defaults - else { - ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): No Sent mailbox set"); - if($this->addSentMessage("INBOX.Sent", $headers, $body)) { - ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): Outgoing mail saved in 'INBOX.Sent'"); - $asf = true; - } - else if ($this->addSentMessage("Sent", $headers, $body)) { - ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): Outgoing mail saved in 'Sent'"); - $asf = true; - } - else if ($this->addSentMessage("Sent Items", $headers, $body)) { - ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail():IMAP-SendMail: Outgoing mail saved in 'Sent Items'"); - $asf = true; - } - } - - if (!$asf) { - ZLog::Write(LOGLEVEL_ERROR, "BackendIMAP->SendMail(): The email could not be saved to Sent Items folder. Check your configuration."); - } + unset($finalHeaders); + unset($finalBody); return $send; } + /** + * Add text parts to a mimepart object, with reply or forward tags + * + * @param Mail_mimePart $email reference to the object + * @param Mail_mimeDecode $message reference to the message + * @param Mail_mimeDecode $sourceMessage reference to the original message + * @param boolean $isReply true if it's a reply, false if it's a forward + * + * @access private + * @return void + */ + private function addTextParts(&$email, &$message, &$sourceMessage, $isReply = true) { + $htmlBody = $plainBody = ''; + Mail_mimeDecode::getBodyRecursive($message, "html", $htmlBody); + Mail_mimeDecode::getBodyRecursive($message, "plain", $plainBody); + $htmlSource = $plainSource = ''; + Mail_mimeDecode::getBodyRecursive($sourceMessage, "html", $htmlSource); + Mail_mimeDecode::getBodyRecursive($sourceMessage, "plain", $plainSource); + + $separator = ''; + if ($isReply) { + $separator = ">\r\n"; + $separatorHtml = "
"; + $separatorHtmlEnd = "
"; + } + else { + $separator = ""; + $separatorHtml = "
"; + $separatorHtmlEnd = "
"; + } + + $altEmail = new Mail_mimePart('', array('content_type' => 'multipart/alternative')); + + if (strlen($htmlBody) > 0) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextParts(): The message has HTML body")); + if (strlen($htmlSource) > 0) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextParts(): The original message had HTML body")); + $altEmail->addSubPart($htmlBody . $separatorHtml . $htmlSource . $separatorHtmlEnd, array('content_type' => 'text/html; charset=utf-8', 'encoding' => 'base64')); + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextParts(): The original message had not HTML body, we use original PLAIN body to create HTML")); + $altEmail->addSubPart($htmlBody . $separatorHtml . "

" . $plainSource . "

" . $separatorHtmlEnd, array('content_type' => 'text/html; charset=utf-8', 'encoding' => 'base64')); + } + } + if (strlen($plainBody) > 0) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextParts(): The message has PLAIN body")); + if (strlen($htmlSource) > 0) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextParts(): The original message had HTML body, we cast new PLAIN to HTML")); + $altEmail->addSubPart('

' . str_replace("\n", "
", str_replace("\r\n", "\n", $plainBody)) . "

" . $separatorHtml . $htmlSource . $separatorHtmlEnd, array('content_type' => 'text/html; charset=utf-8', 'encoding' => 'base64')); + } + if (strlen($plainSource) > 0) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextParts(): The original message had PLAIN body")); + $altEmail->addSubPart($plainBody . $separator . str_replace("\n", "\n> ", "> ".$plainSource), array('content_type' => 'text/plain; charset=utf-8', 'encoding' => 'base64')); + } + } + + $boundary = '=_' . md5(rand() . microtime()); + $altEmail = $altEmail->encode($boundary); + + $email->addSubPart($altEmail['body'], array('content_type' => 'multipart/alternative;'."\n".' boundary="'.$boundary.'"')); + + unset($altEmail); + + unset($htmlBody); + unset($htmlSource); + unset($plainBody); + unset($plainSource); + } + + /** + * Add text parts to a mimepart object + * + * @param Mail_mimePart $email reference to the object + * @param Mail_mimeDecode $message reference to the message + * + * @access private + * @return void + */ + private function addTextPartsMessage(&$email, &$message) { + $htmlBody = $plainBody = ''; + Mail_mimeDecode::getBodyRecursive($message, "html", $htmlBody); + Mail_mimeDecode::getBodyRecursive($message, "plain", $plainBody); + + $altEmail = new Mail_mimePart('', array('content_type' => 'multipart/alternative')); + + if (strlen($htmlBody) > 0) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextPartsMessage(): The message has HTML body")); + $altEmail->addSubPart($htmlBody, array('content_type' => 'text/html; charset=utf-8', 'encoding' => 'base64')); + } + if (strlen($plainBody) > 0) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextPartsMessage(): The message has PLAIN body")); + $altEmail->addSubPart($plainBody, array('content_type' => 'text/plain; charset=utf-8', 'encoding' => 'base64')); + } + + $boundary = '=_' . md5(rand() . microtime()); + $altEmail = $altEmail->encode($boundary); + + $email->addSubPart($altEmail['body'], array('content_type' => 'multipart/alternative;'."\n".' boundary="'.$boundary.'"')); + + unset($altEmail); + + unset($htmlBody); + unset($plainBody); + } + /** * Returns the waste basket * @@ -561,18 +496,26 @@ class BackendIMAP extends BackendDiff { list($folderid, $id, $part) = explode(":", $attname); - if (!$folderid || !$id || !$part) + if (!isset($folderid) || !isset($id) || !isset($part)) throw new StatusException(sprintf("BackendIMAP->GetAttachmentData('%s'): Error, attachment name key can not be parsed", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); // convert back to work on an imap-id $folderImapid = $this->getImapIdFromFolderId($folderid); - $this->imap_reopenFolder($folderImapid); + $this->imap_reopen_folder($folderImapid); $mail = @imap_fetchheader($this->mbox, $id, FT_UID) . @imap_body($this->mbox, $id, FT_PEEK | FT_UID); + if (empty($mail)) { + throw new StatusException(sprintf("BackendIMAP->GetAttachmentData('%s'): Error, message not found, maybe was moved", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + } + $mobj = new Mail_mimeDecode($mail); $message = $mobj->decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')); + if (!isset($message->parts)) { + throw new StatusException(sprintf("BackendIMAP->GetAttachmentData('%s'): Error, message without parts. Requesting part key: '%d'", $attname, $part), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + } + /* BEGIN fmbiete's contribution r1528, ZP-320 */ //trying parts $mparts = $message->parts; @@ -618,8 +561,6 @@ class BackendIMAP extends BackendDiff { * @return boolean */ public function HasChangesSink() { - $this->sinkfolders = array(); - $this->sinkstates = array(); return true; } @@ -634,16 +575,16 @@ class BackendIMAP extends BackendDiff { * @return boolean false if found can not be found */ public function ChangesSinkInitialize($folderid) { - ZLog::Write(LOGLEVEL_DEBUG, sprintf("IMAPBackend->ChangesSinkInitialize(): folderid '%s'", $folderid)); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->ChangesSinkInitialize(): folderid '%s'", $folderid)); $imapid = $this->getImapIdFromFolderId($folderid); - if ($imapid) { + if ($imapid !== false) { $this->sinkfolders[] = $imapid; - return true; + $this->changessinkinit = true; } - return false; + return $this->changessinkinit; } /** @@ -661,9 +602,17 @@ class BackendIMAP extends BackendDiff { $notifications = array(); $stopat = time() + $timeout - 1; + //We can get here and the ChangesSink not be initialized yet + if (!$this->changessinkinit) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP>ChangesSink - Not initialized ChangesSink, sleep and exit")); + // We sleep and do nothing else + sleep($timeout); + return $notifications; + } + while($stopat > time() && empty($notifications)) { - foreach ($this->sinkfolders as $imapid) { - $this->imap_reopenFolder($imapid); + foreach ($this->sinkfolders as $i => $imapid) { + $this->imap_reopen_folder($imapid); // courier-imap only cleares the status cache after checking @imap_check($this->mbox); @@ -675,12 +624,14 @@ class BackendIMAP extends BackendDiff { else { $newstate = "M:". $status->messages ."-R:". $status->recent ."-U:". $status->unseen; - if (! isset($this->sinkstates[$imapid]) ) + if (! isset($this->sinkstates[$imapid]) ) { $this->sinkstates[$imapid] = $newstate; + } if ($this->sinkstates[$imapid] != $newstate) { $notifications[] = $this->getFolderIdFromImapId($imapid); $this->sinkstates[$imapid] = $newstate; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->ChangesSink(): ChangesSink detected!!")); } } } @@ -773,42 +724,41 @@ class BackendIMAP extends BackendDiff { // compare on lowercase strings $lid = strtolower($imapid); // TODO WasteID or SentID could be saved for later ussage - if($lid == "inbox") { + if($lid == strtolower(IMAP_FOLDER_ROOT)) { $folder->parentid = "0"; // Root $folder->displayname = "Inbox"; $folder->type = SYNC_FOLDER_TYPE_INBOX; } // Zarafa IMAP-Gateway outputs - else if($lid == "drafts") { + else if($lid == "drafts" || $lid == strtolower(IMAP_FOLDER_DRAFT)) { $folder->parentid = "0"; $folder->displayname = "Drafts"; $folder->type = SYNC_FOLDER_TYPE_DRAFTS; } - else if($lid == "trash") { + else if(($lid == "trash" || $lid == "deleted messages" || $lid == strtolower(IMAP_FOLDER_TRASH)) && ($this->wasteID === false || $this->wasteID == $id)) { $folder->parentid = "0"; $folder->displayname = "Trash"; $folder->type = SYNC_FOLDER_TYPE_WASTEBASKET; $this->wasteID = $id; } - else if($lid == "sent" || $lid == "sent items" || $lid == IMAP_SENTFOLDER) { + else if($lid == "sent" || $lid == "sent items" || $lid == "sent messages" || $lid == strtolower(IMAP_FOLDER_SENT)) { $folder->parentid = "0"; $folder->displayname = "Sent"; $folder->type = SYNC_FOLDER_TYPE_SENTMAIL; $this->sentID = $id; } - // courier-imap outputs and cyrus-imapd outputs - else if($lid == "inbox.drafts" || $lid == "inbox/drafts") { + else if($lid == strtolower(IMAP_FOLDER_ROOT) . $this->serverdelimiter . "drafts" || $lid == strtolower(IMAP_FOLDER_DRAFT)) { $folder->parentid = $this->convertImapId($fhir[0]); $folder->displayname = "Drafts"; $folder->type = SYNC_FOLDER_TYPE_DRAFTS; } - else if($lid == "inbox.trash" || $lid == "inbox/trash") { + else if(($lid == strtolower(IMAP_FOLDER_ROOT) . $this->serverdelimiter . "trash" || $lid == strtolower(IMAP_FOLDER_ROOT) . $this->serverdelimiter . "deleted messages" || $lid == strtolower(IMAP_FOLDER_TRASH)) && ($this->wasteID === false || $this->wasteID == $id)) { $folder->parentid = $this->convertImapId($fhir[0]); $folder->displayname = "Trash"; $folder->type = SYNC_FOLDER_TYPE_WASTEBASKET; $this->wasteID = $id; } - else if($lid == "inbox.sent" || $lid == "inbox/sent") { + else if($lid == strtolower(IMAP_FOLDER_ROOT) . $this->serverdelimiter . "sent" || $lid == strtolower(IMAP_FOLDER_ROOT) . $this->serverdelimiter . "sent messages" || $lid == strtolower(IMAP_FOLDER_SENT)) { $folder->parentid = $this->convertImapId($fhir[0]); $folder->displayname = "Sent"; $folder->type = SYNC_FOLDER_TYPE_SENTMAIL; @@ -872,11 +822,11 @@ class BackendIMAP extends BackendDiff { ZLog::Write(LOGLEVEL_INFO, sprintf("BackendIMAP->ChangeFolder('%s','%s','%s','%s')", $folderid, $oldid, $displayname, $type)); // go to parent mailbox - $this->imap_reopenFolder($folderid); + $this->imap_reopen_folder($folderid); // build name for new mailboxBackendMaildir $displayname = Utils::Utf7_iconv_encode(Utils::Utf8_to_utf7($displayname)); - $newname = $this->server . $folderid . $this->serverdelimiter . $displayname; + $newname = $this->server . $this->getImapIdFromFolderId($folderid) . $this->serverdelimiter . $displayname; $csts = false; // if $id is set => rename mailbox, otherwise create @@ -930,7 +880,7 @@ class BackendIMAP extends BackendDiff { throw new StatusException("Folderid not found in cache", SYNC_STATUS_FOLDERHIERARCHYCHANGED); $messages = array(); - $this->imap_reopenFolder($folderid, true); + $this->imap_reopen_folder($folderid, true); $sequence = "1:*"; if ($cutoffdate > 0) { @@ -942,7 +892,10 @@ class BackendIMAP extends BackendDiff { $overviews = @imap_fetch_overview($this->mbox, $sequence); if (!$overviews || !is_array($overviews)) { - ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->GetMessageList('%s','%s'): Failed to retrieve overview: %s",$folderid, $cutoffdate, imap_last_error())); + $error = imap_last_error(); + if (strlen($error) > 0 && imap_num_msg($this->mbox) > 0) { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->GetMessageList('%s','%s'): Failed to retrieve overview: %s", $folderid, $cutoffdate, imap_last_error())); + } return $messages; } @@ -963,11 +916,22 @@ class BackendIMAP extends BackendDiff { $message = array(); $message["mod"] = $date; $message["id"] = $overview->uid; - // 'seen' aka 'read' is the only flag we want to know about - $message["flags"] = 0; - if(array_key_exists( "seen", $vars) && $overview->seen) + // 'seen' aka 'read' + if(array_key_exists("seen", $vars) && $overview->seen) { $message["flags"] = 1; + } + else { + $message["flags"] = 0; + } + + // 'flagged' aka 'FollowUp' aka 'starred' + if (array_key_exists("flagged", $vars) && $overview->flagged) { + $message["star"] = 1; + } + else { + $message["star"] = 0; + } array_push($messages, $message); } @@ -997,9 +961,13 @@ class BackendIMAP extends BackendDiff { $stat = $this->StatMessage($folderid, $id); if ($stat) { - $this->imap_reopenFolder($folderImapid); + $this->imap_reopen_folder($folderImapid); $mail = @imap_fetchheader($this->mbox, $id, FT_UID) . @imap_body($this->mbox, $id, FT_PEEK | FT_UID); + if (empty($mail)) { + throw new StatusException(sprintf("BackendIMAP->GetMessage(): Error, message not found, maybe was moved"), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + } + $mobj = new Mail_mimeDecode($mail); $message = $mobj->decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')); @@ -1013,9 +981,14 @@ class BackendIMAP extends BackendDiff { } ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage - getBodyPreferenceBestMatch: %d", $bpReturnType)); + if (is_smime($message)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage - Message is SMIME, forcing to work with MIME")); + $bpReturnType = SYNC_BODYPREFERENCE_MIME; + } + //Get body data - $this->getBodyRecursive($message, "plain", $plainBody); - $this->getBodyRecursive($message, "html", $htmlBody); + Mail_mimeDecode::getBodyRecursive($message, "plain", $plainBody); + Mail_mimeDecode::getBodyRecursive($message, "html", $htmlBody); if ($plainBody == "") { $plainBody = Utils::ConvertHtmlToText($htmlBody); } @@ -1039,45 +1012,50 @@ class BackendIMAP extends BackendDiff { } break; case SYNC_BODYPREFERENCE_MIME: - //We don't need to create a new MIME mail, we already have one!! - $output->asbody->data = $mail; + if (is_smime($message)) { + $output->asbody->data = $mail; + } + else { + $output->asbody->data = build_mime_message($message); + } break; case SYNC_BODYPREFERENCE_RTF: ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->GetMessage RTF Format NOT CHECKED"); $output->asbody->data = base64_encode($plainBody); break; } - // truncate body, if requested - if(strlen($output->asbody->data) > $truncsize) { + // truncate body, if requested, but never truncate MIME messages + if($bpReturnType !== SYNC_BODYPREFERENCE_MIME && strlen($output->asbody->data) > $truncsize) { $output->asbody->data = Utils::Utf8_truncate($output->asbody->data, $truncsize); $output->asbody->truncated = 1; } + else { + $output->asbody->truncated = 0; + } $output->asbody->type = $bpReturnType; - $output->nativebodytype = $bpReturnType; + if ($bpReturnType == SYNC_BODYPREFERENCE_MIME) { + $output->nativebodytype = SYNC_BODYPREFERENCE_PLAIN; + // http://msdn.microsoft.com/en-us/library/ee220018%28v=exchg.80%29.aspx + } + else { + $output->nativebodytype = $bpReturnType; + } $output->asbody->estimatedDataSize = strlen($output->asbody->data); $bpo = $contentparameters->BodyPreference($output->asbody->type); if (Request::GetProtocolVersion() >= 14.0 && $bpo->GetPreview()) { $output->asbody->preview = Utils::Utf8_truncate(Utils::ConvertHtmlToText($plainBody), $bpo->GetPreview()); } - else { - $output->asbody->truncated = 0; - } } /* END fmbiete's contribution r1528, ZP-320 */ else { // ASV_2.5 $output->bodytruncated = 0; /* BEGIN fmbiete's contribution r1528, ZP-320 */ if ($bpReturnType == SYNC_BODYPREFERENCE_MIME) { - if (strlen($mail) > $truncsize) { - $output->mimedata = Utils::Utf8_truncate($mail, $truncsize); - $output->mimetruncated = 1; - } - else { - $output->mimetruncated = 0; - $output->mimedata = $mail; - } + // truncate body, if requested, but never truncate MIME messages + $output->mimetruncated = 0; + $output->mimedata = $mail; $output->mimesize = strlen($output->mimedata); } else { @@ -1096,7 +1074,12 @@ class BackendIMAP extends BackendDiff { } $output->datereceived = isset($message->headers["date"]) ? $this->cleanupDate($message->headers["date"]) : null; - $output->messageclass = "IPM.Note"; + if (is_smime($message)) { + $output->messageclass = "IPM.Note.SMIME.MultipartSigned"; + } + else { + $output->messageclass = "IPM.Note"; + } $output->subject = isset($message->headers["subject"]) ? $message->headers["subject"] : ""; $output->read = $stat["flags"]; $output->from = isset($message->headers["from"]) ? $message->headers["from"] : null; @@ -1104,12 +1087,35 @@ class BackendIMAP extends BackendDiff { /* BEGIN fmbiete's contribution r1528, ZP-320 */ if (isset($message->headers["thread-topic"])) { $output->threadtopic = $message->headers["thread-topic"]; + /* + //FIXME: Conversation support, get conversationid and conversationindex good values + if (Request::GetProtocolVersion() >= 14.0) { + // since the conversationid must be unique for a thread we could use the threadtopic in base64 minus the == + $output->conversationid = strtoupper(str_replace("=", "", base64_encode($output->threadtopic))); + if (isset($message->headers["thread-index"])) + $output->conversationindex = strtoupper($message->headers["thread-index"]); + } + */ + } + else { + $output->threadtopic = $output->subject; } // Language Code Page ID: http://msdn.microsoft.com/en-us/library/windows/desktop/dd317756%28v=vs.85%29.aspx $output->internetcpid = INTERNET_CPID_UTF8; if (Request::GetProtocolVersion() >= 12.0) { $output->contentclass = "urn:content-classes:message"; + + $output->flag = new SyncMailFlags(); + if (isset($stat["star"]) && $stat["star"]) { + //flagstatus 0: clear, 1: complete, 2: active + $output->flag->flagstatus = SYNC_FLAGSTATUS_ACTIVE; + //flagtype: for follow up + $output->flag->flagtype = "FollowUp"; + } + else { + $output->flag->flagstatus = SYNC_FLAGSTATUS_CLEAR; + } } /* END fmbiete's contribution r1528, ZP-320 */ @@ -1119,32 +1125,45 @@ class BackendIMAP extends BackendDiff { $toaddr = $Mail_RFC822->parseAddressList($message->headers["to"]); if(isset($message->headers["cc"])) $ccaddr = $Mail_RFC822->parseAddressList($message->headers["cc"]); - if(isset($message->headers["reply_to"])) - $replytoaddr = $Mail_RFC822->parseAddressList($message->headers["reply_to"]); + if(isset($message->headers["reply-to"])) + $replytoaddr = $Mail_RFC822->parseAddressList($message->headers["reply-to"]); $output->to = array(); $output->cc = array(); $output->reply_to = array(); foreach(array("to" => $toaddr, "cc" => $ccaddr, "reply_to" => $replytoaddr) as $type => $addrlist) { - foreach($addrlist as $addr) { - $address = $addr->mailbox . "@" . $addr->host; - $name = $addr->personal; - - if (!isset($output->displayto) && $name != "") - $output->displayto = $name; - - if($name == "" || $name == $address) - $fulladdr = w2u($address); + if ($addrlist === false) { + //If we couldn't parse the addresslist we put the raw header (decoded) + if ($type == "reply_to") { + array_push($output->$type, $message->headers["reply-to"]); + } else { - if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') { - $fulladdr = "\"" . w2u($name) ."\" <" . w2u($address) . ">"; - } - else { - $fulladdr = w2u($name) ." <" . w2u($address) . ">"; + array_push($output->$type, $message->headers[$type]); + } + } + else { + foreach($addrlist as $addr) { + if (isset($addr->mailbox) && isset($addr->host) && isset($addr->personal)) { + $address = $addr->mailbox . "@" . $addr->host; + $name = $addr->personal; + + if (!isset($output->displayto) && $name != "") + $output->displayto = $name; + + if($name == "" || $name == $address) + $fulladdr = $address; + else { + if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') { + $fulladdr = "\"" . $name ."\" <" . $address . ">"; + } + else { + $fulladdr = $name ." <" . $address . ">"; + } + } + + array_push($output->$type, $fulladdr); } } - - array_push($output->$type, $fulladdr); } } @@ -1155,75 +1174,98 @@ class BackendIMAP extends BackendDiff { //AS 0 - low, 1 - normal, 2 - important if ($mimeImportance > 3) $output->importance = 0; - if ($mimeImportance == 3) + elseif ($mimeImportance == 3) $output->importance = 1; - if ($mimeImportance < 3) + elseif ($mimeImportance < 3) $output->importance = 2; - } else { /* fmbiete's contribution r1528, ZP-320 */ + } + else { /* fmbiete's contribution r1528, ZP-320 */ $output->importance = 1; } - // Attachments are not needed for MIME messages - if($bpReturnType != SYNC_BODYPREFERENCE_MIME && isset($message->parts)) { + // Attachments are also needed for MIME messages + if(isset($message->parts)) { $mparts = $message->parts; for ($i=0; $ictype_primary == "multipart" && ($part->ctype_secondary == "mixed" || $part->ctype_secondary == "alternative" || $part->ctype_secondary == "related")) { + if ((isset($part->ctype_primary) && $part->ctype_primary == "multipart") && (isset($part->ctype_secondary) && ($part->ctype_secondary == "mixed" || $part->ctype_secondary == "alternative" || $part->ctype_secondary == "related"))) { foreach($part->parts as $spart) $mparts[] = $spart; continue; } - //add part as attachment if it's disposition indicates so or if it is not a text part - if ((isset($part->disposition) && ($part->disposition == "attachment" || $part->disposition == "inline")) || - (isset($part->ctype_primary) && $part->ctype_primary != "text")) { + if (is_calendar($part)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage - text/calendar part found, trying to convert")); + $output->meetingrequest = new SyncMeetingRequest(); + $this->parseMeetingCalendar($part, $output); + } + else { + //add part as attachment if it's disposition indicates so or if it is not a text part + if ((isset($part->disposition) && ($part->disposition == "attachment" || $part->disposition == "inline")) || + (isset($part->ctype_primary) && $part->ctype_primary != "text")) { - if(isset($part->d_parameters['filename'])) - $attname = $part->d_parameters['filename']; - else if(isset($part->ctype_parameters['name'])) - $attname = $part->ctype_parameters['name']; - else if(isset($part->headers['content-description'])) - $attname = $part->headers['content-description']; - else $attname = "unknown attachment"; + if (isset($part->d_parameters['filename'])) + $attname = $part->d_parameters['filename']; + else if (isset($part->ctype_parameters['name'])) + $attname = $part->ctype_parameters['name']; + else if (isset($part->headers['content-description'])) + $attname = $part->headers['content-description']; + else $attname = "unknown attachment"; - /* BEGIN fmbiete's contribution r1528, ZP-320 */ - if (Request::GetProtocolVersion() >= 12.0) { - if (!isset($output->asattachments) || !is_array($output->asattachments)) - $output->asattachments = array(); + /* BEGIN fmbiete's contribution r1528, ZP-320 */ + if (Request::GetProtocolVersion() >= 12.0) { + if (!isset($output->asattachments) || !is_array($output->asattachments)) + $output->asattachments = array(); - $attachment = new SyncBaseAttachment(); + $attachment = new SyncBaseAttachment(); - $attachment->estimatedDataSize = isset($part->d_parameters['size']) ? $part->d_parameters['size'] : isset($part->body) ? strlen($part->body) : 0; + $attachment->estimatedDataSize = isset($part->d_parameters['size']) ? $part->d_parameters['size'] : isset($part->body) ? strlen($part->body) : 0; - $attachment->displayname = $attname; - $attachment->filereference = $folderid . ":" . $id . ":" . $i; - $attachment->method = 1; //Normal attachment - $attachment->contentid = isset($part->headers['content-id']) ? str_replace("<", "", str_replace(">", "", $part->headers['content-id'])) : ""; - if (isset($part->disposition) && $part->disposition == "inline") { - $attachment->isinline = 1; + $attachment->displayname = $attname; + $attachment->filereference = $folderid . ":" . $id . ":" . $i; + $attachment->method = 1; //Normal attachment + $attachment->contentid = isset($part->headers['content-id']) ? str_replace("<", "", str_replace(">", "", $part->headers['content-id'])) : ""; + if (isset($part->disposition) && $part->disposition == "inline") { + $attachment->isinline = 1; + // We try to fix the name for the inline file. + // FIXME: This is a dirty hack as the used in the Zarafa backend, if you have a better method let me know! + if (isset($part->ctype_primary) && isset($part->ctype_secondary)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage - Guessing extension for inline attachment [primary_type %s secondary_type %s]", $part->ctype_primary, $part->ctype_secondary)); + if (isset(BackendIMAP::$mimeTypes[$part->ctype_primary.'/'.$part->ctype_secondary])) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage - primary_type %s secondary_type %s", $part->ctype_primary, $part->ctype_secondary)); + $attachment->displayname = "inline_".$i.".".BackendIMAP::$mimeTypes[$part->ctype_primary.'/'.$part->ctype_secondary]; + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage - no extension found in /etc/mime.types'!!")); + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage - no primary_type or secondary_type")); + } + } + else { + $attachment->isinline = 0; + } + + array_push($output->asattachments, $attachment); } - else { - $attachment->isinline = 0; + else { //ASV_2.5 + if (!isset($output->attachments) || !is_array($output->attachments)) + $output->attachments = array(); + + $attachment = new SyncAttachment(); + + $attachment->attsize = isset($part->d_parameters['size']) ? $part->d_parameters['size'] : isset($part->body) ? strlen($part->body) : 0; + + $attachment->displayname = $attname; + $attachment->attname = $folderid . ":" . $id . ":" . $i; + $attachment->attmethod = 1; + $attachment->attoid = isset($part->headers['content-id']) ? str_replace("<", "", str_replace(">", "", $part->headers['content-id'])) : ""; + + array_push($output->attachments, $attachment); } - - array_push($output->asattachments, $attachment); + /* END fmbiete's contribution r1528, ZP-320 */ } - else { //ASV_2.5 - if (!isset($output->attachments) || !is_array($output->attachments)) - $output->attachments = array(); - - $attachment = new SyncAttachment(); - - $attachment->attsize = isset($part->d_parameters['size']) ? $part->d_parameters['size'] : isset($part->body) ? strlen($part->body) : 0; - - $attachment->displayname = $attname; - $attachment->attname = $folderid . ":" . $id . ":" . $i; - $attachment->attmethod = 1; - $attachment->attoid = isset($part->headers['content-id']) ? str_replace("<", "", str_replace(">", "", $part->headers['content-id'])) : ""; - - array_push($output->attachments, $attachment); - } - /* END fmbiete's contribution r1528, ZP-320 */ } } } @@ -1249,7 +1291,7 @@ class BackendIMAP extends BackendDiff { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->StatMessage('%s','%s')", $folderid, $id)); $folderImapid = $this->getImapIdFromFolderId($folderid); - $this->imap_reopenFolder($folderImapid); + $this->imap_reopen_folder($folderImapid); $overview = @imap_fetch_overview( $this->mbox , $id , FT_UID); if (!$overview) { @@ -1266,11 +1308,22 @@ class BackendIMAP extends BackendDiff { $entry = array(); $entry["mod"] = (array_key_exists( "date", $vars)) ? $overview[0]->date : ""; $entry["id"] = $overview[0]->uid; - // 'seen' aka 'read' is the only flag we want to know about - $entry["flags"] = 0; - if(array_key_exists( "seen", $vars) && $overview[0]->seen) + // 'seen' aka 'read' + if (array_key_exists("seen", $vars) && $overview[0]->seen) { $entry["flags"] = 1; + } + else { + $entry["flags"] = 0; + } + + // 'flagged' aka 'FollowUp' aka 'starred' + if (array_key_exists("flagged", $vars) && $overview[0]->flagged) { + $entry["star"] = 1; + } + else { + $entry["star"] = 0; + } return $entry; } @@ -1282,49 +1335,45 @@ class BackendIMAP extends BackendDiff { * @param string $folderid id of the folder * @param string $id id of the message * @param SyncXXX $message the SyncObject containing a message - * @param ContentParameters $contentParameters + * @param ContentParameters $contentparameters * * @access public * @return array same return value as StatMessage() * @throws StatusException could throw specific SYNC_STATUS_* exceptions */ - public function ChangeMessage($folderid, $id, $message, $contentParameters) { + public function ChangeMessage($folderid, $id, $message, $contentparameters) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->ChangeMessage('%s','%s','%s')", $folderid, $id, get_class($message))); // TODO this could throw several StatusExceptions like e.g. SYNC_STATUS_OBJECTNOTFOUND, SYNC_STATUS_SYNCCANNOTBECOMPLETED - // TODO SyncInterval check + ContentParameters - // see https://jira.zarafa.com/browse/ZP-258 for details - // before changing the message, it should be checked if the message is in the SyncInterval - // to determine the cutoffdate use Utils::GetCutOffDate($contentparameters->GetFilterType()); - // if the message is not in the interval an StatusException with code SYNC_STATUS_SYNCCANNOTBECOMPLETED should be thrown - - /* BEGIN fmbiete's contribution r1529, ZP-321 */ if (isset($message->flag)) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->ChangeMessage('Setting flag')")); $folderImapid = $this->getImapIdFromFolderId($folderid); + $this->imap_reopen_folder($folderImapid); - $this->imap_reopenFolder($folderImapid); + if ($this->imap_inside_cutoffdate(Utils::GetCutOffDate($contentparameters->GetFilterType()), $id)) { + if (isset($message->flag->flagstatus) && $message->flag->flagstatus == 2) { + ZLog::Write(LOGLEVEL_DEBUG, "Set On FollowUp -> IMAP Flagged"); + $status = @imap_setflag_full($this->mbox, $id, "\\Flagged", ST_UID); + } + else { + ZLog::Write(LOGLEVEL_DEBUG, "Clearing Flagged"); + $status = @imap_clearflag_full($this->mbox, $id, "\\Flagged", ST_UID); + } - if (isset($message->flag->flagstatus) && $message->flag->flagstatus == 2) { - ZLog::Write(LOGLEVEL_DEBUG, "Set On FollowUp -> IMAP Flagged"); - $status = @imap_setflag_full($this->mbox, $id, "\\Flagged",ST_UID); + if ($status) { + ZLog::Write(LOGLEVEL_DEBUG, "Flagged changed"); + } + else { + ZLog::Write(LOGLEVEL_DEBUG, "Flagged failed"); + } } else { - ZLog::Write(LOGLEVEL_DEBUG, "Clearing Flagged"); - $status = @imap_clearflag_full ( $this->mbox, $id, "\\Flagged", ST_UID); - } - - if ($status) { - ZLog::Write(LOGLEVEL_DEBUG, "Flagged changed"); - } - else { - ZLog::Write(LOGLEVEL_DEBUG, "Flagged failed"); + throw new StatusException(sprintf("BackendIMAP->ChangeMessage(): Message is outside the sync range"), SYNC_STATUS_SYNCCANNOTBECOMPLETED); } } return $this->StatMessage($folderid, $id); - /* END fmbiete's contribution r1529, ZP-321 */ } /** @@ -1333,30 +1382,63 @@ class BackendIMAP extends BackendDiff { * @param string $folderid id of the folder * @param string $id id of the message * @param int $flags read flag of the message - * @param ContentParameters $contentParameters + * @param ContentParameters $contentparameters * * @access public * @return boolean status of the operation * @throws StatusException could throw specific SYNC_STATUS_* exceptions */ - public function SetReadFlag($folderid, $id, $flags, $contentParameters) { + public function SetReadFlag($folderid, $id, $flags, $contentparameters) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SetReadFlag('%s','%s','%s')", $folderid, $id, $flags)); + $folderImapid = $this->getImapIdFromFolderId($folderid); + $this->imap_reopen_folder($folderImapid); - // TODO SyncInterval check + ContentParameters - // see https://jira.zarafa.com/browse/ZP-258 for details - // before setting the read flag, it should be checked if the message is in the SyncInterval - // to determine the cutoffdate use Utils::GetCutOffDate($contentparameters->GetFilterType()); - // if the message is not in the interval an StatusException with code SYNC_STATUS_OBJECTNOTFOUND should be thrown + if ($this->imap_inside_cutoffdate(Utils::GetCutOffDate($contentparameters->GetFilterType()), $id)) { + if ($flags == 0) { + // set as "Unseen" (unread) + $status = @imap_clearflag_full($this->mbox, $id, "\\Seen", ST_UID); + } else { + // set as "Seen" (read) + $status = @imap_setflag_full($this->mbox, $id, "\\Seen", ST_UID); + } + } + else { + throw new StatusException(sprintf("BackendIMAP->SetReadFlag(): Message is outside the sync range"), SYNC_STATUS_OBJECTNOTFOUND); + } - $this->imap_reopenFolder($folderImapid); + return $status; + } - if ($flags == 0) { - // set as "Unseen" (unread) - $status = @imap_clearflag_full ( $this->mbox, $id, "\\Seen", ST_UID); - } else { - // set as "Seen" (read) - $status = @imap_setflag_full($this->mbox, $id, "\\Seen",ST_UID); + /** + * Changes the 'star' flag of a message on disk + * + * @param string $folderid id of the folder + * @param string $id id of the message + * @param int $flags read flag of the message + * @param ContentParameters $contentparameters + * + * @access public + * @return boolean status of the operation + * @throws StatusException could throw specific SYNC_STATUS_* exceptions + */ + public function SetStarFlag($folderid, $id, $flags, $contentparameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SetStarFlag('%s','%s','%s')", $folderid, $id, $flags)); + + $folderImapid = $this->getImapIdFromFolderId($folderid); + $this->imap_reopen_folder($folderImapid); + + if ($this->imap_inside_cutoffdate(Utils::GetCutOffDate($contentparameters->GetFilterType()), $id)) { + if ($flags == 0) { + // set as "UnFlagged" (unstarred) + $status = @imap_clearflag_full($this->mbox, $id, "\\Flagged", ST_UID); + } else { + // set as "Flagged" (starred) + $status = @imap_setflag_full($this->mbox, $id, "\\Flagged", ST_UID); + } + } + else { + throw new StatusException(sprintf("BackendIMAP->SetStarFlag(): Message is outside the sync range"), SYNC_STATUS_OBJECTNOTFOUND); } return $status; @@ -1367,28 +1449,28 @@ class BackendIMAP extends BackendDiff { * * @param string $folderid id of the folder * @param string $id id of the message - * @param ContentParameters $contentParameters + * @param ContentParameters $contentparameters * * @access public * @return boolean status of the operation * @throws StatusException could throw specific SYNC_STATUS_* exceptions */ - public function DeleteMessage($folderid, $id, $contentParameters) { + public function DeleteMessage($folderid, $id, $contentparameters) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->DeleteMessage('%s','%s')", $folderid, $id)); + $folderImapid = $this->getImapIdFromFolderId($folderid); + $this->imap_reopen_folder($folderImapid); - // TODO SyncInterval check + ContentParameters - // see https://jira.zarafa.com/browse/ZP-258 for details - // before deleting the message, it should be checked if the message is in the SyncInterval - // to determine the cutoffdate use Utils::GetCutOffDate($contentparameters->GetFilterType()); - // if the message is not in the interval an StatusException with code SYNC_STATUS_OBJECTNOTFOUND should be thrown + if ($this->imap_inside_cutoffdate(Utils::GetCutOffDate($contentparameters->GetFilterType()), $id)) { + $s1 = @imap_delete ($this->mbox, $id, FT_UID); + $s11 = @imap_setflag_full($this->mbox, $id, "\\Deleted", FT_UID); + $s2 = @imap_expunge($this->mbox); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->DeleteMessage('%s','%s'): result: s-delete: '%s' s-expunge: '%s' setflag: '%s'", $folderid, $id, $s1, $s2, $s11)); + } + else { + throw new StatusException(sprintf("BackendIMAP->DeleteMessage(): Message is outside the sync range"), SYNC_STATUS_OBJECTNOTFOUND); + } - $this->imap_reopenFolder($folderImapid); - $s1 = @imap_delete ($this->mbox, $id, FT_UID); - $s11 = @imap_setflag_full($this->mbox, $id, "\\Deleted", FT_UID); - $s2 = @imap_expunge($this->mbox); - - ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->DeleteMessage('%s','%s'): result: s-delete: '%s' s-expunge: '%s' setflag: '%s'", $folderid, $id, $s1, $s2, $s11)); return ($s1 && $s2 && $s11); } @@ -1399,72 +1481,433 @@ class BackendIMAP extends BackendDiff { * @param string $folderid id of the source folder * @param string $id id of the message * @param string $newfolderid id of the destination folder - * @param ContentParameters $contentParameters + * @param ContentParameters $contentparameters * * @access public * @return boolean status of the operation * @throws StatusException could throw specific SYNC_MOVEITEMSSTATUS_* exceptions */ - public function MoveMessage($folderid, $id, $newfolderid, $contentParameters) { + public function MoveMessage($folderid, $id, $newfolderid, $contentparameters) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->MoveMessage('%s','%s','%s')", $folderid, $id, $newfolderid)); $folderImapid = $this->getImapIdFromFolderId($folderid); $newfolderImapid = $this->getImapIdFromFolderId($newfolderid); - // TODO SyncInterval check + ContentParameters - // see https://jira.zarafa.com/browse/ZP-258 for details - // before moving the message, it should be checked if the message is in the SyncInterval - // to determine the cutoffdate use Utils::GetCutOffDate($contentparameters->GetFilterType()); - // if the message is not in the interval an StatusException with code SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID should be thrown - - $this->imap_reopenFolder($folderImapid); - - // TODO this should throw a StatusExceptions on errors like SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST,SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID,SYNC_MOVEITEMSSTATUS_CANNOTMOVE - - // read message flags - $overview = @imap_fetch_overview ( $this->mbox , $id, FT_UID); - - if (!$overview) - throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, unable to retrieve overview of source message: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); - else { - // get next UID for destination folder - // when moving a message we have to announce through ActiveSync the new messageID in the - // destination folder. This is a "guessing" mechanism as IMAP does not inform that value. - // when lots of simultaneous operations happen in the destination folder this could fail. - // in the worst case the moved message is displayed twice on the mobile. - $destStatus = imap_status($this->mbox, $this->server . $newfolderImapid, SA_ALL); - if (!$destStatus) - throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, unable to open destination folder: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); - - $newid = $destStatus->uidnext; - - // move message - $s1 = imap_mail_move($this->mbox, $id, $newfolderImapid, CP_UID); - if (! $s1) - throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, copy to destination folder failed: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); - - - // delete message in from-folder - $s2 = imap_expunge($this->mbox); - - // open new folder - $stat = $this->imap_reopenFolder($newfolderImapid); - if (! $s1) - throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, openeing the destination folder: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); - - - // remove all flags - $s3 = @imap_clearflag_full ($this->mbox, $newid, "\\Seen \\Answered \\Flagged \\Deleted \\Draft", FT_UID); - $newflags = ""; - if ($overview[0]->seen) $newflags .= "\\Seen"; - if ($overview[0]->flagged) $newflags .= " \\Flagged"; - if ($overview[0]->answered) $newflags .= " \\Answered"; - $s4 = @imap_setflag_full ($this->mbox, $newid, $newflags, FT_UID); - - ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): result s-move: '%s' s-expunge: '%s' unset-Flags: '%s' set-Flags: '%s'", $folderid, $id, $newfolderid, Utils::PrintAsString($s1), Utils::PrintAsString($s2), Utils::PrintAsString($s3), Utils::PrintAsString($s4))); - - // return the new id "as string"" - return $newid . ""; + if ($folderImapid == $newfolderImapid) { + throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, destination folder is source folder. Canceling the move.", $folderid, $id, $newfolderid), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST); } + + $this->imap_reopen_folder($folderImapid); + + if ($this->imap_inside_cutoffdate(Utils::GetCutOffDate($contentparameters->GetFilterType()), $id)) { + // read message flags + $overview = @imap_fetch_overview($this->mbox, $id, FT_UID); + + if (!is_array($overview)) { + throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, unable to retrieve overview of source message: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); + } + else { + // get next UID for destination folder + // when moving a message we have to announce through ActiveSync the new messageID in the + // destination folder. This is a "guessing" mechanism as IMAP does not inform that value. + // when lots of simultaneous operations happen in the destination folder this could fail. + // in the worst case the moved message is displayed twice on the mobile. + $destStatus = imap_status($this->mbox, $this->server . $newfolderImapid, SA_ALL); + if (!$destStatus) + throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, unable to open destination folder: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); + + $newid = $destStatus->uidnext; + + // move message + $s1 = imap_mail_move($this->mbox, $id, $newfolderImapid, CP_UID); + if (!$s1) + throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, copy to destination folder failed: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); + + + // delete message in from-folder + $s2 = imap_expunge($this->mbox); + + // open new folder + $stat = $this->imap_reopen_folder($newfolderImapid); + if (! $s1) + throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, opening the destination folder: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); + + + // remove all flags + $s3 = @imap_clearflag_full($this->mbox, $newid, "\\Seen \\Answered \\Flagged \\Deleted \\Draft", FT_UID); + $newflags = ""; + if ($overview[0]->seen) + $newflags .= "\\Seen"; + if ($overview[0]->flagged) + $newflags .= " \\Flagged"; + if ($overview[0]->answered) + $newflags .= " \\Answered"; + $s4 = @imap_setflag_full ($this->mbox, $newid, $newflags, FT_UID); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): result s-move: '%s' s-expunge: '%s' unset-Flags: '%s' set-Flags: '%s'", $folderid, $id, $newfolderid, Utils::PrintAsString($s1), Utils::PrintAsString($s2), Utils::PrintAsString($s3), Utils::PrintAsString($s4))); + + // return the new id "as string" + return $newid . ""; + } + } + else { + throw new StatusException(sprintf("BackendIMAP->MoveMessage(): Message is outside the sync range"), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); + } + } + + + /** + * Processes a response to a meeting request. + * + * @param string $requestid id of the object containing the request + * @param string $folderid id of the parent folder of $requestid + * @param string $response + * + * @access public + * @return string id of the created/updated calendar obj + * @throws StatusException + */ + public function MeetingResponse($requestid, $folderid, $response) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->MeetingResponse('%s','%s','%s')", $requestid, $folderid, $response)); + + $folderImapid = $this->getImapIdFromFolderId($folderid); + $this->imap_reopen_folder($folderImapid); + $mail = @imap_fetchheader($this->mbox, $requestid, FT_UID) . @imap_body($this->mbox, $requestid, FT_PEEK | FT_UID); + + if (empty($mail)) { + throw new StatusException(sprintf("BackendIMAP->MeetingResponse(): Error, message not found, maybe was moved"), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + } + + $mobj = new Mail_mimeDecode($mail); + unset($mail); + $message = $mobj->decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')); + unset($mobj); + + $Mail_RFC822 = new Mail_RFC822(); + $from_header = $this->getDefaultFromValue(); + $fromaddr = $this->parseAddr($Mail_RFC822->parseAddressList($from_header)); + $to_header = ""; + if (isset($message->headers["from"])) { + $to_header = $message->headers["from"]; + } + else { + if (isset($message->headers["return-path"])) { + $to_header = $message->headers["return-path"]; + } + else { + throw new StatusException(sprintf("BackendIMAP->MeetingResponse(): Error, no reply address"), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + } + } + $toaddr = $this->parseAddr($Mail_RFC822->parseAddressList($to_header)); + if (isset($message->headers["subject"])) { + $subject_header = $message->headers["subject"]; + } + else { + $subject_header = ""; + } + + $body_part = null; + if(isset($message->parts)) { + $mparts = $message->parts; + for ($i=0; $i < count($mparts); $i++) { + $part = $mparts[$i]; + //recursively add parts + if ((isset($part->ctype_primary) && $part->ctype_primary == "multipart") && (isset($part->ctype_secondary) && ($part->ctype_secondary == "mixed" || $part->ctype_secondary == "alternative" || $part->ctype_secondary == "related"))) { + foreach($part->parts as $spart) + $mparts[] = $spart; + continue; + } + + if (is_calendar($part)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->MeetingResponse - text/calendar part found, trying to reply")); + $body_part = $this->replyMeetingCalendar($part, $response); + } + } + unset($mparts); + } + unset($message); + + if ($body_part === null) { + throw new StatusException(sprintf("BackendIMAP->MeetingResponse(): Error, no calendar part modified"), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + } + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->MeetingResponse - Creating response message")); + $mail = new Mail_mimepart(); + $headers = array("MIME-version" => "1.0", + "From" => $mail->encodeHeader("from", $from_header, "UTF-8"), + "To" => $mail->encodeHeader("to", $to_header, "UTF-8"), + "Date" => gmdate("D, d M Y H:i:s", time())." GMT", + "Subject" => $mail->encodeHeader("subject", $subject_header, "UTF-8"), + "Content-class" => "urn:content-classes:calendarmessage", + "Content-transfer-encoding" => "8BIT"); + unset($mail); + $mail = new Mail_mimepart($body_part, array("content_type" => "text/calendar; method=REPLY; charset=UTF-8", "headers" => $headers)); + + $encoded_mail = $mail->encode(); + unset($mail); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->MeetingResponse - Response message")); + foreach ($encoded_mail["headers"] as $k => $v) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("%s: %s", $k, $v)); + } + ZLog::Write(LOGLEVEL_DEBUG, sprintf("%s", $encoded_mail["body"])); + + $send = $this->sendMessage($fromaddr, $toaddr, $encoded_mail["headers"], $encoded_mail["body"]); + + if ($send) { + $this->saveSentMessage($encoded_mail["headers"], $encoded_mail["body"]); + } + unset($encoded_mail); + + return $send; + } + + + /** + * Returns the email address and the display name of the user. Used by autodiscover. + * + * @param string $username The username + * + * @access public + * @return Array + */ + public function GetUserDetails($username) { + // If the username it's not the email address, here we will have an error. We try creating a valid address + $email = $username; + if (strpos($username, "@") === false && strlen($this->domain) > 0) { + $email .= "@" . $this->domain; + } + return array('emailaddress' => $email, 'fullname' => $this->getDefaultFullNameValue($username)); + } + + + /** + * Returns the BackendIMAP as it implements the ISearchProvider interface + * This could be overwritten by the global configuration + * + * @access public + * @return object Implementation of ISearchProvider + */ + public function GetSearchProvider() { + return $this; + } + + + /**---------------------------------------------------------------------------------------------------------- + * public ISearchProvider methods + */ + + /** + * Indicates if a search type is supported by this SearchProvider + * + * @param string $searchtype + * + * @access public + * @return boolean + */ + public function SupportsType($searchtype) { + return ($searchtype == ISearchProvider::SEARCH_MAILBOX); + } + + + /** + * Queries the IMAP backend + * + * @param string $searchquery string to be searched for + * @param string $searchrange specified searchrange + * + * @access public + * @return array search results + */ + public function GetGALSearchResults($searchquery, $searchrange) { + return false; + } + + /** + * Searches for the emails on the server + * + * @param ContentParameter $cpo + * @param string $prefix If used with the combined backend here will come the backend id and delimiter + * + * @return array + */ + public function GetMailboxSearchResults($cpo, $prefix = '') { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults()")); + + $items = false; + $searchFolderId = $cpo->GetSearchFolderid(); + $searchRange = explode('-', $cpo->GetSearchRange()); + $filter = $this->getSearchRestriction($cpo); + + // Open the folder to search + $search = true; + + if (empty($searchFolderId)) { + $searchFolderId = $this->getFolderIdFromImapId('INBOX'); + } + + // Convert searchFolderId to IMAP id + $imapId = $this->getImapIdFromFolderId($searchFolderId); + + $listMessages = array(); + $numMessages = 0; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: Filter <%s>", $filter)); + + if ($cpo->GetSearchDeepTraversal()) { // Recursive search + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: Recursive search %s", $imapId)); + $listFolders = @imap_list($this->mbox, $this->server, "*"); + if ($listFolders === false) { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->GetMailboxSearchResults: Error recursive list %s", imap_last_error())); + } + else { + foreach ($listFolders as $subFolder) { + if (@imap_reopen($this->mbox, $subFolder)) { + $imapSubFolder = str_replace($this->server, "", $subFolder); + $subFolderId = $this->getFolderIdFromImapId($imapSubFolder); + if ($subFolderId !== false) { // only search found folders + $subList = @imap_search($this->mbox, $filter, SE_UID, "UTF-8"); + if ($subList !== false) { + $numMessages += count($subList); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: SubSearch in %s : %s ocurrences", $imapSubFolder, count($subList))); + $listMessages[] = array($subFolderId => $subList); + } + } + } + } + } + } + else { // Search in folder + if (@imap_reopen($this->mbox, $this->server . $imapId)) { + $subList = @imap_search($this->mbox, $filter, SE_UID, "UTF-8"); + if ($subList !== false) { + $numMessages += count($subList); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: Search in %s : %s ocurrences", $imapId, count($subList))); + $listMessages[] = array($searchFolderId => $subList); + } + } + } + + + if ($numMessages > 0) { + // range for the search results + $rangestart = 0; + $rangeend = SEARCH_MAXRESULTS - 1; + + if (is_array($searchRange) && isset($searchRange[0]) && isset($searchRange[1])) { + $rangestart = $searchRange[0]; + $rangeend = $searchRange[1]; + } + + $querycnt = $numMessages; + $items = array(); + $querylimit = (($rangeend + 1) < $querycnt) ? ($rangeend + 1) : $querycnt + 1; + $items['range'] = $rangestart.'-'.($querylimit - 1); + $items['searchtotal'] = $querycnt; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: %s entries found, returning %s", $items['searchtotal'], $items['range'])); + + $p = 0; + $pc = 0; + for ($i = $rangestart, $j = 0; $i <= $rangeend && $i < $querycnt; $i++, $j++) { + $keys = array_keys($listMessages[$p]); + $cntFolder = count($listMessages[$p][$keys[0]]); + if ($pc >= $cntFolder) { + $p++; + $pc = 0; + $keys = array_keys($listMessages[$p]); + } + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: %s %s %s %s", $p, $pc, $keys[0], $listMessages[$p][$keys[0]][$pc])); + $foundFolderId = $keys[0]; + $items[$j]['class'] = 'Email'; + $items[$j]['longid'] = $prefix . $foundFolderId . ":" . $listMessages[$p][$foundFolderId][$pc]; + $items[$j]['folderid'] = $prefix . $foundFolderId; + $pc++; + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: No messages found!")); + } + + return $items; + } + + /** + * Terminates a search for a given PID + * + * @param int $pid + * + * @return boolean + */ + public function TerminateSearch($pid) { + return true; + } + + /** + * Disconnects from IMAP + * + * @access public + * @return boolean + */ + public function Disconnect() { + // Don't close the mailbox, we will need it open in the Backend methods + return true; + } + + + /** + * Creates a search restriction + * + * @param ContentParameter $cpo + * @return string + */ + private function getSearchRestriction($cpo) { + $searchText = $cpo->GetSearchFreeText(); + $searchGreater = $cpo->GetSearchValueGreater(); + $searchLess = $cpo->GetSearchValueLess(); + + $filter = ''; + if ($searchGreater != '') { + $filter .= ' SINCE "' . $searchGreater . '"'; + } else { + // Only search in sync messages + $limitdate = new DateTime(); + switch (SYNC_FILTERTIME_MAX) { + case SYNC_FILTERTYPE_1DAY: + $limitdate = $limitdate->sub(new DateInterval("P1D")); + break; + case SYNC_FILTERTYPE_3DAYS: + $limitdate = $limitdate->sub(new DateInterval("P3D")); + break; + case SYNC_FILTERTYPE_1WEEK: + $limitdate = $limitdate->sub(new DateInterval("P1W")); + break; + case SYNC_FILTERTYPE_2WEEKS: + $limitdate = $limitdate->sub(new DateInterval("P2W")); + break; + case SYNC_FILTERTYPE_1MONTH: + $limitdate = $limitdate->sub(new DateInterval("P1M")); + break; + case SYNC_FILTERTYPE_3MONTHS: + $limitdate = $limitdate->sub(new DateInterval("P3M")); + break; + case SYNC_FILTERTYPE_6MONTHS: + $limitdate = $limitdate->sub(new DateInterval("P6M")); + break; + default: + $limitdate = false; + break; + } + + if ($limitdate !== false) { + // date format : 7 Jan 2012 + $filter .= ' SINCE "' . ($limitdate->format("d M Y")) . '"'; + } + } + if ($searchLess != '') { + $filter .= ' BEFORE "' . $searchLess . '"'; + } + + $filter .= ' TEXT "' . $searchText . '"'; + + return $filter; } @@ -1567,53 +2010,6 @@ class BackendIMAP extends BackendDiff { return $folderid; } - - /** - * Parses the message and return only the plaintext body - * - * @param string $message html message - * - * @access protected - * @return string plaintext message - */ - protected function getBody($message) { - $body = ""; - $htmlbody = ""; - - $this->getBodyRecursive($message, "plain", $body); - - if($body === "") { - $this->getBodyRecursive($message, "html", $body); - } - - return $body; - } - - /** - * Get all parts in the message with specified type and concatenate them together, unless the - * Content-Disposition is 'attachment', in which case the text is apparently an attachment - * - * @param string $message mimedecode message(part) - * @param string $message message subtype - * @param string &$body body reference - * - * @access protected - * @return - */ - protected function getBodyRecursive($message, $subtype, &$body) { - if(!isset($message->ctype_primary)) return; - if(strcasecmp($message->ctype_primary,"text")==0 && strcasecmp($message->ctype_secondary,$subtype)==0 && isset($message->body)) - $body .= $message->body; - - if(strcasecmp($message->ctype_primary,"multipart")==0 && isset($message->parts) && is_array($message->parts)) { - foreach($message->parts as $part) { - if(!isset($part->disposition) || strcasecmp($part->disposition,"attachment")) { - $this->getBodyRecursive($part, $subtype, $body); - } - } - } - } - /** * Returns the serverdelimiter for folder parsing * @@ -1638,96 +2034,91 @@ class BackendIMAP extends BackendDiff { * @param boolean $force re-open the folder even if currently opened * * @access protected - * @return + * @return boolean if folder is opened */ - protected function imap_reopenFolder($folderid, $force = false) { + protected function imap_reopen_folder($folderid, $force = false) { + // if the stream is not alive, we open it again + if (!@imap_ping($this->mbox)) { + $this->mbox = @imap_open($this->server, $this->username, $this->password, OP_HALFOPEN); + $this->mboxFolder = ""; + } + // to see changes, the folder has to be reopened! - if ($this->mboxFolder != $folderid || $force) { - $s = @imap_reopen($this->mbox, $this->server . $folderid); - // TODO throw status exception - if (!$s) { - ZLog::Write(LOGLEVEL_WARN, "BackendIMAP->imap_reopenFolder('%s'): failed to change folder: ",$folderid, implode(", ", imap_errors())); + if ($this->mboxFolder != $folderid || $force) { + $s = @imap_reopen($this->mbox, $this->server . $folderid); + // TODO throw status exception + if (!$s) { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->imap_reopen_folder('%s'): failed to change folder: %s", $folderid, implode(", ", imap_errors()))); return false; - } + } $this->mboxFolder = $folderid; } + + return true; } /** - * Build a multipart RFC822, embedding body and one file (for attachments) + * Creates a new IMAP folder. * - * @param string $filenm name of the file to be attached - * @param long $filesize size of the file to be attached - * @param string $file_cont content of the file - * @param string $body current body - * @param string $body_ct content-type - * @param string $body_cte content-transfer-encoding - * @param string $boundary optional existing boundary + * @param string $foldername full folder name * - * @access protected - * @return array with [0] => $mail_header and [1] => $mail_body + * @access private + * @return boolean success */ - protected function mail_attach($filenm,$filesize,$file_cont,$body, $body_ct, $body_cte, $boundary = false) { - if (!$boundary) $boundary = strtoupper(md5(uniqid(time()))); + private function imap_create_folder($foldername) { + $name = Utils::Utf7_iconv_encode(Utils::Utf8_to_utf7($foldername)); - //remove the ending boundary because we will add it at the end - $body = str_replace("--$boundary--", "", $body); + $res = @imap_createmailbox($this->mbox, $name); + if ($res) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_create_folder('%s'): new folder created", $foldername)); + } + else { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->imap_create_folder('%s'): failed to create folder: %s", $foldername, implode(", ", imap_errors()))); + } - $mail_header = "Content-Type: multipart/mixed; boundary=$boundary\n"; - - // build main body with the sumitted type & encoding from the pda - $mail_body = $this->enc_multipart($boundary, $body, $body_ct, $body_cte); - $mail_body .= $this->enc_attach_file($boundary, $filenm, $filesize, $file_cont); - - $mail_body .= "--$boundary--\n\n"; - return array($mail_header, $mail_body); + return $res; } /** - * Helper for mail_attach() + * Check if the message was sent before the cutoffdate. * - * @param string $boundary boundary - * @param string $body current body - * @param string $body_ct content-type - * @param string $body_cte content-transfer-encoding - * - * @access protected - * @return string message body + * @access private + * @param integer $cutoffdate EPOCH of the bottom sync range. 0 if no range is defined + * @param integer $id Message id + * @return boolean */ - protected function enc_multipart($boundary, $body, $body_ct, $body_cte) { - $mail_body = "This is a multi-part message in MIME format\n\n"; - $mail_body .= "--$boundary\n"; - $mail_body .= "Content-Type: $body_ct\n"; - $mail_body .= "Content-Transfer-Encoding: $body_cte\n\n"; - $mail_body .= "$body\n\n"; + private function imap_inside_cutoffdate($cutoffdate, $id) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_inside_cutoffdate(): Checking if the messages is withing the cutoffdate %d, %s", $cutoffdate, $id)); + $is_inside = false; - return $mail_body; - } + if ($cutoffdate == 0) { + // No cutoffdate, all the messages are in range + $is_inside = true; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_inside_cutoffdate(): No cutoffdate, all the messages are in range")); + } + else { + $overview = imap_fetch_overview($this->mbox, $id, FT_UID); + if (is_array($overview)) { + if (isset($overview[0]->date)) { + $epoch_sent = strtotime($overview[0]->date); + $is_inside = ($cutoffdate <= $epoch_sent); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_inside_cutoffdate(): Message is %s cutoffdate range", ($is_inside ? "INSIDE" : "OUTSIDE"))); + } + else { + // No sent date defined, that's a buggy message but we will think that the message is in range + $is_inside = true; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_inside_cutoffdate(): No sent date defined, that's a buggy message but we will think that the message is in range")); + } + } + else { + // No overview, maybe the message is no longer there + $is_inside = false; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_inside_cutoffdate(): No overview, maybe the message is no longer there")); + } + } - /** - * Helper for mail_attach() - * - * @param string $boundary boundary - * @param string $filenm name of the file to be attached - * @param long $filesize size of the file to be attached - * @param string $file_cont content of the file - * @param string $content_type optional content-type - * - * @access protected - * @return string message body - */ - protected function enc_attach_file($boundary, $filenm, $filesize, $file_cont, $content_type = "") { - if (!$content_type) $content_type = "text/plain"; - $mail_body = "--$boundary\n"; - $mail_body .= "Content-Type: $content_type; name=\"$filenm\"\n"; - $mail_body .= "Content-Transfer-Encoding: base64\n"; - $mail_body .= "Content-Disposition: attachment; filename=\"$filenm\"\n"; - $mail_body .= "Content-Description: $filenm\n\n"; - //contrib - chunk base64 encoded attachments - $mail_body .= chunk_split(base64_encode($file_cont)) . "\n\n"; - - return $mail_body; + return $is_inside; } /** @@ -1813,15 +2204,590 @@ class BackendIMAP extends BackendDiff { * @return string */ protected function cleanupDate($receiveddate) { + if (is_array($receiveddate)) { + // Header Date could be repeated in the message, we only check the first + $receiveddate = $receiveddate[0]; + } $receiveddate = strtotime(preg_replace("/\(.*\)/", "", $receiveddate)); if ($receiveddate == false || $receiveddate == -1) { - debugLog("Received date is false. Message might be broken."); + ZLog::Write(LOGLEVEL_DEBUG, "cleanupDate() : Received date is false. Message might be broken."); return null; } return $receiveddate; } + /** + * Returns the default value for "From" + * + * @access private + * @return string + */ + private function getDefaultFromValue() { + $v = ""; + switch (IMAP_DEFAULTFROM) { + case 'username': + $v = $this->username; + break; + case 'domain': + $v = $this->domain; + break; + case 'ldap': + $v = $this->getIdentityFromLdap($this->username, $this->domain, IMAP_FROM_LDAP_FROM, true); + break; + case 'sql': + $v = $this->getIdentityFromSql($this->username, $this->domain, IMAP_FROM_SQL_FROM, true); + break; + case 'passwd': + $v = $this->getIdentityFromPasswd($this->username, $this->domain, 'FROM', true); + break; + default: + $v = $this->username . IMAP_DEFAULTFROM; + break; + } + + return $v; + } + + /** + * Return the default value for "FullName" + * + * @access private + * @param string $username Username + * @return string + */ + private function getDefaultFullNameValue($username) { + $v = $this->username; + switch (IMAP_DEFAULTFROM) { + case 'ldap': + $v = $this->getIdentityFromSql($username, $this->domain, IMAP_FROM_LDAP_FULLNAME, false); + break; + case 'sql': + $v = $this->getIdentityFromSql($username, $this->domain, IMAP_FROM_SQL_FULLNAME, false); + break; + case 'passwd': + $v = $this->getIdentityFromPasswd($username, $this->domain, 'FULLNAME', false); + break; + } + + return $v; + } + + /** + * Generate the "From"/"FullName" value stored in a LDAP server + * + * @access private + * @params string $username username value + * @params string $domain domain value + * @params string $identity pattern to fill with ldap values + * @params boolean $encode if the result should be encoded as a header + * @return string + */ + private function getIdentityFromLdap($username, $domain, $identity, $encode = true) { + $ret_value = $username; + + $ldap_conn = null; + try { + $ldap_conn = ldap_connect(IMAP_FROM_LDAP_SERVER, IMAP_FROM_LDAP_SERVER_PORT); + if ($ldap_conn) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getIdentityFromLdap() - Connected to LDAP")); + ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3); + ldap_set_option($ldap_conn, LDAP_OPT_REFERRALS, 0); + $ldap_bind = ldap_bind($ldap_conn, IMAP_FROM_LDAP_USER, IMAP_FROM_LDAP_PASSWORD); + + if ($ldap_bind) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getIdentityFromLdap() - Authenticated in LDAP")); + $filter = str_replace('#username', $username, str_replace('#domain', $domain, IMAP_FROM_LDAP_QUERY)); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getIdentityFromLdap() - Searching From with filter: %s", $filter)); + $search = ldap_search($ldap_conn, IMAP_FROM_LDAP_BASE, $filter, unserialize(IMAP_FROM_LDAP_FIELDS)); + $items = ldap_get_entries($ldap_conn, $search); + if ($items['count'] > 0) { + $ret_value = $identity; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getIdentityFromLdap() - Found entry in LDAP. Generating From")); + // We get the first object. It's your responsability to make the query unique + foreach (unserialize(IMAP_FROM_LDAP_FIELDS) as $field) { + $ret_value = str_replace('#'.$field, $items[0][$field][0], $ret_value); + } + if ($encode) { + $ret_value = $this->encodeFrom($ret_value); + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getIdentityFromLdap() - No entry found in LDAP")); + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getIdentityFromLdap() - Not authenticated in LDAP server")); + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getIdentityFromLdap() - Not connected to LDAP server")); + } + } + catch(Exception $ex) { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->getIdentityFromLdap() - Error getting From value from LDAP server: %s", $ex)); + } + + ldap_close($ldap_conn); + + return $ret_value; + } + + + /** + * Generate the "From" value stored in a SQL Database + * + * @access private + * @params string $username username value + * @params string $domain domain value + * @return string + */ + private function getIdentityFromSql($username, $domain, $identity, $encode = true) { + $ret_value = $username; + + $dbh = $sth = $record = null; + try { + $dbh = new PDO(IMAP_FROM_SQL_DSN, IMAP_FROM_SQL_USER, IMAP_FROM_SQL_PASSWORD, unserialize(IMAP_FROM_SQL_OPTIONS)); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getIdentityFromSql() - Connected to SQL Database")); + + $sql = str_replace('#username', $username, str_replace('#domain', $domain, IMAP_FROM_SQL_QUERY)); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getIdentityFromSql() - Searching From with filter: %s", $sql)); + $sth = $dbh->prepare($sql); + $sth->execute(); + $record = $sth->fetch(PDO::FETCH_ASSOC); + if ($record) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getIdentityFromSql() - Found entry in SQL Database. Generating From")); + $ret_value = $identity; + foreach (unserialize(IMAP_FROM_SQL_FIELDS) as $field) { + $ret_value = str_replace('#'.$field, $record[$field], $ret_value); + } + if ($encode) { + $ret_value = $this->encodeFrom($ret_value); + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getIdentityFromSql() - No entry found in SQL Database")); + } + } + catch(PDOException $ex) { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->getIdentityFromSql() - Error getting From value from SQL Database: %s", $ex)); + } + + $dbh = $sth = $record = null; + + return $ret_value; + } + + /** + * Generate the "From" value from the local posix passwd database + * + * @access private + * @params string $username username value + * @params string $domain domain value + * @return string + */ + private function getIdentityFromPasswd($username, $domain, $identity, $encode = true) { + $ret_value = $username; + + try { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getIdentityFromPasswd() - Fetching info for user %s", $username)); + + $local_user = posix_getpwnam($username); + if ($local_user) { + $tmp = $local_user['gecos']; + $tmp = explode(',', $tmp); + $name = $tmp[0]; + unset($tmp); + + switch ($identity) { + case 'FROM': + if (strlen($domain) > 0) { + $ret_value = sprintf("%s <%s@%s>", $name, $username, $domain); + } else { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->getIdentityFromPasswd() - No domain passed. Cannot construct From address.")); + } + break; + case 'FULLNAME': + $ret_value = sprintf("%s", $name); + break; + } + if ($encode) { + $ret_value = $this->encodeFrom($ret_value); + } + } else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getIdentityFromPasswd() - No entry found in Password database")); + + } + + } + catch(Exception $ex) { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->getIdentityFromPasswd() - Error getting From value from passwd database: %s", $ex)); + } + + return $ret_value; + } + + + /** + * Encode the From value as Base64 + * + * @access private + * @param string $from From value + * @return string + */ + private function encodeFrom($from) { + $items = explode("<", $from); + $name = trim($items[0]); + return "=?UTF-8?B?" . base64_encode($name) . "?= <" . $items[1]; + } + + + /** + * Returns a list of mime-types with extension files + * + * @access private + * @return array[mime-type => extension] + */ + private function SystemExtensionMimeTypes() { + $out = array(); + $mime_file = '/etc/mime.types'; + if (file_exists($mime_file)) { + $file = fopen($mime_file, 'r'); + while(($line = fgets($file)) !== false) { + $line = trim(preg_replace('/#.*/', '', $line)); + if(!$line) + continue; + $parts = preg_split('/\s+/', $line); + if(count($parts) == 1) + continue; + $type = array_shift($parts); + foreach($parts as $part) { + if (!isset($out[$type])) { + $out[$type] = $part; + } + } + } + fclose($file); + } + + return $out; + } + + + /** + * Modify a text/calendar part to transform it in a reply + * + * @access private + * @param $part MIME part + * @param $response Response numeric value + * @return string MIME text/calendar + */ + private function replyMeetingCalendar($part, $response) { + $response_text = "ACCEPTED"; // 1 or default is ACCEPTED + switch ($response) { + case 1: + $response_text = "ACCEPTED"; + break; + case 2: + $response_text = "TENTATIVE"; + break; + case 3: + $response_text = "DECLINED"; + break; + } + + $ical = new iCalComponent(); + $ical->ParseFrom($part->body); + + $ical->SetPValue("METHOD", "REPLY"); + $ical->SetCPParameterValue("VEVENT", "ATTENDEE", "PARTSTAT", $response_text); + + return $ical->Render(); + } + + + /** + * Converts a text/calendar part into SyncMeetingRequest + * + * @access private + * @param $part MIME part + * @param $output SyncMail object + */ + private function parseMeetingCalendar($part, &$output) { + $ical = new iCalComponent(); + $ical->ParseFrom($part->body); + + if (isset($part->ctype_parameters["method"])) { + switch (strtolower($part->ctype_parameters["method"])) { + case "cancel": + $output->messageclass = "IPM.Schedule.Meeting.Canceled"; + break; + case "counter": + $output->messageclass = "IPM.Schedule.Meeting.Resp.Tent"; + break; + case "reply": + $props = $ical->GetPropertiesByPath('!VTIMEZONE/ATTENDEE'); + if (count($props) == 1) { + $props_params = $props[0]->Parameters(); + if (isset($props_params["PARTSTAT"])) { + switch (strtolower($props_params["PARTSTAT"])) { + case "accepted": + $output->messageclass = "IPM.Schedule.Meeting.Resp.Pos"; + break; + case "needs-action": + case "tentative": + $output->messageclass = "IPM.Schedule.Meeting.Resp.Tent"; + break; + case "declined": + $output->messageclass = "IPM.Schedule.Meeting.Resp.Neg"; + break; + default: + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->parseMeetingCalendar() - Unknown reply status %s", strtolower($props_params["PARTSTAT"]))); + $output->messageclass = "IPM.Appointment"; + break; + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->parseMeetingCalendar() - No reply status found")); + $output->messageclass = "IPM.Appointment"; + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->parseMeetingCalendar() - There are not attendees")); + $output->messageclass = "IPM.Appointment"; + } + break; + case "request": + $output->messageclass = "IPM.Schedule.Meeting.Request"; + break; + default: + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->parseMeetingCalendar() - Unknown method %s", strtolower($part->headers["method"]))); + $output->messageclass = "IPM.Appointment"; + break; + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->parseMeetingCalendar() - No method header")); + $output->messageclass = "IPM.Appointment"; + } + + $props = $ical->GetPropertiesByPath('VEVENT/DTSTAMP'); + if (count($props) == 1) { + $output->meetingrequest->dtstamp = Utils::MakeUTCDate($props[0]->Value()); + } + $props = $ical->GetPropertiesByPath('VEVENT/UID'); + if (count($props) == 1) { + $output->meetingrequest->globalobjid = $props[0]->Value(); + } + $props = $ical->GetPropertiesByPath('VEVENT/DTSTART'); + if (count($props) == 1) { + $output->meetingrequest->starttime = Utils::MakeUTCDate($props[0]->Value()); + if (strlen($props[0]->Value()) == 8) { + $output->meetingrequest->alldayevent = 1; + } + } + $props = $ical->GetPropertiesByPath('VEVENT/DTEND'); + if (count($props) == 1) { + $output->meetingrequest->endtime = Utils::MakeUTCDate($props[0]->Value()); + if (strlen($props[0]->Value()) == 8) { + $output->meetingrequest->alldayevent = 1; + } + } + $props = $ical->GetPropertiesByPath('VEVENT/ORGANIZER'); + if (count($props) == 1) { + $output->meetingrequest->organizer = str_ireplace("MAILTO:", "", $props[0]->Value()); + } + $props = $ical->GetPropertiesByPath('VEVENT/LOCATION'); + if (count($props) == 1) { + $output->meetingrequest->location = $props[0]->Value(); + } + $props = $ical->GetPropertiesByPath('VEVENT/CLASS'); + if (count($props) == 1) { + switch ($props[0]->Value()) { + case "PUBLIC": + $output->meetingrequest->sensitivity = "0"; + break; + case "PRIVATE": + $output->meetingrequest->sensitivity = "2"; + break; + case "CONFIDENTIAL": + $output->meetingrequest->sensitivity = "3"; + break; + default: + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->parseMeetingCalendar() - No sensitivity class. Using 2")); + $output->meetingrequest->sensitivity = "2"; + break; + } + } + + // Get $tz from first timezone + $props = $ical->GetPropertiesByPath("VTIMEZONE/TZID"); + if (count($props) > 0) { + $tzname = $props[0]->Value(); + $tz = TimezoneUtil::GetFullTZFromTZName($tzname); + } + else { + $tz = TimezoneUtil::GetFullTZ(); + } + $output->meetingrequest->timezone = base64_encode(TimezoneUtil::getSyncBlobFromTZ($tz)); + + // Fixed values + $output->meetingrequest->instancetype = 0; + $output->meetingrequest->responserequested = 1; + $output->meetingrequest->busystatus = 2; + + // TODO: reminder + $output->meetingrequest->reminder = ""; + } + + + /** + * Sends a message + * + * @access private + * @param $fromaddr From address + * @param $toaddr To address + * @param $headers Headers array + * @param $body Body array + * @return boolean True if sent + * @throws StatusException + */ + private function sendMessage($fromaddr, $toaddr, $headers, $body) { + global $imap_smtp_params; + + $sendingMethod = 'mail'; + if (defined('IMAP_SMTP_METHOD')) { + $sendingMethod = IMAP_SMTP_METHOD; + if ($sendingMethod == 'smtp') { + if (isset($imap_smtp_params['username']) && $imap_smtp_params['username'] == 'imap_username') { + $imap_smtp_params['username'] = $this->username; + } + if (isset($imap_smtp_params['password']) && $imap_smtp_params['password'] == 'imap_password') { + $imap_smtp_params['password'] = $this->password; + } + } + } + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->sendMessage(): SendingMail with %s", $sendingMethod)); + $mail =& Mail::factory($sendingMethod, $sendingMethod == 'mail' ? '-f '.$fromaddr : $imap_smtp_params); + $send = $mail->send($toaddr, $headers, $body); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->sendMessage(): send return value %s", $send)); + + if ($send !== true) { + throw new StatusException(sprintf("BackendIMAP->sendMessage(): The email could not be sent"), SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED); + } + + return $send; + } + + + /** + * Saves a copy of a message in the Sent folder + * + * @access public + * @param $finalHeaders Array of headers + * @param $finalBody Body part + * @return boolean If the message is saved + */ + private function saveSentMessage($finalHeaders, $finalBody) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->saveSentMessage(): saving message in Sent Items folder")); + + $headers = ""; + foreach ($finalHeaders as $k => $v) { + if (strlen($headers) > 0) { + $headers .= "\n"; + } + $headers .= "$k: $v"; + } + + $saved = false; + if ($this->sentID) { + $saved = $this->addSentMessage($this->sentID, $headers, $finalBody); + } + else if (strlen(IMAP_FOLDER_SENT) > 0) { + // try to open the sentfolder + if (!$this->imap_reopen_folder(IMAP_FOLDER_SENT, false)) { + // if we cannot open it, it mustn't exist, we try to create it. + $this->imap_create_folder($this->server . IMAP_FOLDER_SENT); + } + $saved = $this->addSentMessage(IMAP_FOLDER_SENT, $headers, $finalBody); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->saveSentMessage(): Outgoing mail saved in configured 'Sent' folder '%s'", IMAP_FOLDER_SENT)); + } + // No Sent folder set, try defaults + else { + ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->saveSentMessage(): No Sent mailbox set"); + if($this->addSentMessage("INBOX.Sent", $headers, $finalBody)) { + ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->saveSentMessage(): Outgoing mail saved in 'INBOX.Sent'"); + $saved = true; + } + else if ($this->addSentMessage("Sent", $headers, $finalBody)) { + ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->saveSentMessage(): Outgoing mail saved in 'Sent'"); + $saved = true; + } + else if ($this->addSentMessage("Sent Items", $headers, $finalBody)) { + ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->saveSentMessage(): Outgoing mail saved in 'Sent Items'"); + $saved = true; + } + } + + unset($headers); + + if (!$saved) { + ZLog::Write(LOGLEVEL_ERROR, "BackendIMAP->saveSentMessage(): The email could not be saved to Sent Items folder. Check your configuration."); + } + + return $saved; + } + + + /** + * Set the from header value if not set or we are overwriting by configuration. + * + * @param array &$headers + * @return void + * @access private + */ + private function setFromHeaderValue(&$headers) { + $from = $this->getDefaultFromValue(); + + // If the message is not s/mime + if (isset($headers["from"])) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFromHeaderValue(): from defined: %s", $headers["from"])); + if (strlen(IMAP_DEFAULTFROM) > 0) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFromHeaderValue(): Overwriting From: %s", $from)); + $headers["from"] = $from; + } + } + elseif (isset($headers["From"])) { + // if the message is s/mime + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFromHeaderValue(): From defined: %s", $headers["From"])); + if (strlen(IMAP_DEFAULTFROM) > 0) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFromHeaderValue(): Overwriting From: %s", $from)); + $headers["From"] = $from; + } + } + else { + // not From header found + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFromHeaderValue(): No From address defined, we try for a default one")); + $headers["from"] = $from; + } + } + + /** + * Set the Return-Path header value if not set + * + * @param array &$headers + * @param string $fromaddr + * @return void + * @access private + */ + private function setReturnPathValue(&$headers, $fromaddr) { + if (!(isset($headers["return-path"]) || isset($headers["Return-Path"]))) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->setReturnPathValue(): No Return-Path address defined, we use From")); + $headers["return-path"] = $fromaddr; + } + } + + /* BEGIN fmbiete's contribution r1528, ZP-320 */ /** * Indicates which AS version is supported by the backend. diff --git a/sources/backend/imap/mime_encode.php b/sources/backend/imap/mime_encode.php new file mode 100644 index 0000000..c5f3a7f --- /dev/null +++ b/sources/backend/imap/mime_encode.php @@ -0,0 +1,295 @@ +disposition) && $part->disposition == "attachment") { + // it's an attachment + $new_part = add_sub_part($email, $part); + } + else { + if (isset($part->ctype_primary) && $part->ctype_primary != "text" && $part->ctype_primary != "multipart") { + // it's not a text part or a multipart + $new_part = add_sub_part($email, $part); + } + } + if (isset($part->parts)) { + // We add sub-parts to the new part (if any), not to the main message. Recursive calling + if ($new_part === null) { + add_extra_sub_parts($email, $part->parts); + } + else { + add_extra_sub_parts($new_part, $part->parts); + } + } + } + } +} + +/** + * Add a subpart to a mimepart object. + * + * @param Mail_mimePart $email reference to the object + * @param object $part message part + * + * @access private + * @return void + */ +function add_sub_part(&$email, $part) { + //http://tools.ietf.org/html/rfc4021 + $new_part = null; + $params = array(); + $params['content_type'] = ''; + if (isset($part) && isset($email)) { + if (isset($part->ctype_primary)) { + $params['content_type'] = $part->ctype_primary; + } + if (isset($part->ctype_secondary)) { + $params['content_type'] .= '/' . $part->ctype_secondary; + } + if (isset($part->ctype_parameters)) { + foreach ($part->ctype_parameters as $k => $v) { + if(strcasecmp($k, 'boundary') != 0) { + $params['content_type'] .= '; ' . $k . '=' . $v; + } + } + } + if (isset($part->disposition)) { + $params['disposition'] = $part->disposition; + } + //FIXME: dfilename => filename + if (isset($part->d_parameters)) { + foreach ($part->d_parameters as $k => $v) { + $params[$k] = $v; + } + } + foreach ($part->headers as $k => $v) { + switch($k) { + case "content-description": + $params['description'] = $v; + break; + case "content-type": + case "content-disposition": + case "content-transfer-encoding": + // Do nothing, we already did + break; + case "content-id": + $params['cid'] = str_replace('<', '', str_replace('>', '', $v)); + break; + default: + $params[$k] = $v; + break; + } + } + + // If not exist body, the part will be multipart/alternative, so we don't add encoding + if (!isset($params['encoding']) && isset($part->body)) { + $params['encoding'] = 'base64'; + } + // We could not have body; recursive messages + $new_part = $email->addSubPart(isset($part->body) ? $part->body : "", $params); + unset($params); + } + + // return the new part + return $new_part; +} + +/** + * Add a subpart to a mimepart object. + * + * @param Mail_mimePart $email reference to the object + * @param object $part message part + * + * @access private + * @return void + */ +function change_charset_and_add_subparts(&$email, $part) { + if (isset($part)) { + $new_part = null; + if (isset($part->ctype_parameters['charset'])) { + $part->ctype_parameters['charset'] = 'UTF-8'; + $new_part = add_sub_part($email, $part); + } + else { + // We don't add the charset because it could be a non-text part + $new_part = add_sub_part($email, $part); + } + + if (isset($part->parts)) { + foreach ($part->parts as $subpart) { + // Subparts are added to the part, not the main message + change_charset_and_add_subparts($new_part, $subpart); + } + } + } +} + +/** + * Creates a MIME message from a decoded MIME message, reencoding and fixing the text. + * + * @param array $message array returned from Mail_mimeDecode->decode + * + * @access public + * @return string MIME message + */ +function build_mime_message($message) { + $finalEmail = new Mail_mimePart(isset($message->body) ? $message->body : "", array('headers' => $message->headers)); + if (isset($message->parts)) { + foreach ($message->parts as $part) { + change_charset_and_add_subparts($finalEmail, $part); + } + } + + $mimeHeaders = Array(); + $mimeHeaders['headers'] = Array(); + $is_mime = false; + foreach ($message->headers as $key => $value) { + switch($key) { + case 'content-type': + $new_value = $message->ctype_primary . "/" . $message->ctype_secondary; + $is_mime = (strcasecmp($message->ctype_primary, 'multipart') == 0); + + if (isset($message->ctype_parameters)) { + foreach ($message->ctype_parameters as $ckey => $cvalue) { + switch($ckey) { + case 'charset': + $new_value .= '; charset="UTF-8"'; + break; + case 'boundary': + // Do nothing, we are encoding also the headers + break; + default: + $new_value .= '; ' . $ckey . '="' . $cvalue . '"'; + break; + } + } + } + + $mimeHeaders['content_type'] = $new_value; + break; + case 'content-transfer-encoding': + if (strcasecmp($value, "base64") == 0 || strcasecmp($value, "binary") == 0) { + $mimeHeaders['encoding'] = "base64"; + } + else { + $mimeHeaders['encoding'] = "8bit"; + } + break; + case 'content-id': + $mimeHeaders['cid'] = $value; + break; + case 'content-location': + $mimeHeaders['location'] = $value; + break; + case 'content-disposition': + $mimeHeaders['disposition'] = $value; + break; + case 'content-description': + $mimeHeaders['description'] = $value; + break; + default: + if (is_array($value)) { + foreach($value as $v) { + $mimeHeaders['headers'][$key] = $v; + } + } + else { + $mimeHeaders['headers'][$key] = $value; + } + break; + } + } + + $finalEmail = new Mail_mimePart(isset($message->body) ? $message->body : "", $mimeHeaders); + unset($mimeHeaders['headers']); + unset($mimeHeaders); + + if (isset($message->parts)) { + foreach ($message->parts as $part) { + change_charset_and_add_subparts($finalEmail, $part); + } + } + + $boundary = '=_' . md5(rand() . microtime()); + $finalEmail = $finalEmail->encode($boundary); + + $headers = ""; + $mimePart = new Mail_mimePart(); + foreach ($finalEmail['headers'] as $key => $value) { + if (is_array($value)) { + foreach ($values as $ikey => $ivalue) { + $headers .= $key . ": " . $mimePart->encodeHeader($key, $ivalue, "utf-8", "base64") . "\n"; + } + } + else { + $headers .= $key . ": " . $mimePart->encodeHeader($key, $value, "utf-8", "base64") . "\n"; + } + } + unset($mimePart); + + + if ($is_mime) { + $built_message = "$headers\nThis is a multi-part message in MIME format.\n".$finalEmail['body']; + } + else { + $built_message = "$headers\n".$finalEmail['body']; + } + unset($headers); + unset($finalEmail); + + return $built_message; +} + +/** + * Detect if the message-part is VCALENDAR + * Content-Type: text/calendar; + * + * @param Mail_mimeDecode $message + * @return boolean + * @access public + */ +function is_calendar($message) { + $res = false; + + if (isset($message->ctype_primary) && isset($message->ctype_secondary)) { + if ($message->ctype_primary == "text" && $message->ctype_secondary == "calendar") { + $res = true; + } + } + + return $res; +} + + +/** + * Detect if the message-part is SMIME + * Content-Type: multipart/signed; + * Content-Type: application/pkcs7-mime; + * + * @param Mail_mimeDecode $message + * @return boolean + * @access public + */ +function is_smime($message) { + $res = false; + + if (isset($message->ctype_primary) && isset($message->ctype_secondary)) { + if (($message->ctype_primary == "multipart" && $message->ctype_secondary == "signed") || ($message->ctype_primary == "application" && $message->ctype_secondary == "pkcs7-mime")) { + $res = true; + } + } + + return $res; +} \ No newline at end of file diff --git a/sources/backend/ldap/AUTHOR b/sources/backend/ldap/AUTHOR new file mode 100644 index 0000000..af8002c --- /dev/null +++ b/sources/backend/ldap/AUTHOR @@ -0,0 +1,3 @@ +The Author of this backend is dupondje, I could have modified it. +You can found the original code here: +https://github.com/dupondje/PHP-Push-2 diff --git a/sources/backend/ldap/config.php b/sources/backend/ldap/config.php new file mode 100644 index 0000000..586b993 --- /dev/null +++ b/sources/backend/ldap/config.php @@ -0,0 +1,60 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// ********************** +// BackendLDAP settings +// ********************** + +// Server address +define('LDAP_SERVER', 'localhost'); + +// Server Port +define('LDAP_SERVER_PORT', '389'); + +// LDAP USER DN +define('LDAP_USER_DN', 'uid=%u,ou=mailaccount,dc=phppush,dc=com'); + +// LDAP BASE DNS +define('LDAP_BASE_DNS', 'Contacts:ou=addressbook,uid=%u,ou=mailaccount,dc=phppush,dc=com'); //Multiple values separator is | + +?> \ No newline at end of file diff --git a/sources/backend/ldap/ldap.php b/sources/backend/ldap/ldap.php new file mode 100644 index 0000000..940c7f9 --- /dev/null +++ b/sources/backend/ldap/ldap.php @@ -0,0 +1,584 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// config file +require_once("backend/ldap/config.php"); + +include_once('lib/default/diffbackend/diffbackend.php'); + +class BackendLDAP extends BackendDiff { + + private $ldap_link; + private $user; + + public function Logon($username, $domain, $password) { + $this->user = $username; + $user_dn = str_replace('%u', $username, LDAP_USER_DN); + $this->ldap_link = ldap_connect(LDAP_SERVER, LDAP_SERVER_PORT); + ldap_set_option($this->ldap_link, LDAP_OPT_PROTOCOL_VERSION, 3); + if (ldap_bind($this->ldap_link, $user_dn, $password)) { + ZLog::Write(LOGLEVEL_INFO, sprintf("BackendLDAP->Logon(): User '%s' is authenticated on LDAP", $username)); + return true; + } + else { + ZLog::Write(LOGLEVEL_INFO, sprintf("BackendLDAP->Logon(): User '%s' is not authenticated on LDAP. Error: ", $username, ldap_error($this->ldap_link))); + return false; + } + } + + public function Logoff() { + if (ldap_unbind($this->ldap_link)) { + ZLog::Write(LOGLEVEL_INFO, sprintf("BackendLDAP->Logoff(): Disconnection successfull.")); + } + else { + ZLog::Write(LOGLEVEL_INFO, sprintf("BackendLDAP->Logoff(): Disconnection failed. Error: %s", ldap_error($this->ldap_link))); + } + return true; + } + + public function SendMail($sm) { + return false; + } + + public function GetAttachmentData($attname) { + return false; + } + + public function GetWasteBasket() { + return false; + } + + public function GetFolderList() { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendLDAP->GetFolderList(): Getting all folders.")); + $contacts = array(); + $dns = explode("|", LDAP_BASE_DNS); + foreach ($dns as $dn) { + $name = explode(":", $dn); + $folder = $this->StatFolder($name[0]); + $contacts[] = $folder; + } + return $contacts; + } + + public function GetFolder($id) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendLDAP->GetFolder('%s')", $id)); + $folder = new SyncFolder(); + $folder->serverid = $id; + $folder->parentid = "0"; + $folder->displayname = $id; + $folder->type = SYNC_FOLDER_TYPE_CONTACT; + return $folder; + } + + public function StatFolder($id) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendLDAP->StatFolder('%s')", $id)); + $folder = $this->GetFolder($id); + $stat = array(); + $stat["id"] = $id; + $stat["parent"] = $folder->parentid; + $stat["mod"] = $folder->displayname; + return $stat; + } + + public function ChangeFolder($folderid, $oldid, $displayname, $type) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendLDAP->ChangeFolder('%s','%s','%s','%s')", $folderid, $oldid, $displayname, $type)); + return false; + } + + public function DeleteFolder($id, $parentid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendLDAP->DeleteFolder('%s','%s')", $id, $parentid)); + return false; + } + + public function GetMessageList($folderid, $cutoffdate) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendLDAP->GetMessageList('%s','%s')", $folderid, $cutoffdate)); + + $cutoff = date("YmdHis\Z", $cutoffdate); + $filter = sprintf('(modifyTimestamp>=%s)', $cutoff); + $attributes = array("entryUUID", "modifyTimestamp"); + $messages = array(); + + $base_dns = explode("|", LDAP_BASE_DNS); + foreach ($base_dns as $base_dn) { + $folder = explode(":", $base_dn); + if ($folder[0] == $folderid) { + $base_dn = str_replace('%u', $this->user, $folder[1]); + $results = ldap_list($this->ldap_link, $base_dn, $filter, $attributes); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendLDAP->GetMessageList(): Got %s contacts in base_dn '%s'.", ldap_count_entries($this->ldap_link, $results), $base_dn)); + $entries = ldap_get_entries($this->ldap_link, $results); + for ($i = 0; $i < $entries["count"]; $i++) { + $message = array(); + $message["id"] = $entries[$i]["entryuuid"][0]; + $message["mod"] = $entries[$i]["modifytimestamp"][0]; + $message["flags"] = "1"; + $messages[] = $message; + } + } + } + return $messages; + } + + public function GetMessage($folderid, $id, $contentparameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendLDAP->GetMessage('%s','%s')", $folderid, $id)); + + $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation()); + $base_dns = explode("|", LDAP_BASE_DNS); + foreach ($base_dns as $base_dn) { + $folder = explode(":", $base_dn); + if ($folder[0] == $folderid) { + $base_dn = str_replace('%u', $this->user, $folder[1]); + $result_id = ldap_list($this->ldap_link, $base_dn, "(entryUUID=".$id.")"); + if ($result_id) { + $entry_id = ldap_first_entry($this->ldap_link, $result_id); + if ($entry_id) { + return $this->_ParseLDAPMessage($result_id, $entry_id, $truncsize); + } + } + } + } + } + + private function _ParseLDAPMessage($result_id, $entry_id, $truncsize = -1) { + $contact = new SyncContact(); + + $values = ldap_get_attributes($this->ldap_link, $entry_id); + for ($i = 0; $i < $values["count"]; $i++) { + $name = $values[$i]; + $value = $values[$name][0]; + + switch ($name) { + //person + case "cn": + case "fileAs": + $contact->fileas = $value; + break; + case "sn": + $contact->lastname = $value; + break; + //inetOrgPerson + case "departmentNumber": + $contact->department = $value; + break; + case "givenName": + $contact->firstname = $value; + break; + case "homePhone": + $contact->homephonenumber = $value; + if ($values[$name]["count"] >= 2) { + $contact->home2phonenumber = $values[$name][1]; + } + break; + case "jpegPhoto": + $contact->picture = base64_encode($value); + break; + case "labeledURI": + $contact->webpage = $value; + break; + case "mail": + $contact->email1address = $value; + if ($values[$name]["count"] >= 2) { + $contact->email2address = $values[$name][1]; + } + if ($values[$name]["count"] >= 3) { + $contact->email3address = $values[$name][2]; + } + break; + case "mobile": + $contact->mobilephonenumber = $value; + break; + case "o": + $contact->companyname = $value; + break; + case "pager": + $contact->pagernumber = $value; + break; + case "secretary": + case "assistantName": + $contact->assistantname = $value; + break; + //organizationalPerson + case "l": + $contact->businesscity = $value; + break; + case "ou": + $contact->department = $value; + break; + case "physicalDeliveryOfficeName": + $contact->officelocation = $value; + break; + case "postalCode": + $contact->businesspostalcode = $value; + break; + case "st": + $contact->businessstate = $value; + break; + case "street": + $contact->businessstreet = $value; + break; + case "telephoneNumber": + $contact->businessphonenumber = $value; + if ($values[$name]["count"] >= 2) { + $contact->business2phonenumber = $values[$name][1]; + } + break; + case "title": + $contact->title = $value; + break; + case "description": + case "note": + if (Request::GetProtocolVersion() >= 12.0) { + $contact->asbody = new SyncBaseBody(); + $contact->asbody->type = SYNC_BODYPREFERENCE_PLAIN; + $contact->asbody->data = $value; + if ($truncsize > 0 && $truncsize < strlen($contact->asbody->data)) { + $contact->asbody->truncated = 1; + $contact->asbody->data = Utils::Utf8_truncate($contact->asbody->data, $truncsize); + } + else { + $contact->asbody->truncated = 0; + } + $contact->asbody->estimatedDataSize = strlen($contact->asbody->data); + } + else { + $contact->body = $value; + if ($truncsize > 0 && $truncsize < strlen($contact->body)) { + $contact->bodytruncated = 1; + $contact->body = Utils::Utf8_truncate($contact->body, $truncsize); + } + else { + $contact->bodytruncated = 0; + } + $contact->bodysize = strlen($contact->body); + } + break; + case "assistantPhone": + $contact->assistnamephonenumber = $value; + break; + case "birthDate": + $contact->birthday = $value; + break; + case "anniversary": + $contact->anniversary = $value; + break; + case "businessRole": + $contact->jobtitle = $value; + break; + case "carPhone": + $contact->carphonenumber = $value; + break; + case "facsimileTelephoneNumber": + $contact->businessfaxnumber = $value; + break; + case "homeFacsimileTelephoneNumber": + $contact->homefaxnumber = $value; + break; + case "spouseName": + $contact->spouse = $value; + break; + case "managerName": + $contact->managername = $value; + break; + case "radio": + $contact->radiophonenumber = $value; + break; + } + } + return $contact; + } + + public function StatMessage($folderid, $id) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendLDAP->StatMessage('%s','%s')", $folderid, $id)); + $base_dns = explode("|", LDAP_BASE_DNS); + foreach ($base_dns as $base_dn) { + $folder = explode(":", $base_dn); + if ($folder[0] == $folderid) { + $base_dn = str_replace('%u', $this->user, $folder[1]); + $result_id = ldap_list($this->ldap_link, $base_dn, "(entryUUID=".$id.")", array("modifyTimestamp")); + if ($result_id) { + $entry_id = ldap_first_entry($this->ldap_link, $result_id); + if ($entry_id) { + $mod = ldap_get_values($this->ldap_link, $entry_id, "modifyTimestamp"); + $message = array(); + $message["id"] = $id; + $message["mod"] = $mod[0]; + $message["flags"] = "1"; + return $message; + } + } + } + } + } + + public function ChangeMessage($folderid, $id, $message, $contentParameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendLDAP->ChangeMessage('%s','%s')", $folderid, $id)); + $base_dns = explode("|", LDAP_BASE_DNS); + foreach ($base_dns as $base_dn) { + $folder = explode(":", $base_dn); + if ($folder[0] == $folderid) { + $base_dn = str_replace('%u', $this->user, $folder[1]); + $ldap_attributes = $this->_GenerateLDAPArray($message); + $result_id = ldap_list($this->ldap_link, $base_dn, "(entryUUID=".$id.")", array("modifyTimestamp")); + if ($result_id) { + $entry_id = ldap_first_entry($this->ldap_link, $result_id); + if ($entry_id) { + $dn = ldap_get_dn($this->ldap_link, $entry_id); + + // We cannot ldap_modify objectClass, but we can use ldap_mod_replace + $ldap_classes = array(); + $ldap_classes['objectclass'] = Array("top", "person", "inetOrgPerson", "organizationalPerson", "evolutionPerson"); + $mode = ldap_mod_replace($this->ldap_link, $dn, $ldap_classes); + + $mod = ldap_modify($this->ldap_link, $dn, $ldap_attributes); + if (!$mod) { + return false; + } + return $this->StatMessage($folderid, $id); + } + else { + $uid = time() . mt_rand(100000, 999999); + $dn = "uid=" . $uid . "," . $base_dn; + $add = ldap_add($this->ldap_link, $dn, $ldap_attributes); + if (!$add) { + return false; + } + $result = ldap_read($this->ldap_link, $dn, "objectClass=*", array("entryUUID")); + $entry = ldap_first_entry($this->ldap_link, $result); + $values = ldap_get_values($this->ldap_link, $entry, "entryUUID"); + $entryuuid = $values[0]; + return $this->StatMessage($folderid, $entryuuid); + } + } + } + } + return false; + } + + private function _GenerateLDAPArray($message) { + $ldap = array(); + //Set the Object Class + $ldap["objectClass"] = Array("top", "person", "inetOrgPerson", "organizationalPerson", "evolutionPerson"); + + //Parse Data + if ($message->fileas) { + $ldap["cn"] = $message->fileas; + $ldap["fileAs"] = $message->fileas; + } + if ($message->lastname) { + $ldap["sn"] = $message->lastname; + } + if ($message->department) { + $ldap["departmentNumber"] = $message->department; + } + if ($message->firstname) { + $ldap["givenName"] = $message->firstname; + } + if ($message->homephonenumber) { + $ldap["homePhone"][0] = $message->homephonenumber; + } + if ($message->home2phonenumber) { + $ldap["homePhone"][1] = $message->home2phonenumber; + } + if ($message->picture) { + $ldap["jpegPhoto"] = base64_decode($message->picture); + } + if ($message->webpage) { + $ldap["labeledURI"] = $message->webpage; + } + if ($message->email1address) { + $ldap["mail"][] = $message->email1address; + } + if ($message->email2address) { + $ldap["mail"][] = $message->email2address; + } + if ($message->email3address) { + $ldap["mail"][] = $message->email3address; + } + if ($message->mobilephonenumber) { + $ldap["mobile"] = $message->mobilephonenumber; + } + if ($message->companyname) { + $ldap["o"] = $message->companyname; + } + if ($message->pagernumber) { + $ldap["pager"] = $message->pagernumber; + } + if ($message->assistantname) { + $ldap["secretary"] = $message->assistantname; + $ldap["assistantName"] = $message->assistantname; + } + if ($message->businesscity) { + $ldap["l"] = $message->businesscity; + } + if ($message->department) { + $ldap["ou"] = $message->department; + } + if ($message->officelocation) { + $ldap["physicalDeliveryOfficeName"] = $message->officelocation; + } + if ($message->businesspostalcode) { + $ldap["postalCode"] = $message->businesspostalcode; + } + if ($message->businessstate) { + $ldap["st"] = $message->businessstate; + } + if ($message->businessstreet) { + $ldap["street"] = $message->businessstreet; + } + if ($message->businessphonenumber) { + $ldap["telephoneNumber"][] = $message->businessphonenumber; + } + if ($message->business2phonenumber) { + $ldap["telephoneNumber"][] = $message->business2phonenumber; + } + if ($message->title) { + $ldap["title"] = $message->title; + } + if ($message->body) { + $ldap["description"] = $message->body; + } + if ($message->asbody) { + $ldap["description"] = $message->asbody->data; + } + if ($message->assistnamephonenumber) { + $ldap["assistantPhone"] = $message->assistnamephonenumber; + } + if ($message->birthday) { + $ldap["birthDate"] = $message->birthday; + } + if ($message->anniversary) { + $ldap["anniversary"] = $message->anniversary; + } + if ($message->jobtitle) { + $ldap["businessRole"] = $message->jobtitle; + } + if ($message->carphonenumber) { + $ldap["carPhone"] = $message->carphonenumber; + } + if ($message->businessfaxnumber) { + $ldap["facsimileTelephoneNumber"] = $message->businessfaxnumber; + } + if ($message->homefaxnumber) { + $ldap["homeFacsimileTelephoneNumber"] = $message->homefaxnumber; + } + if ($message->spouse) { + $ldap["spouseName"] = $message->spouse; + } + if ($message->managername) { + $ldap["managerName"] = $message->managername; + } + if ($message->radiophonenumber) { + $ldap["radio"] = $message->radiophonenumber; + } + + return $ldap; + } + + public function SetReadFlag($folderid, $id, $flags, $contentParameters) { + return false; + } + + public function SetStarFlag($folderid, $id, $flags, $contentParameters) { + return false; + } + + public function DeleteMessage($folderid, $id, $contentParameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendLDAP->DeleteMessage('%s','%s')", $folderid, $id)); + $base_dns = explode("|", LDAP_BASE_DNS); + foreach ($base_dns as $base_dn) { + $folder = explode(":", $base_dn); + if ($folder[0] == $folderid) { + $base_dn = str_replace('%u', $this->user, $folder[1]); + $result_id = ldap_list($this->ldap_link, $base_dn, "(entryUUID=".$id.")", array("entryUUID")); + if ($result_id) { + $entry_id = ldap_first_entry($this->ldap_link, $result_id); + if ($entry_id) { + $dn = ldap_get_dn($this->ldap_link, $entry_id); + return ldap_delete($this->ldap_link, $dn); + } + } + } + } + return false; + } + + public function MoveMessage($folderid, $id, $newfolderid, $contentParameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendLDAP->MoveMessage('%s','%s', '%s')", $folderid, $id, $newfolderid)); + $base_dns = explode("|", LDAP_BASE_DNS); + $old = ""; + $new = ""; + foreach ($base_dns as $base_dn) { + $folder = explode(":", $base_dn); + if ($folder[0] == $folderid) { + $old = str_replace('%u', $this->user, $folder[1]); + } + if ($folder[0] == $newfolderid) { + $new = str_replace('%u', $this->user, $folder[1]); + } + } + $result_id = ldap_list($this->ldap_link, $old, "(entryUUID=".$id.")", array("entryUUID")); + if ($result_id) { + $entry_id = ldap_first_entry($this->ldap_link, $result_id); + if ($entry_id) { + $dn = ldap_get_dn($this->ldap_link, $entry_id); + $newdn = ldap_explode_dn($dn, 0); + return ldap_rename($this->ldap_link, $dn, $newdn[0], true); + } + } + return false; + } + + /** + * Indicates which AS version is supported by the backend. + * + * @access public + * @return string AS version constant + */ + public function GetSupportedASVersion() { + return ZPush::ASV_14; + } +} +?> diff --git a/sources/backend/maildir/maildir.php b/sources/backend/maildir/maildir.php index a3c337d..b859e74 100644 --- a/sources/backend/maildir/maildir.php +++ b/sources/backend/maildir/maildir.php @@ -57,6 +57,7 @@ require_once("backend/maildir/config.php"); include_once('lib/default/diffbackend/diffbackend.php'); + include_once('include/mimeDecode.php'); require_once('include/z_RFC822.php'); @@ -541,6 +542,22 @@ class BackendMaildir extends BackendDiff { return true; } + /** + * Changes the 'star' flag of a message on disk + * + * @param string $folderid id of the folder + * @param string $id id of the message + * @param int $flags star flag of the message + * @param ContentParameters $contentParameters + * + * @access public + * @return boolean status of the operation + * @throws StatusException could throw specific SYNC_STATUS_* exceptions + */ + public function SetStarFlag($folderid, $id, $flags, $contentParameters) { + return false; + } + /** * Called when the user has requested to delete (really delete) a message * diff --git a/sources/backend/searchldap/searchldap.php b/sources/backend/searchldap/searchldap.php index f908271..50d69e2 100644 --- a/sources/backend/searchldap/searchldap.php +++ b/sources/backend/searchldap/searchldap.php @@ -138,7 +138,7 @@ class BackendSearchLDAP implements ISearchProvider { $querycnt = $searchresult['count']; //do not return more results as requested in range $querylimit = (($rangeend + 1) < $querycnt) ? ($rangeend + 1) : $querycnt; - $items['range'] = $rangestart.'-'.($querycnt-1); + $items['range'] = $rangestart.'-'.($querylimit-1); $items['searchtotal'] = $querycnt; $rc = 0; diff --git a/sources/backend/vcarddir/vcarddir.php b/sources/backend/vcarddir/vcarddir.php index b1d8a0a..e93a07f 100644 --- a/sources/backend/vcarddir/vcarddir.php +++ b/sources/backend/vcarddir/vcarddir.php @@ -597,6 +597,22 @@ class BackendVCardDir extends BackendDiff { return false; } + /** + * Changes the 'star' flag of a message on disk + * + * @param string $folderid id of the folder + * @param string $id id of the message + * @param int $flags star flag of the message + * @param ContentParameters $contentParameters + * + * @access public + * @return boolean status of the operation + * @throws StatusException could throw specific SYNC_STATUS_* exceptions + */ + public function SetStarFlag($folderid, $id, $flags, $contentParameters) { + return false; + } + /** * Called when the user has requested to delete (really delete) a message * diff --git a/sources/backend/zarafa/importer.php b/sources/backend/zarafa/importer.php index ac9579a..5f67895 100644 --- a/sources/backend/zarafa/importer.php +++ b/sources/backend/zarafa/importer.php @@ -452,6 +452,21 @@ class ImportChangesICS implements IImportChanges { return true; } + + /** + * Imports a change in 'star' flag + * This can never conflict + * + * @param string $id + * @param int $flags + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ImportMessageStarFlag($id, $flags) { + return false; + } /** * Imports a move of a message. This occurs when a user moves an item to another folder diff --git a/sources/backend/zarafa/mapi/mapidefs.php b/sources/backend/zarafa/mapi/mapidefs.php index d450fc8..ca17dca 100644 --- a/sources/backend/zarafa/mapi/mapidefs.php +++ b/sources/backend/zarafa/mapi/mapidefs.php @@ -603,6 +603,7 @@ define('fbFree' ,0); define('fbTentative' ,1); define('fbBusy' ,2); define('fbOutOfOffice' ,3); +define('fbWorkingElsewhere' ,4); /* ICS flags */ diff --git a/sources/backend/zarafa/mapi/mapitags.php b/sources/backend/zarafa/mapi/mapitags.php index af8f1a8..18def2f 100644 --- a/sources/backend/zarafa/mapi/mapitags.php +++ b/sources/backend/zarafa/mapi/mapitags.php @@ -1165,6 +1165,15 @@ define('PR_EC_STATS_SESSION_LOCKED' ,mapi_prop_tag(PT_BOOLEAN, define('PR_EC_STATS_SESSION_BUSYSTATES' ,mapi_prop_tag(PT_MV_STRING8, PR_EC_BASE+0x47)); define('PR_EC_COMPANY_NAME' ,mapi_prop_tag(PT_STRING8, PR_EC_BASE+0x48)); +/* user features */ +define('PR_EC_ENABLED_FEATURES' ,mapi_prop_tag(PT_MV_TSTRING, PR_EC_BASE+0xB3)); +define('PR_EC_ENABLED_FEATURES_A' ,mapi_prop_tag(PT_MV_STRING8, PR_EC_BASE+0xB3)); +define('PR_EC_ENABLED_FEATURES_W' ,mapi_prop_tag(PT_MV_UNICODE, PR_EC_BASE+0xB3)); + +define('PR_EC_DISABLED_FEATURES' ,mapi_prop_tag(PT_MV_TSTRING, PR_EC_BASE+0xB4)); +define('PR_EC_DISABLED_FEATURES_A' ,mapi_prop_tag(PT_MV_STRING8, PR_EC_BASE+0xB4)); +define('PR_EC_DISABLED_FEATURES_W' ,mapi_prop_tag(PT_MV_UNICODE, PR_EC_BASE+0xB4)); + /* WA properties */ define('PR_EC_WA_ATTACHMENT_HIDDEN_OVERRIDE' ,mapi_prop_tag(PT_BOOLEAN, PR_EC_BASE+0xE0)); diff --git a/sources/backend/zarafa/mapimapping.php b/sources/backend/zarafa/mapimapping.php index 8d85586..0c5be9f 100644 --- a/sources/backend/zarafa/mapimapping.php +++ b/sources/backend/zarafa/mapimapping.php @@ -224,7 +224,7 @@ class MAPIMapping { "responserequested" => PR_RESPONSE_REQUESTED, // timezone "alldayevent" => "PT_BOOLEAN:PSETID_Appointment:0x8215", - "busystatus" => "PT_LONG:PSETID_Appointment:0x8205", + "busystatus" => "PT_LONG:PSETID_Appointment:0x8224", "rtf" => PR_RTF_COMPRESSED, "dtstamp" => PR_LAST_MODIFICATION_TIME, "endtime" => "PT_SYSTIME:PSETID_Appointment:0x820e", diff --git a/sources/backend/zarafa/mapiprovider.php b/sources/backend/zarafa/mapiprovider.php index 56834ed..85a4444 100644 --- a/sources/backend/zarafa/mapiprovider.php +++ b/sources/backend/zarafa/mapiprovider.php @@ -231,7 +231,7 @@ class MAPIProvider { // set server default timezone (correct timezone should be configured!) $tz = TimezoneUtil::GetFullTZ(); } - $message->timezone = base64_encode($this->getSyncBlobFromTZ($tz)); + $message->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz)); if(isset($messageprops[$appointmentprops["isrecurring"]]) && $messageprops[$appointmentprops["isrecurring"]]) { // Process recurrence @@ -314,6 +314,11 @@ class MAPIProvider { if (!isset($message->nativebodytype)) $message->nativebodytype = $this->getNativeBodyType($messageprops); + // If the user is working from a location other than the office the busystatus should be interpreted as free. + if (isset($message->busystatus) && $message->busystatus == fbWorkingElsewhere) { + $message->busystatus = fbFree; + } + return $message; } @@ -459,6 +464,10 @@ class MAPIProvider { if(!isset($syncMessage->exceptions)) $syncMessage->exceptions = array(); + // If the user is working from a location other than the office the busystatus should be interpreted as free. + if (isset($exception->busystatus) && $exception->busystatus == fbWorkingElsewhere) { + $exception->busystatus = fbFree; + } array_push($syncMessage->exceptions, $exception); } @@ -544,7 +553,7 @@ class MAPIProvider { else $tz = $this->getGMTTZ(); - $message->meetingrequest->timezone = base64_encode($this->getSyncBlobFromTZ($tz)); + $message->meetingrequest->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz)); // send basedate if exception if(isset($props[$meetingrequestproperties["recReplTime"]]) || @@ -624,6 +633,7 @@ class MAPIProvider { $req->processMeetingCancellation(); } } + $message->contentclass = DEFAULT_CALENDAR_CONTENTCLASS; } // Add attachments @@ -746,7 +756,7 @@ class MAPIProvider { //TODO contentclass and nativebodytype and internetcpid if (!isset($message->internetcpid)) $message->internetcpid = (defined('STORE_INTERNET_CPID')) ? constant('STORE_INTERNET_CPID') : INTERNET_CPID_WINDOWS1252; $this->setFlag($mapimessage, $message); - $message->contentclass = DEFAULT_EMAIL_CONTENTCLASS; + if (!isset($message->contentclass)) $message->contentclass = DEFAULT_EMAIL_CONTENTCLASS; if (!isset($message->nativebodytype)) $message->nativebodytype = $this->getNativeBodyType($messageprops); // reply, reply to all, forward flags @@ -1842,27 +1852,6 @@ class MAPIProvider { return $tz; } - /** - * Pack timezone info for Sync - * - * @param array $tz - * - * @access private - * @return string - */ - private function getSyncBlobFromTZ($tz) { - // set the correct TZ name (done using the Bias) - if (!isset($tz["tzname"]) || !$tz["tzname"] || !isset($tz["tznamedst"]) || !$tz["tznamedst"]) - $tz = TimezoneUtil::FillTZNames($tz); - - $packed = pack("la64vvvvvvvv" . "la64vvvvvvvv" . "l", - $tz["bias"], $tz["tzname"], 0, $tz["dstendmonth"], $tz["dstendday"], $tz["dstendweek"], $tz["dstendhour"], $tz["dstendminute"], $tz["dstendsecond"], $tz["dstendmillis"], - $tz["stdbias"], $tz["tznamedst"], 0, $tz["dststartmonth"], $tz["dststartday"], $tz["dststartweek"], $tz["dststarthour"], $tz["dststartminute"], $tz["dststartsecond"], $tz["dststartmillis"], - $tz["dstbias"]); - - return $packed; - } - /** * Pack timezone info for MAPI * diff --git a/sources/backend/zarafa/zarafa.php b/sources/backend/zarafa/zarafa.php index 7f2b37a..93c9d55 100644 --- a/sources/backend/zarafa/zarafa.php +++ b/sources/backend/zarafa/zarafa.php @@ -91,6 +91,9 @@ class BackendZarafa implements IBackend, ISearchProvider { private $wastebasket; private $addressbook; + // ZCP config parameter for PR_EC_ENABLED_FEATURES / PR_EC_DISABLED_FEATURES + const ZPUSH_ENABLED = 'mobile'; + /** * Constructor of the Zarafa Backend * @@ -200,6 +203,8 @@ class BackendZarafa implements IBackend, ISearchProvider { ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->Logon(): User '%s' is authenticated",$user)); + $this->isZPushEnabled(); + // check if this is a Zarafa 7 store with unicode support MAPIUtils::IsUnicodeStore($this->store); return true; @@ -1180,6 +1185,22 @@ class BackendZarafa implements IBackend, ISearchProvider { } } + /** + * Returns the email address and the display name of the user. Used by autodiscover. + * + * @param string $username The username + * + * @access public + * @return Array + */ + public function GetUserDetails($username) { + ZLog::Write(LOGLEVEL_WBXML, sprintf("ZarafaBackend->GetUserDetails for '%s'.", $username)); + $zarafauserinfo = @mapi_zarafa_getuser_by_name($this->defaultstore, $username); + $userDetails['emailaddress'] = (isset($zarafauserinfo['emailaddress']) && $zarafauserinfo['emailaddress']) ? $zarafauserinfo['emailaddress'] : false; + $userDetails['fullname'] = (isset($zarafauserinfo['fullname']) && $zarafauserinfo['fullname']) ? $zarafauserinfo['fullname'] : false; + return $userDetails; + } + /**---------------------------------------------------------------------------------------------------------- * Private methods @@ -1549,8 +1570,11 @@ class BackendZarafa implements IBackend, ISearchProvider { $searchGreater = strtotime($cpo->GetSearchValueGreater()); $searchLess = strtotime($cpo->GetSearchValueLess()); + if (version_compare(phpversion(),'5.3.4') < 0) { + ZLog::Write(LOGLEVEL_WARN, sprintf("Your system's PHP version (%s) might not correctly process unicode strings. Search containing such characters might not return correct results. It is recommended to update to at least PHP 5.3.4. See ZP-541 for more information.", phpversion())); + } // split the search on whitespache and look for every word - $searchText = preg_split("/\W+/", $searchText); + $searchText = preg_split("/\W+/u", $searchText); $searchProps = array(PR_BODY, PR_SUBJECT, PR_DISPLAY_TO, PR_DISPLAY_CC, PR_SENDER_NAME, PR_SENDER_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS); $resAnd = array(); foreach($searchText as $term) { @@ -1831,6 +1855,25 @@ class BackendZarafa implements IBackend, ISearchProvider { } return $this->addressbook; } + + /** + * Checks if the user is not disabled for Z-Push. + * + * @access private + * @throws FatalException if user is disabled for Z-Push + * + * @return boolean + */ + private function isZPushEnabled() { + $addressbook = $this->getAddressbook(); + $userEntryid = mapi_getprops($this->store, array(PR_MAILBOX_OWNER_ENTRYID)); + $mailuser = mapi_ab_openentry($addressbook, $userEntryid[PR_MAILBOX_OWNER_ENTRYID]); + $enabledFeatures = mapi_getprops($mailuser, array(PR_EC_DISABLED_FEATURES)); + if (isset($enabledFeatures[PR_EC_DISABLED_FEATURES]) && is_array($enabledFeatures[PR_EC_DISABLED_FEATURES]) && in_array(self::ZPUSH_ENABLED, $enabledFeatures[PR_EC_DISABLED_FEATURES])) { + throw new FatalException("User is disabled for Z-Push."); + } + return true; + } } /** diff --git a/sources/config.php b/sources/config.php index 6f61c89..759e7e8 100644 --- a/sources/config.php +++ b/sources/config.php @@ -60,12 +60,68 @@ // This setting specifies the owner parameter in the certificate to look at. define("CERTIFICATE_OWNER_PARAMETER", "SSL_CLIENT_S_DN_CN"); + /* + * Whether to use the complete email address as a login name + * (e.g. user@company.com) or the username only (user). + * This is required for Z-Push to work properly after autodiscover. + * Possible values: + * false - use the username only (default). + * true - use the complete email address. + */ + define('USE_FULLEMAIL_FOR_LOGIN', false); + +/********************************************************************************** + * Device pre-authorization. Useful when using Z-Push as a standalone product. + * + * It will use the STATE_MACHINE specified below, to store the users/devices + * FILE => STATE_DIR/PreAuthUserDevices + * SQL => auth_users + * + * FALSE => default + * TRUE + */ + define('PRE_AUTHORIZE_USERS', false); + + // New users are pre-authorized automatically + define('PRE_AUTHORIZE_NEW_USERS', false); + + // New devices are pre-authorized automatically for pre-authorized users + define('PRE_AUTHORIZE_NEW_DEVICES', false); + + // Max number of devices pre-authorized for user, you can pre-authorize more manually + define('PRE_AUTHORIZE_MAX_DEVICES', 5); + + +/********************************************************************************** + * Select StateMachine mechanism + * + * FILE => FileStateMachine, default + * SQL => SqlStateMachine + */ + define('STATE_MACHINE', 'FILE'); + /********************************************************************************** * Default FileStateMachine settings */ define('STATE_DIR', '/var/lib/z-push/'); +/********************************************************************************** + * Optional SqlStateMachine settings + * + * DSN: formatted PDO connection string + * mysql:host=xxx;port=xxx;dbname=xxx + * DON'T FORGET TO INSTALL THE PHP-DRIVER PACKAGE!!! + * USER: username to DB + * PASSWORD: password to DB + * OPTIONS: array with options needed + */ + define('STATE_SQL_DSN', ''); + define('STATE_SQL_USER', ''); + define('STATE_SQL_PASSWORD', ''); + define('STATE_SQL_OPTIONS', serialize(array(PDO::ATTR_PERSISTENT => true))); + + /********************************************************************************** * Logging settings * Possible LOGLEVEL and LOGUSERLEVEL values are: @@ -192,6 +248,13 @@ // this full list, so this feature is disabled by default. Enable with care. define('ALLOW_WEBSERVICE_USERS_ACCESS', false); + // Users with many folders can use the 'partial foldersync' feature, where the server + // actively stops processing the folder list if it takes too long. Other requests are + // then redirected to the FolderSync to synchronize the remaining items. + // Device compatibility for this procedure is not fully understood. + // NOTE: THIS IS AN EXPERIMENTAL FEATURE WHICH COULD PREVENT YOUR MOBILES FROM SYNCHRONIZING. + define('USE_PARTIAL_FOLDERSYNC', false); + /********************************************************************************** * Backend settings */ diff --git a/sources/include/Auth/SASL.php b/sources/include/Auth/SASL.php new file mode 100755 index 0000000..7ccce4e --- /dev/null +++ b/sources/include/Auth/SASL.php @@ -0,0 +1,150 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Client implementation of various SASL mechanisms +* +* @author Richard Heyes +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + + +/** + * Z-Push changes + * + * removed PEAR dependency by implementing own raiseError() + * + * Reference implementation used: + * http://download.pear.php.net/package/Auth_SASL-1.0.6.tgz + * + * + */ + +//require_once('PEAR.php'); + +class Auth_SASL +{ + /** + * Factory class. Returns an object of the request + * type. + * + * @param string $type One of: Anonymous + * Plain + * CramMD5 + * DigestMD5 + * SCRAM-* (any mechanism of the SCRAM family) + * Types are not case sensitive + */ + function &factory($type) + { + switch (strtolower($type)) { + case 'anonymous': + $filename = 'include/Auth/SASL/Anonymous.php'; + $classname = 'Auth_SASL_Anonymous'; + break; + + case 'login': + $filename = 'include/Auth/SASL/Login.php'; + $classname = 'Auth_SASL_Login'; + break; + + case 'plain': + $filename = 'include/Auth/SASL/Plain.php'; + $classname = 'Auth_SASL_Plain'; + break; + + case 'external': + $filename = 'include/Auth/SASL/External.php'; + $classname = 'Auth_SASL_External'; + break; + + case 'crammd5': + // $msg = 'Deprecated mechanism name. Use IANA-registered name: CRAM-MD5.'; + // trigger_error($msg, E_USER_DEPRECATED); + case 'cram-md5': + $filename = 'include/Auth/SASL/CramMD5.php'; + $classname = 'Auth_SASL_CramMD5'; + break; + + case 'digestmd5': + // $msg = 'Deprecated mechanism name. Use IANA-registered name: DIGEST-MD5.'; + // trigger_error($msg, E_USER_DEPRECATED); + case 'digest-md5': + // $msg = 'DIGEST-MD5 is a deprecated SASL mechanism as per RFC-6331. Using it could be a security risk.'; + // trigger_error($msg, E_USER_NOTICE); + $filename = 'include/Auth/SASL/DigestMD5.php'; + $classname = 'Auth_SASL_DigestMD5'; + break; + + default: + $scram = '/^SCRAM-(.{1,9})$/i'; + if (preg_match($scram, $type, $matches)) + { + $hash = $matches[1]; + $filename = 'include/Auth/SASL/SCRAM.php'; + $classname = 'Auth_SASL_SCRAM'; + $parameter = $hash; + break; + } + return Auth_SASL::raiseError('Invalid SASL mechanism type'); + break; + } + + require_once($filename); + if (isset($parameter)) + $obj = new $classname($parameter); + else + $obj = new $classname(); + return $obj; + } + + /** + * Z-Push helper for error logging + * removing PEAR dependency + * + * @param string debug message + * @return boolean always false as there was an error + * @access private + */ + static function raiseError($message) { + ZLog::Write(LOGLEVEL_ERROR, "Auth_SASL error: ". $message); + return false; + } +} + +?> diff --git a/sources/include/Auth/SASL/Anonymous.php b/sources/include/Auth/SASL/Anonymous.php new file mode 100755 index 0000000..dfccab6 --- /dev/null +++ b/sources/include/Auth/SASL/Anonymous.php @@ -0,0 +1,71 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implmentation of ANONYMOUS SASL mechanism +* +* @author Richard Heyes +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('include/Auth/SASL/Common.php'); + +class Auth_SASL_Anonymous extends Auth_SASL_Common +{ + /** + * Not much to do here except return the token supplied. + * No encoding, hashing or encryption takes place for this + * mechanism, simply one of: + * o An email address + * o An opaque string not containing "@" that can be interpreted + * by the sysadmin + * o Nothing + * + * We could have some logic here for the second option, but this + * would by no means create something interpretable. + * + * @param string $token Optional email address or string to provide + * as trace information. + * @return string The unaltered input token + */ + function getResponse($token = '') + { + return $token; + } +} +?> \ No newline at end of file diff --git a/sources/include/Auth/SASL/Common.php b/sources/include/Auth/SASL/Common.php new file mode 100755 index 0000000..af2641f --- /dev/null +++ b/sources/include/Auth/SASL/Common.php @@ -0,0 +1,129 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Common functionality to SASL mechanisms +* +* @author Richard Heyes +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +/** + * Z-Push changes + * + * removed PEAR dependency by implementing own raiseError() + * + * Reference implementation used: + * http://download.pear.php.net/package/Auth_SASL-1.0.6.tgz + * + * + */ + +class Auth_SASL_Common +{ + /** + * Function which implements HMAC MD5 digest + * + * @param string $key The secret key + * @param string $data The data to hash + * @param bool $raw_output Whether the digest is returned in binary or hexadecimal format. + * + * @return string The HMAC-MD5 digest + */ + function _HMAC_MD5($key, $data, $raw_output = FALSE) + { + if (strlen($key) > 64) { + $key = pack('H32', md5($key)); + } + + if (strlen($key) < 64) { + $key = str_pad($key, 64, chr(0)); + } + + $k_ipad = substr($key, 0, 64) ^ str_repeat(chr(0x36), 64); + $k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64); + + $inner = pack('H32', md5($k_ipad . $data)); + $digest = md5($k_opad . $inner, $raw_output); + + return $digest; + } + + /** + * Function which implements HMAC-SHA-1 digest + * + * @param string $key The secret key + * @param string $data The data to hash + * @param bool $raw_output Whether the digest is returned in binary or hexadecimal format. + * @return string The HMAC-SHA-1 digest + * @author Jehan + * @access protected + */ + protected function _HMAC_SHA1($key, $data, $raw_output = FALSE) + { + if (strlen($key) > 64) { + $key = sha1($key, TRUE); + } + + if (strlen($key) < 64) { + $key = str_pad($key, 64, chr(0)); + } + + $k_ipad = substr($key, 0, 64) ^ str_repeat(chr(0x36), 64); + $k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64); + + $inner = pack('H40', sha1($k_ipad . $data)); + $digest = sha1($k_opad . $inner, $raw_output); + + return $digest; + } + +/** + * Z-Push helper for error logging + * removing PEAR dependency + * + * @param string debug message + * @return boolean always false as there was an error + * @access private + */ + function raiseError($message) { + ZLog::Write(LOGLEVEL_ERROR, "SCRAM error: ". $message); + return false; + } +} +?> diff --git a/sources/include/Auth/SASL/CramMD5.php b/sources/include/Auth/SASL/CramMD5.php new file mode 100755 index 0000000..09e7667 --- /dev/null +++ b/sources/include/Auth/SASL/CramMD5.php @@ -0,0 +1,68 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implmentation of CRAM-MD5 SASL mechanism +* +* @author Richard Heyes +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('include/Auth/SASL/Common.php'); + +class Auth_SASL_CramMD5 extends Auth_SASL_Common +{ + /** + * Implements the CRAM-MD5 SASL mechanism + * This DOES NOT base64 encode the return value, + * you will need to do that yourself. + * + * @param string $user Username + * @param string $pass Password + * @param string $challenge The challenge supplied by the server. + * this should be already base64_decoded. + * + * @return string The string to pass back to the server, of the form + * " ". This is NOT base64_encoded. + */ + function getResponse($user, $pass, $challenge) + { + return $user . ' ' . $this->_HMAC_MD5($pass, $challenge); + } +} +?> \ No newline at end of file diff --git a/sources/include/Auth/SASL/DigestMD5.php b/sources/include/Auth/SASL/DigestMD5.php new file mode 100755 index 0000000..892c217 --- /dev/null +++ b/sources/include/Auth/SASL/DigestMD5.php @@ -0,0 +1,197 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implmentation of DIGEST-MD5 SASL mechanism +* +* @author Richard Heyes +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('include/Auth/SASL/Common.php'); + +class Auth_SASL_DigestMD5 extends Auth_SASL_Common +{ + /** + * Provides the (main) client response for DIGEST-MD5 + * requires a few extra parameters than the other + * mechanisms, which are unavoidable. + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $challenge The digest challenge sent by the server + * @param string $hostname The hostname of the machine you're connecting to + * @param string $service The servicename (eg. imap, pop, acap etc) + * @param string $authzid Authorization id (username to proxy as) + * @return string The digest response (NOT base64 encoded) + * @access public + */ + function getResponse($authcid, $pass, $challenge, $hostname, $service, $authzid = '') + { + $challenge = $this->_parseChallenge($challenge); + $authzid_string = ''; + if ($authzid != '') { + $authzid_string = ',authzid="' . $authzid . '"'; + } + + if (!empty($challenge)) { + $cnonce = $this->_getCnonce(); + $digest_uri = sprintf('%s/%s', $service, $hostname); + $response_value = $this->_getResponseValue($authcid, $pass, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $authzid); + + if ($challenge['realm']) { + return sprintf('username="%s",realm="%s"' . $authzid_string . +',nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,maxbuf=%d', $authcid, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']); + } else { + return sprintf('username="%s"' . $authzid_string . ',nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,maxbuf=%d', $authcid, $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']); + } + } else { + return $this->raiseError('Invalid digest challenge'); + } + } + + /** + * Parses and verifies the digest challenge* + * + * @param string $challenge The digest challenge + * @return array The parsed challenge as an assoc + * array in the form "directive => value". + * @access private + */ + function _parseChallenge($challenge) + { + $tokens = array(); + while (preg_match('/^([a-z-]+)=("[^"]+(? diff --git a/sources/include/Auth/SASL/External.php b/sources/include/Auth/SASL/External.php new file mode 100644 index 0000000..7e96d8e --- /dev/null +++ b/sources/include/Auth/SASL/External.php @@ -0,0 +1,63 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implmentation of EXTERNAL SASL mechanism +* +* @author Christoph Schulz +* @access public +* @version 1.0.3 +* @package Auth_SASL +*/ + +require_once('include/Auth/SASL/Common.php'); + +class Auth_SASL_External extends Auth_SASL_Common +{ + /** + * Returns EXTERNAL response + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $authzid Autorization id + * @return string EXTERNAL Response + */ + function getResponse($authcid, $pass, $authzid = '') + { + return $authzid; + } +} +?> diff --git a/sources/include/Auth/SASL/Login.php b/sources/include/Auth/SASL/Login.php new file mode 100755 index 0000000..cf988f5 --- /dev/null +++ b/sources/include/Auth/SASL/Login.php @@ -0,0 +1,65 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* This is technically not a SASL mechanism, however +* it's used by Net_Sieve, Net_Cyrus and potentially +* other protocols , so here is a good place to abstract +* it. +* +* @author Richard Heyes +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('include/Auth/SASL/Common.php'); + +class Auth_SASL_Login extends Auth_SASL_Common +{ + /** + * Pseudo SASL LOGIN mechanism + * + * @param string $user Username + * @param string $pass Password + * @return string LOGIN string + */ + function getResponse($user, $pass) + { + return sprintf('LOGIN %s %s', $user, $pass); + } +} +?> \ No newline at end of file diff --git a/sources/include/Auth/SASL/Plain.php b/sources/include/Auth/SASL/Plain.php new file mode 100755 index 0000000..4de1813 --- /dev/null +++ b/sources/include/Auth/SASL/Plain.php @@ -0,0 +1,63 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implmentation of PLAIN SASL mechanism +* +* @author Richard Heyes +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('include/Auth/SASL/Common.php'); + +class Auth_SASL_Plain extends Auth_SASL_Common +{ + /** + * Returns PLAIN response + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $authzid Autorization id + * @return string PLAIN Response + */ + function getResponse($authcid, $pass, $authzid = '') + { + return $authzid . chr(0) . $authcid . chr(0) . $pass; + } +} +?> diff --git a/sources/include/Auth/SASL/SCRAM.php b/sources/include/Auth/SASL/SCRAM.php new file mode 100644 index 0000000..5a5dd3b --- /dev/null +++ b/sources/include/Auth/SASL/SCRAM.php @@ -0,0 +1,305 @@ + +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('include/Auth/SASL/Common.php'); + +class Auth_SASL_SCRAM extends Auth_SASL_Common +{ + /** + * Construct a SCRAM-H client where 'H' is a cryptographic hash function. + * + * @param string $hash The name cryptographic hash function 'H' as registered by IANA in the "Hash Function Textual + * Names" registry. + * @link http://www.iana.org/assignments/hash-function-text-names/hash-function-text-names.xml "Hash Function Textual + * Names" + * format of core PHP hash function. + * @access public + */ + function __construct($hash) + { + // Though I could be strict, I will actually also accept the naming used in the PHP core hash framework. + // For instance "sha1" is accepted, while the registered hash name should be "SHA-1". + $hash = strtolower($hash); + $hashes = array('md2' => 'md2', + 'md5' => 'md5', + 'sha-1' => 'sha1', + 'sha1' => 'sha1', + 'sha-224' > 'sha224', + 'sha224' > 'sha224', + 'sha-256' => 'sha256', + 'sha256' => 'sha256', + 'sha-384' => 'sha384', + 'sha384' => 'sha384', + 'sha-512' => 'sha512', + 'sha512' => 'sha512'); + if (function_exists('hash_hmac') && isset($hashes[$hash])) + { + $this->hash = create_function('$data', 'return hash("' . $hashes[$hash] . '", $data, TRUE);'); + $this->hmac = create_function('$key,$str,$raw', 'return hash_hmac("' . $hashes[$hash] . '", $str, $key, $raw);'); + } + elseif ($hash == 'md5') + { + $this->hash = create_function('$data', 'return md5($data, true);'); + $this->hmac = array($this, '_HMAC_MD5'); + } + elseif (in_array($hash, array('sha1', 'sha-1'))) + { + $this->hash = create_function('$data', 'return sha1($data, true);'); + $this->hmac = array($this, '_HMAC_SHA1'); + } + else + return $this->raiseError('Invalid SASL mechanism type'); + } + + /** + * Provides the (main) client response for SCRAM-H. + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $challenge The challenge sent by the server. + * If the challenge is NULL or an empty string, the result will be the "initial response". + * @param string $authzid Authorization id (username to proxy as) + * @return string|false The response (binary, NOT base64 encoded) + * @access public + */ + public function getResponse($authcid, $pass, $challenge = NULL, $authzid = NULL) + { + $authcid = $this->_formatName($authcid); + if (empty($authcid)) + { + return false; + } + if (!empty($authzid)) + { + $authzid = $this->_formatName($authzid); + if (empty($authzid)) + { + return false; + } + } + + if (empty($challenge)) + { + return $this->_generateInitialResponse($authcid, $authzid); + } + else + { + return $this->_generateResponse($challenge, $pass); + } + + } + + /** + * Prepare a name for inclusion in a SCRAM response. + * + * @param string $username a name to be prepared. + * @return string the reformated name. + * @access private + */ + private function _formatName($username) + { + // TODO: prepare through the SASLprep profile of the stringprep algorithm. + // See RFC-4013. + + $username = str_replace('=', '=3D', $username); + $username = str_replace(',', '=2C', $username); + return $username; + } + + /** + * Generate the initial response which can be either sent directly in the first message or as a response to an empty + * server challenge. + * + * @param string $authcid Prepared authentication identity. + * @param string $authzid Prepared authorization identity. + * @return string The SCRAM response to send. + * @access private + */ + private function _generateInitialResponse($authcid, $authzid) + { + $init_rep = ''; + $gs2_cbind_flag = 'n,'; // TODO: support channel binding. + $this->gs2_header = $gs2_cbind_flag . (!empty($authzid)? 'a=' . $authzid : '') . ','; + + // I must generate a client nonce and "save" it for later comparison on second response. + $this->cnonce = $this->_getCnonce(); + // XXX: in the future, when mandatory and/or optional extensions are defined in any updated RFC, + // this message can be updated. + $this->first_message_bare = 'n=' . $authcid . ',r=' . $this->cnonce; + return $this->gs2_header . $this->first_message_bare; + } + + /** + * Parses and verifies a non-empty SCRAM challenge. + * + * @param string $challenge The SCRAM challenge + * @return string|false The response to send; false in case of wrong challenge or if an initial response has not + * been generated first. + * @access private + */ + private function _generateResponse($challenge, $password) + { + // XXX: as I don't support mandatory extension, I would fail on them. + // And I simply ignore any optional extension. + $server_message_regexp = "#^r=([\x21-\x2B\x2D-\x7E]+),s=((?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9]{3}=|[A-Xa-z0-9]{2}==)?),i=([0-9]*)(,[A-Za-z]=[^,])*$#"; + if (!isset($this->cnonce, $this->gs2_header) + || !preg_match($server_message_regexp, $challenge, $matches)) + { + return false; + } + $nonce = $matches[1]; + $salt = base64_decode($matches[2]); + if (!$salt) + { + // Invalid Base64. + return false; + } + $i = intval($matches[3]); + + $cnonce = substr($nonce, 0, strlen($this->cnonce)); + if ($cnonce <> $this->cnonce) + { + // Invalid challenge! Are we under attack? + return false; + } + + $channel_binding = 'c=' . base64_encode($this->gs2_header); // TODO: support channel binding. + $final_message = $channel_binding . ',r=' . $nonce; // XXX: no extension. + + // TODO: $password = $this->normalize($password); // SASLprep profile of stringprep. + $saltedPassword = $this->hi($password, $salt, $i); + $this->saltedPassword = $saltedPassword; + $clientKey = call_user_func($this->hmac, $saltedPassword, "Client Key", TRUE); + $storedKey = call_user_func($this->hash, $clientKey, TRUE); + $authMessage = $this->first_message_bare . ',' . $challenge . ',' . $final_message; + $this->authMessage = $authMessage; + $clientSignature = call_user_func($this->hmac, $storedKey, $authMessage, TRUE); + $clientProof = $clientKey ^ $clientSignature; + $proof = ',p=' . base64_encode($clientProof); + + return $final_message . $proof; + } + + /** + * SCRAM has also a server verification step. On a successful outcome, it will send additional data which must + * absolutely be checked against this function. If this fails, the entity which we are communicating with is probably + * not the server as it has not access to your ServerKey. + * + * @param string $data The additional data sent along a successful outcome. + * @return bool Whether the server has been authenticated. + * If false, the client must close the connection and consider to be under a MITM attack. + * @access public + */ + public function processOutcome($data) + { + $verifier_regexp = '#^v=((?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9]{3}=|[A-Xa-z0-9]{2}==)?)$#'; + if (!isset($this->saltedPassword, $this->authMessage) + || !preg_match($verifier_regexp, $data, $matches)) + { + // This cannot be an outcome, you never sent the challenge's response. + return false; + } + + $verifier = $matches[1]; + $proposed_serverSignature = base64_decode($verifier); + $serverKey = call_user_func($this->hmac, $this->saltedPassword, "Server Key", true); + $serverSignature = call_user_func($this->hmac, $serverKey, $this->authMessage, TRUE); + return ($proposed_serverSignature === $serverSignature); + } + + /** + * Hi() call, which is essentially PBKDF2 (RFC-2898) with HMAC-H() as the pseudorandom function. + * + * @param string $str The string to hash. + * @param string $hash The hash value. + * @param int $i The iteration count. + * @access private + */ + private function hi($str, $salt, $i) + { + $int1 = "\0\0\0\1"; + $ui = call_user_func($this->hmac, $str, $salt . $int1, true); + $result = $ui; + for ($k = 1; $k < $i; $k++) + { + $ui = call_user_func($this->hmac, $str, $ui, true); + $result = $result ^ $ui; + } + return $result; + } + + + /** + * Creates the client nonce for the response + * + * @return string The cnonce value + * @access private + * @author Richard Heyes + */ + private function _getCnonce() + { + // TODO: I reused the nonce function from the DigestMD5 class. + // I should probably make this a protected function in Common. + if (@file_exists('/dev/urandom') && $fd = @fopen('/dev/urandom', 'r')) { + return base64_encode(fread($fd, 32)); + + } elseif (@file_exists('/dev/random') && $fd = @fopen('/dev/random', 'r')) { + return base64_encode(fread($fd, 32)); + + } else { + $str = ''; + for ($i=0; $i<32; $i++) { + $str .= chr(mt_rand(0, 255)); + } + + return base64_encode($str); + } + } +} + +?> diff --git a/sources/include/Mail.php b/sources/include/Mail.php new file mode 100644 index 0000000..645bff7 --- /dev/null +++ b/sources/include/Mail.php @@ -0,0 +1,300 @@ + + * @copyright 1997-2010 Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Mail.php 307489 2011-01-14 19:06:57Z alec $ + * @link http://pear.php.net/package/Mail/ + */ + + + /** + * Z-Push changes + * + * removed PEAR dependency by implementing own raiseError() + * + * Reference implementation used: + * http://download.pear.php.net/package/Mail-1.2.0.tgz + * SVN trunk version r333509 + * + * + */ + + +/** + * PEAR's Mail:: interface. Defines the interface for implementing + * mailers under the PEAR hierarchy, and provides supporting functions + * useful in multiple mailer backends. + * + * @access public + * @version $Revision: 307489 $ + * @package Mail + */ +class Mail +{ + /** + * Line terminator used for separating header lines. + * @var string + */ + var $sep = "\r\n"; + + /** + * Provides an interface for generating Mail:: objects of various + * types + * + * @param string $driver The kind of Mail:: object to instantiate. + * @param array $params The parameters to pass to the Mail:: object. + * @return object Mail a instance of the driver class or if fails a PEAR Error + * @access public + */ + static function &factory($driver, $params = array()) + { + $driver = strtolower($driver); + @include_once 'include/Mail/' . $driver . '.php'; + $class = 'Mail_' . $driver; + if (class_exists($class)) { + $mailer = new $class($params); + return $mailer; + } else { + return Mail::raiseError('Unable to find class for driver ' . $driver); + } + } + + /** + * Implements Mail::send() function using php's built-in mail() + * command. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * + * @access public + * @deprecated use Mail_mail::send instead + */ + function send($recipients, $headers, $body) + { + if (!is_array($headers)) { + return Mail::raiseError('$headers must be an array'); + } + + $result = $this->_sanitizeHeaders($headers); + //if (is_a($result, 'PEAR_Error')) { + if ($result === false) { + return $result; + } + + // if we're passed an array of recipients, implode it. + if (is_array($recipients)) { + $recipients = implode(', ', $recipients); + } + + // get the Subject out of the headers array so that we can + // pass it as a seperate argument to mail(). + $subject = ''; + if (isset($headers['Subject'])) { + $subject = $headers['Subject']; + unset($headers['Subject']); + } + + // flatten the headers out. + list(, $text_headers) = Mail::prepareHeaders($headers); + + return mail($recipients, $subject, $body, $text_headers); + } + + /** + * Sanitize an array of mail headers by removing any additional header + * strings present in a legitimate header's value. The goal of this + * filter is to prevent mail injection attacks. + * + * @param array $headers The associative array of headers to sanitize. + * + * @access private + */ + function _sanitizeHeaders(&$headers) + { + foreach ($headers as $key => $value) { + $headers[$key] = + preg_replace('=((||0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', + null, $value); + } + + return true; + } + + /** + * Take an array of mail headers and return a string containing + * text usable in sending a message. + * + * @param array $headers The array of headers to prepare, in an associative + * array, where the array key is the header name (ie, + * 'Subject'), and the array value is the header + * value (ie, 'test'). The header produced from those + * values would be 'Subject: test'. + * + * @return mixed Returns false if it encounters a bad address, + * otherwise returns an array containing two + * elements: Any From: address found in the headers, + * and the plain text version of the headers. + * @access private + */ + function prepareHeaders($headers) + { + $lines = array(); + $from = null; + + foreach ($headers as $key => $value) { + if (strcasecmp($key, 'From') === 0) { + include_once 'include/z_RFC822.php'; + $parser = new Mail_RFC822(); + $addresses = $parser->parseAddressList($value, 'localhost', false); + //if (is_a($addresses, 'PEAR_Error')) { + if ($addresses === false) { + return $addresses; + } + + $from = $addresses[0]->mailbox . '@' . $addresses[0]->host; + + // Reject envelope From: addresses with spaces. + if (strstr($from, ' ')) { + return false; + } + + $lines[] = $key . ': ' . $value; + } elseif (strcasecmp($key, 'Received') === 0) { + $received = array(); + if (is_array($value)) { + foreach ($value as $line) { + $received[] = $key . ': ' . $line; + } + } + else { + $received[] = $key . ': ' . $value; + } + // Put Received: headers at the top. Spam detectors often + // flag messages with Received: headers after the Subject: + // as spam. + $lines = array_merge($received, $lines); + } else { + // If $value is an array (i.e., a list of addresses), convert + // it to a comma-delimited string of its elements (addresses). + if (is_array($value)) { + $value = implode(', ', $value); + } + $lines[] = $key . ': ' . $value; + } + } + + return array($from, join($this->sep, $lines)); + } + + /** + * Take a set of recipients and parse them, returning an array of + * bare addresses (forward paths) that can be passed to sendmail + * or an smtp server with the rcpt to: command. + * + * @param mixed Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. + * + * @return mixed An array of forward paths (bare addresses) or a PEAR_Error + * object if the address list could not be parsed. + * @access private + */ + function parseRecipients($recipients) + { + include_once 'include/z_RFC822.php'; + + // if we're passed an array, assume addresses are valid and + // implode them before parsing. + if (is_array($recipients)) { + $recipients = implode(', ', $recipients); + } + + // Parse recipients, leaving out all personal info. This is + // for smtp recipients, etc. All relevant personal information + // should already be in the headers. + $parser = new Mail_RFC822(); + $addresses = $parser->parseAddressList($recipients, 'localhost', false); + + // If parseAddressList() returned a PEAR_Error object, just return it. + //if (is_a($addresses, 'PEAR_Error')) { + if ($addresses === false) { + return $addresses; + } + + $recipients = array(); + if (is_array($addresses)) { + foreach ($addresses as $ob) { + $recipients[] = $ob->mailbox . '@' . $ob->host; + } + } + + return $recipients; + } + + /** + * Z-Push helper for error logging + * removing PEAR dependency + * + * @param string debug message + * @return boolean always false as there was an error + * @access private + */ + static function raiseError($message) { + ZLog::Write(LOGLEVEL_ERROR, "Mail error: ". $message); + return false; + } +} diff --git a/sources/include/Mail/mail.php b/sources/include/Mail/mail.php new file mode 100644 index 0000000..1d56eec --- /dev/null +++ b/sources/include/Mail/mail.php @@ -0,0 +1,193 @@ + + * @copyright 2010 Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: mail.php 294747 2010-02-08 08:18:33Z clockwerx $ + * @link http://pear.php.net/package/Mail/ + */ + +/** + * Z-Push changes + * + * removed PEAR dependency by implementing own raiseError() + * + * Reference implementation used: + * http://download.pear.php.net/package/Mail-1.2.0.tgz + * + * + */ + +/** + * internal PHP-mail() implementation of the PEAR Mail:: interface. + * @package Mail + * @version $Revision: 294747 $ + */ +class Mail_mail extends Mail { + + /** + * Any arguments to pass to the mail() function. + * @var string + */ + var $_params = ''; + + /** + * Constructor. + * + * Instantiates a new Mail_mail:: object based on the parameters + * passed in. + * + * @param array $params Extra arguments for the mail() function. + */ + function Mail_mail($params = null) + { + // The other mail implementations accept parameters as arrays. + // In the interest of being consistent, explode an array into + // a string of parameter arguments. + if (is_array($params)) { + $this->_params = join(' ', $params); + } else { + $this->_params = $params; + } + + /* Because the mail() function may pass headers as command + * line arguments, we can't guarantee the use of the standard + * "\r\n" separator. Instead, we use the system's native line + * separator. */ + if (defined('PHP_EOL')) { + $this->sep = PHP_EOL; + } else { + $this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n"; + } + } + + /** + * Implements Mail_mail::send() function using php's built-in mail() + * command. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * + * @access public + */ + function send($recipients, $headers, $body) + { + if (!is_array($headers)) { + return Mail_mail::raiseError('$headers must be an array'); + } + + $result = $this->_sanitizeHeaders($headers); + //if (is_a($result, 'PEAR_Error')) { + if ($result === false) { + return $result; + } + + // If we're passed an array of recipients, implode it. + if (is_array($recipients)) { + $recipients = implode(', ', $recipients); + } + + // Get the Subject out of the headers array so that we can + // pass it as a seperate argument to mail(). + $subject = ''; + if (isset($headers['Subject'])) { + $subject = $headers['Subject']; + unset($headers['Subject']); + } + + // Also remove the To: header. The mail() function will add its own + // To: header based on the contents of $recipients. + unset($headers['To']); + + // Flatten the headers out. + $headerElements = $this->prepareHeaders($headers); + //if (is_a($headerElements, 'PEAR_Error')) { + if ($headerElements === false) { + return $headerElements; + } + list(, $text_headers) = $headerElements; + + // We only use mail()'s optional fifth parameter if the additional + // parameters have been provided and we're not running in safe mode. + if (empty($this->_params) || ini_get('safe_mode')) { + $result = mail($recipients, $subject, $body, $text_headers); + } else { + $result = mail($recipients, $subject, $body, $text_headers, + $this->_params); + } + + // If the mail() function returned failure, we need to create a + // PEAR_Error object and return it instead of the boolean result. + if ($result === false) { + $result = Mail_mail::raiseError('mail() returned failure'); + } + + return $result; + } + + /** + * Z-Push helper for error logging + * removing PEAR dependency + * + * @param string debug message + * @return boolean always false as there was an error + * @access private + */ + static function raiseError($message) { + ZLog::Write(LOGLEVEL_ERROR, "Mail error: ". $message); + return false; + } +} diff --git a/sources/include/Mail/sendmail.php b/sources/include/Mail/sendmail.php new file mode 100644 index 0000000..984c009 --- /dev/null +++ b/sources/include/Mail/sendmail.php @@ -0,0 +1,197 @@ + | +// +----------------------------------------------------------------------+ + +/** + * Z-Push changes + * + * removed PEAR dependency by implementing own raiseError() + * + * Reference implementation used: + * http://download.pear.php.net/package/Mail-1.2.0.tgz + * + * + */ + +/** + * Sendmail implementation of the PEAR Mail:: interface. + * @access public + * @package Mail + * @version $Revision: 294744 $ + */ +class Mail_sendmail extends Mail { + + /** + * The location of the sendmail or sendmail wrapper binary on the + * filesystem. + * @var string + */ + var $sendmail_path = '/usr/sbin/sendmail'; + + /** + * Any extra command-line parameters to pass to the sendmail or + * sendmail wrapper binary. + * @var string + */ + var $sendmail_args = '-i'; + + /** + * Constructor. + * + * Instantiates a new Mail_sendmail:: object based on the parameters + * passed in. It looks for the following parameters: + * sendmail_path The location of the sendmail binary on the + * filesystem. Defaults to '/usr/sbin/sendmail'. + * + * sendmail_args Any extra parameters to pass to the sendmail + * or sendmail wrapper binary. + * + * If a parameter is present in the $params array, it replaces the + * default. + * + * @param array $params Hash containing any parameters different from the + * defaults. + * @access public + */ + function Mail_sendmail($params) + { + if (isset($params['sendmail_path'])) { + $this->sendmail_path = $params['sendmail_path']; + } + if (isset($params['sendmail_args'])) { + $this->sendmail_args = $params['sendmail_args']; + } + + /* + * Because we need to pass message headers to the sendmail program on + * the commandline, we can't guarantee the use of the standard "\r\n" + * separator. Instead, we use the system's native line separator. + */ + if (defined('PHP_EOL')) { + $this->sep = PHP_EOL; + } else { + $this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n"; + } + } + + /** + * Implements Mail::send() function using the sendmail + * command-line binary. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + */ + function send($recipients, $headers, $body) + { + if (!is_array($headers)) { + return Mail_sendmail::raiseError('$headers must be an array'); + } + + $result = $this->_sanitizeHeaders($headers); + //if (is_a($result, 'PEAR_Error')) { + if ($result === false) { + return $result; + } + + $recipients = $this->parseRecipients($recipients); + //if (is_a($recipients, 'PEAR_Error')) { + if ($recipients === false) { + return $recipients; + } + $recipients = implode(' ', array_map('escapeshellarg', $recipients)); + + $headerElements = $this->prepareHeaders($headers); + //if (is_a($headerElements, 'PEAR_Error')) { + if ($headerElements === false) { + return $headerElements; + } + list($from, $text_headers) = $headerElements; + + /* Since few MTAs are going to allow this header to be forged + * unless it's in the MAIL FROM: exchange, we'll use + * Return-Path instead of From: if it's set. */ + if (!empty($headers['Return-Path'])) { + $from = $headers['Return-Path']; + } + + if (!isset($from)) { + return Mail_sendmail::raiseError('No from address given.'); + } elseif (strpos($from, ' ') !== false || + strpos($from, ';') !== false || + strpos($from, '&') !== false || + strpos($from, '`') !== false) { + return Mail_sendmail::raiseError('From address specified with dangerous characters.'); + } + + $from = escapeshellarg($from); // Security bug #16200 + + $mail = @popen($this->sendmail_path . (!empty($this->sendmail_args) ? ' ' . $this->sendmail_args : '') . " -f$from -- $recipients", 'w'); + if (!$mail) { + return Mail_sendmail::raiseError('Failed to open sendmail [' . $this->sendmail_path . '] for execution.'); + } + + // Write the headers following by two newlines: one to end the headers + // section and a second to separate the headers block from the body. + fputs($mail, $text_headers . $this->sep . $this->sep); + + fputs($mail, $body); + $result = pclose($mail); + if (version_compare(phpversion(), '4.2.3') == -1) { + // With older php versions, we need to shift the pclose + // result to get the exit code. + $result = $result >> 8 & 0xFF; + } + + if ($result != 0) { + return Mail_sendmail::raiseError('sendmail returned error code ' . $result, + $result); + } + + return true; + } + + /** + * Z-Push helper for error logging + * removing PEAR dependency + * + * @param string debug message + * @return boolean always false as there was an error + * @access private + */ + static function raiseError($message) { + ZLog::Write(LOGLEVEL_ERROR, "Mail error: ". $message); + return false; + } +} diff --git a/sources/include/Mail/smtp.php b/sources/include/Mail/smtp.php new file mode 100644 index 0000000..8388aec --- /dev/null +++ b/sources/include/Mail/smtp.php @@ -0,0 +1,490 @@ + + * @author Chuck Hagenbuch + * @copyright 2010 Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: smtp.php 307488 2011-01-14 19:00:54Z alec $ + * @link http://pear.php.net/package/Mail/ + */ + +/** + * Z-Push changes + * + * removed PEAR dependency by implementing own raiseError() + * + * Reference implementation used: + * http://download.pear.php.net/package/Mail-1.2.0.tgz + * + * + */ + +/** Error: Failed to create a Net_SMTP object */ +define('PEAR_MAIL_SMTP_ERROR_CREATE', 10000); + +/** Error: Failed to connect to SMTP server */ +define('PEAR_MAIL_SMTP_ERROR_CONNECT', 10001); + +/** Error: SMTP authentication failure */ +define('PEAR_MAIL_SMTP_ERROR_AUTH', 10002); + +/** Error: No From: address has been provided */ +define('PEAR_MAIL_SMTP_ERROR_FROM', 10003); + +/** Error: Failed to set sender */ +define('PEAR_MAIL_SMTP_ERROR_SENDER', 10004); + +/** Error: Failed to add recipient */ +define('PEAR_MAIL_SMTP_ERROR_RECIPIENT', 10005); + +/** Error: Failed to send data */ +define('PEAR_MAIL_SMTP_ERROR_DATA', 10006); + +/** + * SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class. + * @access public + * @package Mail + * @version $Revision: 307488 $ + */ +class Mail_smtp extends Mail { + + /** + * SMTP connection object. + * + * @var object + * @access private + */ + var $_smtp = null; + + /** + * The list of service extension parameters to pass to the Net_SMTP + * mailFrom() command. + * @var array + */ + var $_extparams = array(); + + /** + * The SMTP host to connect to. + * @var string + */ + var $host = 'localhost'; + + /** + * The port the SMTP server is on. + * @var integer + */ + var $port = 25; + + /** + * Should SMTP authentication be used? + * + * This value may be set to true, false or the name of a specific + * authentication method. + * + * If the value is set to true, the Net_SMTP package will attempt to use + * the best authentication method advertised by the remote SMTP server. + * + * @var mixed + */ + var $auth = false; + + /** + * The username to use if the SMTP server requires authentication. + * @var string + */ + var $username = ''; + + /** + * The password to use if the SMTP server requires authentication. + * @var string + */ + var $password = ''; + + /** + * Hostname or domain that will be sent to the remote SMTP server in the + * HELO / EHLO message. + * + * @var string + */ + var $localhost = 'localhost'; + + /** + * SMTP connection timeout value. NULL indicates no timeout. + * + * @var integer + */ + var $timeout = null; + + /** + * Turn on Net_SMTP debugging? + * + * @var boolean $debug + */ + var $debug = false; + + /** + * Indicates whether or not the SMTP connection should persist over + * multiple calls to the send() method. + * + * @var boolean + */ + var $persist = false; + + /** + * Use SMTP command pipelining (specified in RFC 2920) if the SMTP server + * supports it. This speeds up delivery over high-latency connections. By + * default, use the default value supplied by Net_SMTP. + * @var bool + */ + var $pipelining; + + /** + * Constructor. + * + * Instantiates a new Mail_smtp:: object based on the parameters + * passed in. It looks for the following parameters: + * host The server to connect to. Defaults to localhost. + * port The port to connect to. Defaults to 25. + * auth SMTP authentication. Defaults to none. + * username The username to use for SMTP auth. No default. + * password The password to use for SMTP auth. No default. + * localhost The local hostname / domain. Defaults to localhost. + * timeout The SMTP connection timeout. Defaults to none. + * verp Whether to use VERP or not. Defaults to false. + * DEPRECATED as of 1.2.0 (use setMailParams()). + * debug Activate SMTP debug mode? Defaults to false. + * persist Should the SMTP connection persist? + * pipelining Use SMTP command pipelining + * + * If a parameter is present in the $params array, it replaces the + * default. + * + * @param array Hash containing any parameters different from the + * defaults. + * @access public + */ + function Mail_smtp($params) + { + if (isset($params['host'])) $this->host = $params['host']; + if (isset($params['port'])) $this->port = $params['port']; + if (isset($params['auth'])) $this->auth = $params['auth']; + if (isset($params['username'])) $this->username = $params['username']; + if (isset($params['password'])) $this->password = $params['password']; + if (isset($params['localhost'])) $this->localhost = $params['localhost']; + if (isset($params['timeout'])) $this->timeout = $params['timeout']; + if (isset($params['debug'])) $this->debug = (bool)$params['debug']; + if (isset($params['persist'])) $this->persist = (bool)$params['persist']; + if (isset($params['pipelining'])) $this->pipelining = (bool)$params['pipelining']; + + // Deprecated options + if (isset($params['verp'])) { + $this->addServiceExtensionParameter('XVERP', is_bool($params['verp']) ? null : $params['verp']); + } + + register_shutdown_function(array(&$this, '_Mail_smtp')); + } + + /** + * Destructor implementation to ensure that we disconnect from any + * potentially-alive persistent SMTP connections. + */ + function _Mail_smtp() + { + $this->disconnect(); + } + + /** + * Implements Mail::send() function using SMTP. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (e.g., 'Subject'), and the array value + * is the header value (e.g., 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * MIME parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + */ + function send($recipients, $headers, $body) + { + /* If we don't already have an SMTP object, create one. */ + $result = &$this->getSMTPObject(); + //if (PEAR::isError($result)) { + if ($result === false) { + return $result; + } + + if (!is_array($headers)) { + return Mail_smtp::raiseError('$headers must be an array'); + } + + $this->_sanitizeHeaders($headers); + + $headerElements = $this->prepareHeaders($headers); + //if (is_a($headerElements, 'PEAR_Error')) { + if ($headerElements === false) { + $this->_smtp->rset(); + return $headerElements; + } + list($from, $textHeaders) = $headerElements; + + /* Since few MTAs are going to allow this header to be forged + * unless it's in the MAIL FROM: exchange, we'll use + * Return-Path instead of From: if it's set. */ + if (!empty($headers['Return-Path'])) { + $from = $headers['Return-Path']; + } + + if (!isset($from)) { + $this->_smtp->rset(); + return Mail_smtp::raiseError('No From: address has been provided', + PEAR_MAIL_SMTP_ERROR_FROM); + } + + $params = null; + if (!empty($this->_extparams)) { + foreach ($this->_extparams as $key => $val) { + $params .= ' ' . $key . (is_null($val) ? '' : '=' . $val); + } + } + //if (PEAR::isError($res = $this->_smtp->mailFrom($from, ltrim($params)))) { + if (($res = $this->_smtp->mailFrom($from, ltrim($params))) === false) { + $error = $this->_error("Failed to set sender: $from", $res); + $this->_smtp->rset(); + return Mail_smtp::raiseError($error, PEAR_MAIL_SMTP_ERROR_SENDER); + } + + $recipients = $this->parseRecipients($recipients); + //if (is_a($recipients, 'PEAR_Error')) { + if ($recipients === false) { + $this->_smtp->rset(); + return $recipients; + } + + // FIX: Cc and Bcc headers are sent, but we need to make sure that the recipient list contains them + foreach (array("CC", "cc", "Cc", "BCC", "Bcc", "bcc") as $key) { + if (!empty($headers[$key])) { + $extra_recipients = $this->parseRecipients($headers[$key]); + if ($extra_recipients === false) { + $this->_smtp->rset(); + return $extra_recipients; + } + $recipients = array_merge($recipients, $extra_recipients); + } + } + + // Remove repeated rcptTo + $recipients = array_unique($recipients); + + + foreach ($recipients as $recipient) { + $res = $this->_smtp->rcptTo($recipient); + //if (is_a($res, 'PEAR_Error')) { + if ($res === false) { + $error = $this->_error("Failed to add recipient: $recipient", $res); + $this->_smtp->rset(); + return Mail_smtp::raiseError($error, PEAR_MAIL_SMTP_ERROR_RECIPIENT); + } + } + + /* Send the message's headers and the body as SMTP data. */ + $res = $this->_smtp->data($body, $textHeaders); + list(,$args) = $this->_smtp->getResponse(); + + if (preg_match("/Ok: queued as (.*)/", $args, $queued)) { + $this->queued_as = $queued[1]; + } + + /* we need the greeting; from it we can extract the authorative name of the mail server we've really connected to. + * ideal if we're connecting to a round-robin of relay servers and need to track which exact one took the email */ + $this->greeting = $this->_smtp->getGreeting(); + + //if (is_a($res, 'PEAR_Error')) { + if ($res === false) { + $error = $this->_error('Failed to send data', $res); + $this->_smtp->rset(); + return Mail_smtp::raiseError($error, PEAR_MAIL_SMTP_ERROR_DATA); + } + + /* If persistent connections are disabled, destroy our SMTP object. */ + if ($this->persist === false) { + $this->disconnect(); + } + + return true; + } + + /** + * Connect to the SMTP server by instantiating a Net_SMTP object. + * + * @return mixed Returns a reference to the Net_SMTP object on success, or + * a PEAR_Error containing a descriptive error message on + * failure. + * + * @since 1.2.0 + * @access public + */ + function &getSMTPObject() + { + if (is_object($this->_smtp) !== false) { + return $this->_smtp; + } + + include_once 'include/Net/SMTP.php'; + $this->_smtp = &new Net_SMTP($this->host, + $this->port, + $this->localhost, + $this->pipelining); + + /* If we still don't have an SMTP object at this point, fail. */ + if (is_object($this->_smtp) === false) { + return Mail_smtp::raiseError('Failed to create a Net_SMTP object', + PEAR_MAIL_SMTP_ERROR_CREATE); + } + + /* Configure the SMTP connection. */ + if ($this->debug) { + $this->_smtp->setDebug(true); + } + + /* Attempt to connect to the configured SMTP server. */ + //if (PEAR::isError($res = $this->_smtp->connect($this->timeout))) { + if (($res = $this->_smtp->connect($this->timeout)) === false) { + $error = $this->_error('Failed to connect to ' . + $this->host . ':' . $this->port, + $res); + return Mail_smtp::raiseError($error, PEAR_MAIL_SMTP_ERROR_CONNECT); + } + + /* Attempt to authenticate if authentication has been enabled. */ + if ($this->auth) { + $method = is_string($this->auth) ? $this->auth : ''; + + //if (PEAR::isError($res = $this->_smtp->auth($this->username, $this->password, $method))) { + if (($res = $this->_smtp->auth($this->username, $this->password, $method)) === false) { + $error = $this->_error("$method authentication failure", + $res); + $this->_smtp->rset(); + return Mail_smtp::raiseError($error, PEAR_MAIL_SMTP_ERROR_AUTH); + } + } + + return $this->_smtp; + } + + /** + * Add parameter associated with a SMTP service extension. + * + * @param string Extension keyword. + * @param string Any value the keyword needs. + * + * @since 1.2.0 + * @access public + */ + function addServiceExtensionParameter($keyword, $value = null) + { + $this->_extparams[$keyword] = $value; + } + + /** + * Disconnect and destroy the current SMTP connection. + * + * @return boolean True if the SMTP connection no longer exists. + * + * @since 1.1.9 + * @access public + */ + function disconnect() + { + /* If we have an SMTP object, disconnect and destroy it. */ + if (is_object($this->_smtp) && $this->_smtp->disconnect()) { + $this->_smtp = null; + } + + /* We are disconnected if we no longer have an SMTP object. */ + return ($this->_smtp === null); + } + + /** + * Build a standardized string describing the current SMTP error. + * + * @param string $text Custom string describing the error context. + * @param object $error Reference to the current PEAR_Error object. + * + * @return string A string describing the current SMTP error. + * + * @since 1.1.7 + * @access private + */ + function _error($text, &$error) + { + /* Split the SMTP response into a code and a response string. */ + list($code, $response) = $this->_smtp->getResponse(); + + /* Build our standardized error string. */ + return $text + . ' [SMTP: ' . $error->getMessage() + . " (code: $code, response: $response)]"; + } + + /** + * Z-Push helper for error logging + * removing PEAR dependency + * + * @param string debug message + * @return boolean always false as there was an error + * @access private + */ + static function raiseError($message) { + ZLog::Write(LOGLEVEL_ERROR, "Mail error: ". $message); + return false; + } +} diff --git a/sources/include/Net/SMTP.php b/sources/include/Net/SMTP.php new file mode 100644 index 0000000..2f65078 --- /dev/null +++ b/sources/include/Net/SMTP.php @@ -0,0 +1,1254 @@ + | +// | Jon Parise | +// | Damian Alejandro Fernandez Sosa | +// +----------------------------------------------------------------------+ + + +/** + * Z-Push changes + * + * removed PEAR dependency by implementing own raiseError() + * + * Reference implementation used: + * http://download.pear.php.net/package/Net_SMTP-1.6.2.tgz + * https://github.com/pear/Net_SMTP Commit 558b92f5c2ecbb857094a3926a100e51211a08c2 2014/03/09 + * + * + */ + +//require_once 'PEAR.php'; +//require_once 'PEAR/Exception.php'; +require_once 'include/Net/Socket.php'; + +/** + * Provides an implementation of the SMTP protocol using PEAR's + * Net_Socket:: class. + * + * @package Net_SMTP + * @author Chuck Hagenbuch + * @author Jon Parise + * @author Damian Alejandro Fernandez Sosa + * + * @example basic.php A basic implementation of the Net_SMTP package. + */ +class Net_SMTP +{ + /** + * The server to connect to. + * + * @var string + */ + public $host = 'localhost'; + + /** + * The port to connect to. + * + * @var int + */ + public $port = 25; + + /** + * The value to give when sending EHLO or HELO. + * + * @var string + */ + public $localhost = 'localhost'; + + /** + * List of supported authentication methods, in preferential order. + * + * @var array + */ + public $auth_methods = array(); + + /** + * Use SMTP command pipelining (specified in RFC 2920) if the SMTP + * server supports it. + * + * When pipeling is enabled, rcptTo(), mailFrom(), sendFrom(), + * somlFrom() and samlFrom() do not wait for a response from the + * SMTP server but return immediately. + * + * @var bool + */ + public $pipelining = false; + + /** + * Number of pipelined commands. + * + * @var int + */ + protected $_pipelined_commands = 0; + + /** + * Should debugging output be enabled? + * + * @var boolean + */ + protected $_debug = false; + + /** + * Debug output handler. + * + * @var callback + */ + protected $_debug_handler = null; + + /** + * The socket resource being used to connect to the SMTP server. + * + * @var resource + */ + protected $_socket = null; + + /** + * Array of socket options that will be passed to Net_Socket::connect(). + * + * @see stream_context_create() + * + * @var array + */ + protected $_socket_options = null; + + /** + * The socket I/O timeout value in seconds. + * + * @var int + */ + protected $_timeout = 0; + + /** + * The most recent server response code. + * + * @var int + */ + protected $_code = -1; + + /** + * The most recent server response arguments. + * + * @var array + */ + protected $_arguments = array(); + + /** + * Stores the SMTP server's greeting string. + * + * @var string + */ + protected $_greeting = null; + + /** + * Stores detected features of the SMTP server. + * + * @var array + */ + protected $_esmtp = array(); + + /** + * Instantiates a new Net_SMTP object, overriding any defaults + * with parameters that are passed in. + * + * If you have SSL support in PHP, you can connect to a server + * over SSL using an 'ssl://' prefix: + * + * // 465 is a common smtps port. + * $smtp = new Net_SMTP('ssl://mail.host.com', 465); + * $smtp->connect(); + * + * @param string $host The server to connect to. + * @param integer $port The port to connect to. + * @param string $localhost The value to give when sending EHLO or HELO. + * @param boolean $pipeling Use SMTP command pipelining + * @param integer $timeout Socket I/O timeout in seconds. + * @param array $socket_options Socket stream_context_create() options. + */ + public function __construct($host = null, $port = null, $localhost = null, + $pipelining = false, $timeout = 0, + $socket_options = null) + { + if (isset($host)) { + $this->host = $host; + } + if (isset($port)) { + $this->port = $port; + } + if (isset($localhost)) { + $this->localhost = $localhost; + } + $this->pipelining = $pipelining; + + $this->_socket = new Net_Socket(); + $this->_socket_options = $socket_options; + $this->_timeout = $timeout; + + /* Include the Auth_SASL package. If the package is available, we + * enable the authentication methods that depend upon it. */ + if (@include_once 'include/Auth/SASL.php') { + $this->setAuthMethod('CRAM-MD5', array($this, '_authCram_MD5')); + $this->setAuthMethod('DIGEST-MD5', array($this, '_authDigest_MD5')); + } + + /* These standard authentication methods are always available. */ + $this->setAuthMethod('LOGIN', array($this, '_authLogin'), false); + $this->setAuthMethod('PLAIN', array($this, '_authPlain'), false); + } + + /** + * Set the socket I/O timeout value in seconds plus microseconds. + * + * @param integer $seconds Timeout value in seconds. + * @param integer $microseconds Additional value in microseconds. + */ + public function setTimeout($seconds, $microseconds = 0) + { + return $this->_socket->setTimeout($seconds, $microseconds); + } + + /** + * Set the value of the debugging flag. + * + * @param boolean $debug New value for the debugging flag. + */ + public function setDebug($debug, $handler = null) + { + $this->_debug = $debug; + $this->_debug_handler = $handler; + } + + /** + * Write the given debug text to the current debug output handler. + * + * @param string $message Debug message text. + */ + protected function _debug($message) + { + if ($this->_debug) { + if ($this->_debug_handler) { + call_user_func_array($this->_debug_handler, + array(&$this, $message)); + } else { + ZLog::Write(LOGLEVEL_DEBUG, "Net_SMTP DEBUG: ". $message); + } + } + } + + /** + * Send the given string of data to the server. + * + * @param string $data The string of data to send. + * + * @return integer The number of bytes that were actually written. + * @throws PEAR_Exception + */ + protected function _send($data) + { + $this->_debug("Send: $data"); + + $result = $this->_socket->write($data); + if ($result === false) { + return Net_SMTP::raiseError('Failed to write to socket: ' . $result->getMessage(), + $result); + } + + return $result; + } + + /** + * Send a command to the server with an optional string of + * arguments. A carriage return / linefeed (CRLF) sequence will + * be appended to each command string before it is sent to the + * SMTP server - an error will be thrown if the command string + * already contains any newline characters. Use _send() for + * commands that must contain newlines. + * + * @param string $command The SMTP command to send to the server. + * @param string $args A string of optional arguments to append + * to the command. + * + * @return integer The number of bytes that were actually written. + * @throws PEAR_Exception + */ + protected function _put($command, $args = '') + { + if (!empty($args)) { + $command .= ' ' . $args; + } + + if (strcspn($command, "\r\n") !== strlen($command)) { + return Net_SMTP::raiseError('Commands cannot contain newlines'); + } + + return $this->_send($command . "\r\n"); + } + + /** + * Read a reply from the SMTP server. The reply consists of a response + * code and a response message. + * + * @see getResponse + * + * @param mixed $valid The set of valid response codes. These + * may be specified as an array of integer + * values or as a single integer value. + * @param bool $later Do not parse the response now, but wait + * until the last command in the pipelined + * command group + * + * @throws PEAR_Exception + */ + protected function _parseResponse($valid, $later = false) + { + $this->_code = -1; + $this->_arguments = array(); + + if ($later) { + ++$this->_pipelined_commands; + return; + } + + for ($i = 0; $i <= $this->_pipelined_commands; ++$i) { + while ($line = $this->_socket->readLine()) { + $this->_debug("Recv: $line"); + + /* If we receive an empty line, the connection was closed. */ + if (empty($line)) { + $this->disconnect(); + return Net_SMTP::raiseError('Connection was closed', + null, PEAR_ERROR_RETURN); + } + + /* Read the code and store the rest in the arguments array. */ + $code = substr($line, 0, 3); + $this->_arguments[] = trim(substr($line, 4)); + + /* Check the syntax of the response code. */ + if (is_numeric($code)) { + $this->_code = (int)$code; + } else { + $this->_code = -1; + break; + } + + /* If this is not a multiline response, we're done. */ + if (substr($line, 3, 1) != '-') { + break; + } + } + } + + $this->_pipelined_commands = 0; + + /* Compare the server's response code with the valid code/codes. */ + if ((is_int($valid) && ($this->_code === $valid)) || + (is_array($valid) && in_array($this->_code, $valid, true))) { + return; + } + + return Net_SMTP::raiseError('Invalid response code received from server', + $this->_code, PEAR_ERROR_RETURN); + } + + /** + * Issue an SMTP command and verify its response. + * + * @param string $command The SMTP command string or data. + * @param mixed $valid The set of valid response codes. These + * may be specified as an array of integer + * values or as a single integer value. + * + * @throws PEAR_Exception + */ + public function command($command, $valid) + { + //if (PEAR::isError($error = $this->_put($command))) { + if (($error = $this->_put($command)) === false) { + return $error; + } + //if (PEAR::isError($error = $this->_parseResponse($valid))) { + if (($error = $this->_parseResponse($valid)) === false) { + return $error; + } + + return true; + } + + /** + * Return a 2-tuple containing the last response from the SMTP server. + * + * @return array A two-element array: the first element contains the + * response code as an integer and the second element + * contains the response's arguments as a string. + */ + public function getResponse() + { + return array($this->_code, join("\n", $this->_arguments)); + } + + /** + * Return the SMTP server's greeting string. + * + * @return string A string containing the greeting string, or null if a + * greeting has not been received. + */ + public function getGreeting() + { + return $this->_greeting; + } + + /** + * Attempt to connect to the SMTP server. + * + * @param int $timeout The timeout value (in seconds) for the + * socket connection attempt. + * @param bool $persistent Should a persistent socket connection + * be used? + * + * @throws PEAR_Exception + */ + public function connect($timeout = null, $persistent = false) + { + $this->_greeting = null; + $result = $this->_socket->connect($this->host, $this->port, + $persistent, $timeout, + $this->_socket_options); + //if (PEAR::isError($result)) { + if ($result === false) { + return Net_SMTP::raiseError('Failed to connect socket: ' . + $result->getMessage()); + } + + /* + * Now that we're connected, reset the socket's timeout value for + * future I/O operations. This allows us to have different socket + * timeout values for the initial connection (our $timeout parameter) + * and all other socket operations. + */ + if ($this->_timeout > 0) { + //if (PEAR::isError($error = $this->setTimeout($this->_timeout))) { + if (($error = $this->setTimeout($this->_timeout)) === false) { + return $error; + } + } + + //if (PEAR::isError($error = $this->_parseResponse(220))) { + if (($error = $this->_parseResponse(220)) === false) { + return $error; + } + + /* Extract and store a copy of the server's greeting string. */ + list(, $this->_greeting) = $this->getResponse(); + + //if (PEAR::isError($error = $this->_negotiate())) { + if (($error = $this->_negotiate()) === false) { + return $error; + } + + return true; + } + + /** + * Attempt to disconnect from the SMTP server. + * + * @throws PEAR_Exception + */ + public function disconnect() + { + //if (PEAR::isError($error = $this->_put('QUIT'))) { + if (($error = $this->_put('QUIT')) === false) { + return $error; + } + //if (PEAR::isError($error = $this->_parseResponse(221))) { + if (($error = $this->_parseResponse(221)) === false) { + return $error; + } + //if (PEAR::isError($error = $this->_socket->disconnect())) { + if (($error = $this->_socket->disconnect()) === false) { + return Net_SMTP::raiseError('Failed to disconnect socket: ' . + $error->getMessage()); + } + + return true; + } + + /** + * Attempt to send the EHLO command and obtain a list of ESMTP + * extensions available, and failing that just send HELO. + * + * @throws PEAR_Exception + */ + protected function _negotiate() + { + //if (PEAR::isError($error = $this->_put('EHLO', $this->localhost))) { + if (($error = $this->_put('EHLO', $this->localhost)) === false) { + return $error; + } + + //if (PEAR::isError($this->_parseResponse(250))) { + if (($this->_parseResponse(250)) === false) { + /* If the EHLO failed, try the simpler HELO command. */ + //if (PEAR::isError($error = $this->_put('HELO', $this->localhost))) { + if (($error = $this->_put('HELO', $this->localhost)) === false) { + return $error; + } + //if (PEAR::isError($this->_parseResponse(250))) { + if (($this->_parseResponse(250)) === false) { + return Net_SMTP::raiseError('HELO was not accepted: ', $this->_code, + PEAR_ERROR_RETURN); + } + + return true; + } + + foreach ($this->_arguments as $argument) { + $verb = strtok($argument, ' '); + $arguments = substr($argument, strlen($verb) + 1, + strlen($argument) - strlen($verb) - 1); + $this->_esmtp[$verb] = $arguments; + } + + if (!isset($this->_esmtp['PIPELINING'])) { + $this->pipelining = false; + } + + return true; + } + + /** + * Returns the name of the best authentication method that the server + * has advertised. + * + * @return mixed Returns a string containing the name of the best + * supported authentication method. + * @throws PEAR_Exception + */ + protected function _getBestAuthMethod() + { + $available_methods = explode(' ', $this->_esmtp['AUTH']); + + foreach ($this->auth_methods as $method => $callback) { + if (in_array($method, $available_methods)) { + return $method; + } + } + + return Net_SMTP::raiseError('No supported authentication methods', + null, PEAR_ERROR_RETURN); + } + + /** + * Attempt to do SMTP authentication. + * + * @param string $uid The userid to authenticate as. + * @param string $pwd The password to authenticate with. + * @param string $method The requested authentication method. If none is + * specified, the best supported method will be + * used. + * @param bool $tls Flag indicating whether or not TLS should be + * attempted. + * @param string $authz An optional authorization identifier. If + * specified, this identifier will be used as the + * authorization proxy. + * + * @throws PEAR_Exception + */ + public function auth($uid, $pwd, $method = '', $tls = true, $authz = '') + { + /* We can only attempt a TLS connection if one has been requested, + * we're running PHP 5.1.0 or later, have access to the OpenSSL + * extension, are connected to an SMTP server which supports the + * STARTTLS extension, and aren't already connected over a secure + * (SSL) socket connection. */ + if ($tls && version_compare(PHP_VERSION, '5.1.0', '>=') && + extension_loaded('openssl') && isset($this->_esmtp['STARTTLS']) && + strncasecmp($this->host, 'ssl://', 6) !== 0) { + /* Start the TLS connection attempt. */ + //if (PEAR::isError($result = $this->_put('STARTTLS'))) { + if (($result = $this->_put('STARTTLS')) === false) { + return $result; + } + //if (PEAR::isError($result = $this->_parseResponse(220))) { + if (($result = $this->_parseResponse(220)) === false) { + return $result; + } + //if (PEAR::isError($result = $this->_socket->enableCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT))) { + if (($result = $this->_socket->enableCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) === false) { + return $result; + } elseif ($result !== true) { + return Net_SMTP::raiseError('STARTTLS failed'); + } + + /* Send EHLO again to recieve the AUTH string from the + * SMTP server. */ + $this->_negotiate(); + } + + if (empty($this->_esmtp['AUTH'])) { + return Net_SMTP::raiseError('SMTP server does not support authentication'); + } + + /* If no method has been specified, get the name of the best + * supported method advertised by the SMTP server. */ + if (empty($method)) { + //if (PEAR::isError($method = $this->_getBestAuthMethod())) { + if (($method = $this->_getBestAuthMethod()) === false) { + return $method; + } + } else { + $method = strtoupper($method); + } + + if (!array_key_exists($method, $this->auth_methods)) { + return Net_SMTP::raiseError("$method is not a supported authentication method"); + } + + if (!is_callable($this->auth_methods[$method], false)) { + return Net_SMTP::raiseError("$method authentication method cannot be called"); + } + + if (is_array($this->auth_methods[$method])) { + list($object, $method) = $this->auth_methods[$method]; + $result = $object->{$method}($uid, $pwd, $authz, $this); + } else { + $func = $this->auth_methods[$method]; + $result = $func($uid, $pwd, $authz, $this); + } + + /* If an error was encountered, return the PEAR_Error object. */ + //if (PEAR::isError($result)) { + if ($result === false) { + return $result; + } + + return true; + } + + /** + * Add a new authentication method. + * + * @param string $name The authentication method name (e.g. 'PLAIN') + * @param mixed $callback The authentication callback (given as the name + * of a function or as an (object, method name) + * array). + * @param bool $prepend Should the new method be prepended to the list + * of available methods? This is the default + * behavior, giving the new method the highest + * priority. + * + * @throws PEAR_Exception + */ + public function setAuthMethod($name, $callback, $prepend = true) + { + if (!is_string($name)) { + return Net_SMTP::raiseError('Method name is not a string'); + } + + if (!is_string($callback) && !is_array($callback)) { + return Net_SMTP::raiseError('Method callback must be string or array'); + } + + if (is_array($callback) && + (!is_object($callback[0]) || !is_string($callback[1]))) { + return Net_SMTP::raiseError('Bad mMethod callback array'); + } + + if ($prepend) { + $this->auth_methods = array_merge(array($name => $callback), + $this->auth_methods); + } else { + $this->auth_methods[$name] = $callback; + } + + return true; + } + + /** + * Authenticates the user using the DIGEST-MD5 method. + * + * @param string $uid The userid to authenticate as. + * @param string $pwd The password to authenticate with. + * @param string $authz The optional authorization proxy identifier. + * + * @throws PEAR_Exception + */ + protected function _authDigest_MD5($uid, $pwd, $authz = '') + { + //if (PEAR::isError($error = $this->_put('AUTH', 'DIGEST-MD5'))) { + if (($error = $this->_put('AUTH', 'DIGEST-MD5')) === false) { + return $error; + } + + /* 334: Continue authentication request */ + //if (PEAR::isError($error = $this->_parseResponse(334))) { + if (($error = $this->_parseResponse(334)) === false) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $challenge = base64_decode($this->_arguments[0]); + $digest = Auth_SASL::factory('digest-md5'); + $auth_str = base64_encode($digest->getResponse($uid, $pwd, $challenge, + $this->host, "smtp", + $authz)); + + //if (PEAR::isError($error = $this->_put($auth_str))) { + if (($error = $this->_put($auth_str)) === false) { + return $error; + } + + /* 334: Continue authentication request */ + //if (PEAR::isError($error = $this->_parseResponse(334))) { + if (($error = $this->_parseResponse(334)) === false) { + return $error; + } + + /* We don't use the protocol's third step because SMTP doesn't + * allow subsequent authentication, so we just silently ignore + * it. */ + //if (PEAR::isError($error = $this->_put(''))) { + if (($error = $this->_put('')) === false) { + return $error; + } + + /* 235: Authentication successful */ + //if (PEAR::isError($error = $this->_parseResponse(235))) { + if (($error = $this->_parseResponse(235)) === false) { + return $error; + } + + return true; + } + + /** + * Authenticates the user using the CRAM-MD5 method. + * + * @param string $uid The userid to authenticate as. + * @param string $pwd The password to authenticate with. + * @param string $authz The optional authorization proxy identifier. + * + * @throws PEAR_Exception + */ + protected function _authCRAM_MD5($uid, $pwd, $authz = '') + { + //if (PEAR::isError($error = $this->_put('AUTH', 'CRAM-MD5'))) { + if (($error = $this->_put('AUTH', 'CRAM-MD5')) === false) { + return $error; + } + + /* 334: Continue authentication request */ + //if (PEAR::isError($error = $this->_parseResponse(334))) { + if (($error = $this->_parseResponse(334)) === false) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $challenge = base64_decode($this->_arguments[0]); + $cram = Auth_SASL::factory('cram-md5'); + $auth_str = base64_encode($cram->getResponse($uid, $pwd, $challenge)); + + //if (PEAR::isError($error = $this->_put($auth_str))) { + if (($error = $this->_put($auth_str)) === false) { + return $error; + } + + /* 235: Authentication successful */ + //if (PEAR::isError($error = $this->_parseResponse(235))) { + if (($error = $this->_parseResponse(235)) === false) { + return $error; + } + + return true; + } + + /** + * Authenticates the user using the LOGIN method. + * + * @param string $uid The userid to authenticate as. + * @param string $pwd The password to authenticate with. + * @param string $authz The optional authorization proxy identifier. + * + * @throws PEAR_Exception + */ + protected function _authLogin($uid, $pwd, $authz = '') + { + //if (PEAR::isError($error = $this->_put('AUTH', 'LOGIN'))) { + if (($error = $this->_put('AUTH', 'LOGIN')) === false) { + return $error; + } + + /* 334: Continue authentication request */ + //if (PEAR::isError($error = $this->_parseResponse(334))) { + if (($error = $this->_parseResponse(334)) === false) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + //if (PEAR::isError($error = $this->_put(base64_encode($uid)))) { + if (($error = $this->_put(base64_encode($uid))) === false) { + return $error; + } + + /* 334: Continue authentication request */ + //if (PEAR::isError($error = $this->_parseResponse(334))) { + if (($error = $this->_parseResponse(334)) === false) { + return $error; + } + + //if (PEAR::isError($error = $this->_put(base64_encode($pwd)))) { + if (($error = $this->_put(base64_encode($pwd))) === false) { + return $error; + } + + /* 235: Authentication successful */ + //if (PEAR::isError($error = $this->_parseResponse(235))) { + if (($error = $this->_parseResponse(235)) === false) { + return $error; + } + + return true; + } + + /** + * Authenticates the user using the PLAIN method. + * + * @param string $uid The userid to authenticate as. + * @param string $pwd The password to authenticate with. + * @param string $authz The optional authorization proxy identifier. + * + * @throws PEAR_Exception + */ + protected function _authPlain($uid, $pwd, $authz = '') + { + //if (PEAR::isError($error = $this->_put('AUTH', 'PLAIN'))) { + if (($error = $this->_put('AUTH', 'PLAIN')) === false) { + return $error; + } + /* 334: Continue authentication request */ + //if (PEAR::isError($error = $this->_parseResponse(334))) { + if (($error = $this->_parseResponse(334)) === false) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $auth_str = base64_encode($authz . chr(0) . $uid . chr(0) . $pwd); + + //if (PEAR::isError($error = $this->_put($auth_str))) { + if (($error = $this->_put($auth_str)) === false) { + return $error; + } + + /* 235: Authentication successful */ + //if (PEAR::isError($error = $this->_parseResponse(235))) { + if (($error = $this->_parseResponse(235)) === false) { + return $error; + } + + return true; + } + + /** + * Send the HELO command. + * + * @param string The domain name to say we are. + * + * @throws PEAR_Exception + */ + public function helo($domain) + { + //if (PEAR::isError($error = $this->_put('HELO', $domain))) { + if (($error = $this->_put('HELO', $domain)) === false) { + return $error; + } + //if (PEAR::isError($error = $this->_parseResponse(250))) { + if (($error = $this->_parseResponse(250)) === false) { + return $error; + } + + return true; + } + + /** + * Return the list of SMTP service extensions advertised by the server. + * + * @return array The list of SMTP service extensions. + */ + public function getServiceExtensions() + { + return $this->_esmtp; + } + + /** + * Send the MAIL FROM: command. + * + * @param string $sender The sender (reverse path) to set. + * @param string $params String containing additional MAIL parameters, + * such as the NOTIFY flags defined by RFC 1891 + * or the VERP protocol. + * + * @throws PEAR_Exception + */ + public function mailFrom($sender, $params = null) + { + $args = "FROM:<$sender>"; + if (is_string($params) && strlen($params)) { + $args .= ' ' . $params; + } + + //if (PEAR::isError($error = $this->_put('MAIL', $args))) { + if (($error = $this->_put('MAIL', $args)) === false) { + return $error; + } + //if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + if (($error = $this->_parseResponse(250, $this->pipelining)) === false) { + return $error; + } + + return true; + } + + /** + * Send the RCPT TO: command. + * + * @param string $recipient The recipient (forward path) to add. + * @param string $params String containing additional RCPT parameters, + * such as the NOTIFY flags defined by RFC 1891. + * + * @throws PEAR_Exception + */ + public function rcptTo($recipient, $params = null) + { + $args = "TO:<$recipient>"; + if (is_string($params) && strlen($params)) { + $args .= ' ' . $params; + } + + //if (PEAR::isError($error = $this->_put('RCPT', $args))) { + if (($error = $this->_put('RCPT', $args)) === false) { + return $error; + } + //if (PEAR::isError($error = $this->_parseResponse(array(250, 251), $this->pipelining))) { + if (($error = $this->_parseResponse(array(250, 251), $this->pipelining)) === false) { + return $error; + } + + return true; + } + + /** + * Quote the data so that it meets SMTP standards. + * + * This is provided as a separate public function to facilitate + * easier overloading for the cases where it is desirable to + * customize the quoting behavior. + * + * @param string &$data The message text to quote. The string must be + * passed by reference, and the text will be + * modified in place. + */ + public function quotedata(&$data) + { + /* Because a single leading period (.) signifies an end to the + * data, legitimate leading periods need to be "doubled" ('..'). + * Also: change Unix (\n) and Mac (\r) linefeeds into CRLF's + * (\r\n). */ + $data = preg_replace( + array('/^\./m', '/(?:\r\n|\n|\r(?!\n))/'), + array('..', "\r\n"), + $data + ); + } + + /** + * Send the DATA command. + * + * @param mixed $data The message data, either as a string or an open + * file resource. + * @param string $headers The message headers. If $headers is provided, + * $data is assumed to contain only body data. + * + * @throws PEAR_Exception + */ + public function data($data, $headers = null) + { + /* Verify that $data is a supported type. */ + if (!is_string($data) && !is_resource($data)) { + return Net_SMTP::raiseError('Expected a string or file resource'); + } + + /* Start by considering the size of the optional headers string. We + * also account for the addition 4 character "\r\n\r\n" separator + * sequence. */ + $size = is_null($headers) ? 0 : strlen($headers) + 4; + + if (is_resource($data)) { + $stat = fstat($data); + if ($stat === false) { + return Net_SMTP::raiseError('Failed to get file size'); + } + $size += $stat['size']; + } else { + $size += strlen($data); + } + + /* RFC 1870, section 3, subsection 3 states "a value of zero indicates + * that no fixed maximum message size is in force". Furthermore, it + * says that if "the parameter is omitted no information is conveyed + * about the server's fixed maximum message size". */ + $limit = isset($this->_esmtp['SIZE']) ? $this->_esmtp['SIZE'] : 0; + if ($limit > 0 && $size >= $limit) { + $this->disconnect(); + return Net_SMTP::raiseError('Message size exceeds server limit'); + } + + /* Initiate the DATA command. */ + //if (PEAR::isError($error = $this->_put('DATA'))) { + if (($error = $this->_put('DATA')) === false) { + return $error; + } + //if (PEAR::isError($error = $this->_parseResponse(354))) { + if (($error = $this->_parseResponse(354)) === false) { + return $error; + } + + /* If we have a separate headers string, send it first. */ + if (!is_null($headers)) { + $this->quotedata($headers); + //if (PEAR::isError($result = $this->_send($headers . "\r\n\r\n"))) { + if (($result = $this->_send($headers . "\r\n\r\n")) === false) { + return $result; + } + } + + /* Now we can send the message body data. */ + if (is_resource($data)) { + /* Stream the contents of the file resource out over our socket + * connection, line by line. Each line must be run through the + * quoting routine. */ + while (strlen($line = fread($data, 8192)) > 0) { + /* If the last character is an newline, we need to grab the + * next character to check to see if it is a period. */ + while (!feof($data)) { + $char = fread($data, 1); + $line .= $char; + if ($char != "\n") { + break; + } + } + $this->quotedata($line); + //if (PEAR::isError($result = $this->_send($line))) { + if (($result = $this->_send($line)) === false) { + return $result; + } + } + } else { + /* Break up the data by sending one chunk (up to 512k) at a time. + * This approach reduces our peak memory usage. */ + for ($offset = 0; $offset < $size;) { + $end = $offset + 512000; + + /* Ensure we don't read beyond our data size or span multiple + * lines. quotedata() can't properly handle character data + * that's split across two line break boundaries. */ + if ($end >= $size) { + $end = $size; + } else { + for (; $end < $size; $end++) { + if ($data[$end] != "\n") { + break; + } + } + } + + /* Extract our chunk and run it through the quoting routine. */ + $chunk = substr($data, $offset, $end - $offset); + $this->quotedata($chunk); + + /* If we run into a problem along the way, abort. */ + //if (PEAR::isError($result = $this->_send($chunk))) { + if (($result = $this->_send($chunk)) === false) { + return $result; + } + + /* Advance the offset to the end of this chunk. */ + $offset = $end; + } + } + + /* Finally, send the DATA terminator sequence. */ + //if (PEAR::isError($result = $this->_send("\r\n.\r\n"))) { + if (($result = $this->_send("\r\n.\r\n")) === false) { + return $result; + } + + /* Verify that the data was successfully received by the server. */ + //if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + if (($error = $this->_parseResponse(250, $this->pipelining)) === false) { + return $error; + } + + return true; + } + + /** + * Send the SEND FROM: command. + * + * @param string $path The reverse path to send. + * + * @throws PEAR_Exception + */ + public function sendFrom($path) + { + //if (PEAR::isError($error = $this->_put('SEND', "FROM:<$path>"))) { + if (($error = $this->_put('SEND', "FROM:<$path>")) === false) { + return $error; + } + //if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + if (($error = $this->_parseResponse(250, $this->pipelining)) === false) { + return $error; + } + + return true; + } + + /** + * Send the SOML FROM: command. + * + * @param string $path The reverse path to send. + * + * @throws PEAR_Exception + */ + public function somlFrom($path) + { + //if (PEAR::isError($error = $this->_put('SOML', "FROM:<$path>"))) { + if (($error = $this->_put('SOML', "FROM:<$path>")) === false) { + return $error; + } + //if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + if (($error = $this->_parseResponse(250, $this->pipelining)) === false) { + return $error; + } + + return true; + } + + /** + * Send the SAML FROM: command. + * + * @param string $path The reverse path to send. + * + * @throws PEAR_Exception + */ + public function samlFrom($path) + { + //if (PEAR::isError($error = $this->_put('SAML', "FROM:<$path>"))) { + if (($error = $this->_put('SAML', "FROM:<$path>")) === false) { + return $error; + } + //if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + if (($error = $this->_parseResponse(250, $this->pipelining)) === false) { + return $error; + } + + return true; + } + + /** + * Send the RSET command. + * + * @throws PEAR_Exception + */ + public function rset() + { + //if (PEAR::isError($error = $this->_put('RSET'))) { + if (($error = $this->_put('RSET')) === false) { + return $error; + } + //if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + if (($error = $this->_parseResponse(250, $this->pipelining)) === false) { + return $error; + } + + return true; + } + + /** + * Send the VRFY command. + * + * @param string $string The string to verify + * + * @throws PEAR_Exception + */ + public function vrfy($string) + { + /* Note: 251 is also a valid response code */ + //if (PEAR::isError($error = $this->_put('VRFY', $string))) { + if (($error = $this->_put('VRFY', $string)) === false) { + return $error; + } + //if (PEAR::isError($error = $this->_parseResponse(array(250, 252)))) { + if (($error = $this->_parseResponse(array(250, 252))) === false) { + return $error; + } + + return true; + } + + /** + * Send the NOOP command. + * + * @throws PEAR_Exception + */ + public function noop() + { + //if (PEAR::isError($error = $this->_put('NOOP'))) { + if (($error = $this->_put('NOOP')) === false) { + return $error; + } + //if (PEAR::isError($error = $this->_parseResponse(250))) { + if (($error = $this->_parseResponse(250)) === false) { + return $error; + } + + return true; + } + + /** + * Z-Push helper for error logging + * removing PEAR dependency + * + * @param string debug message + * @return boolean always false as there was an error + * @access private + */ + static function raiseError($message) { + ZLog::Write(LOGLEVEL_ERROR, "Net_SMTP error: ". $message); + return false; + } +} diff --git a/sources/include/Net/Socket.php b/sources/include/Net/Socket.php new file mode 100644 index 0000000..b01aa15 --- /dev/null +++ b/sources/include/Net/Socket.php @@ -0,0 +1,716 @@ + + * Chuck Hagenbuch + * + * @category Net + * @package Net_Socket + * @author Stig Bakken + * @author Chuck Hagenbuch + * @copyright 1997-2003 The PHP Group + * @license http://www.php.net/license/2_02.txt PHP 2.02 + * @link http://pear.php.net/packages/Net_Socket + */ + +/** + * Z-Push changes + * + * removed PEAR dependency by implementing own raiseError(), and defining OS_WINDOWS + * + * Reference implementation used: + * http://download.pear.php.net/package/Net_Socket-1.0.14.tgz + * + * + */ + +//require_once 'PEAR.php'; + +if (substr(PHP_OS, 0, 3) == 'WIN') { + define('OS_WINDOWS', true); +} else { + define('OS_WINDOWS', false); +} + +define('NET_SOCKET_READ', 1); +define('NET_SOCKET_WRITE', 2); +define('NET_SOCKET_ERROR', 4); + +/** + * Generalized Socket class. + * + * @category Net + * @package Net_Socket + * @author Stig Bakken + * @author Chuck Hagenbuch + * @copyright 1997-2003 The PHP Group + * @license http://www.php.net/license/2_02.txt PHP 2.02 + * @link http://pear.php.net/packages/Net_Socket + */ +//class Net_Socket extends PEAR +class Net_Socket +{ + /** + * Socket file pointer. + * @var resource $fp + */ + var $fp = null; + + /** + * Whether the socket is blocking. Defaults to true. + * @var boolean $blocking + */ + var $blocking = true; + + /** + * Whether the socket is persistent. Defaults to false. + * @var boolean $persistent + */ + var $persistent = false; + + /** + * The IP address to connect to. + * @var string $addr + */ + var $addr = ''; + + /** + * The port number to connect to. + * @var integer $port + */ + var $port = 0; + + /** + * Number of seconds to wait on socket operations before assuming + * there's no more data. Defaults to no timeout. + * @var integer|float $timeout + */ + var $timeout = null; + + /** + * Number of bytes to read at a time in readLine() and + * readAll(). Defaults to 2048. + * @var integer $lineLength + */ + var $lineLength = 2048; + + /** + * The string to use as a newline terminator. Usually "\r\n" or "\n". + * @var string $newline + */ + var $newline = "\r\n"; + + /** + * Connect to the specified port. If called when the socket is + * already connected, it disconnects and connects again. + * + * @param string $addr IP address or host name (may be with protocol prefix). + * @param integer $port TCP port number. + * @param boolean $persistent (optional) Whether the connection is + * persistent (kept open between requests + * by the web server). + * @param integer $timeout (optional) Connection socket timeout. + * @param array $options See options for stream_context_create. + * + * @access public + * + * @return boolean|PEAR_Error True on success or a PEAR_Error on failure. + */ + function connect($addr, $port = 0, $persistent = null, + $timeout = null, $options = null) + { + if (is_resource($this->fp)) { + @fclose($this->fp); + $this->fp = null; + } + + if (!$addr) { + return $this->raiseError('$addr cannot be empty'); + } else if (strspn($addr, ':.0123456789') == strlen($addr)) { + $this->addr = strpos($addr, ':') !== false ? '['.$addr.']' : $addr; + } else { + $this->addr = $addr; + } + + $this->port = $port % 65536; + + if ($persistent !== null) { + $this->persistent = $persistent; + } + + $openfunc = $this->persistent ? 'pfsockopen' : 'fsockopen'; + $errno = 0; + $errstr = ''; + + $old_track_errors = @ini_set('track_errors', 1); + + if ($timeout <= 0) { + $timeout = @ini_get('default_socket_timeout'); + } + + if ($options && function_exists('stream_context_create')) { + $context = stream_context_create($options); + + // Since PHP 5 fsockopen doesn't allow context specification + if (function_exists('stream_socket_client')) { + $flags = STREAM_CLIENT_CONNECT; + + if ($this->persistent) { + $flags = STREAM_CLIENT_PERSISTENT; + } + + $addr = $this->addr . ':' . $this->port; + $fp = stream_socket_client($addr, $errno, $errstr, + $timeout, $flags, $context); + } else { + $fp = @$openfunc($this->addr, $this->port, $errno, + $errstr, $timeout, $context); + } + } else { + $fp = @$openfunc($this->addr, $this->port, $errno, $errstr, $timeout); + } + + if (!$fp) { + if ($errno == 0 && !strlen($errstr) && isset($php_errormsg)) { + $errstr = $php_errormsg; + } + @ini_set('track_errors', $old_track_errors); + return $this->raiseError($errstr, $errno); + } + + @ini_set('track_errors', $old_track_errors); + $this->fp = $fp; + $this->setTimeout(); + return $this->setBlocking($this->blocking); + } + + /** + * Disconnects from the peer, closes the socket. + * + * @access public + * @return mixed true on success or a PEAR_Error instance otherwise + */ + function disconnect() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + @fclose($this->fp); + $this->fp = null; + return true; + } + + /** + * Set the newline character/sequence to use. + * + * @param string $newline Newline character(s) + * @return boolean True + */ + function setNewline($newline) + { + $this->newline = $newline; + return true; + } + + /** + * Find out if the socket is in blocking mode. + * + * @access public + * @return boolean The current blocking mode. + */ + function isBlocking() + { + return $this->blocking; + } + + /** + * Sets whether the socket connection should be blocking or + * not. A read call to a non-blocking socket will return immediately + * if there is no data available, whereas it will block until there + * is data for blocking sockets. + * + * @param boolean $mode True for blocking sockets, false for nonblocking. + * + * @access public + * @return mixed true on success or a PEAR_Error instance otherwise + */ + function setBlocking($mode) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $this->blocking = $mode; + stream_set_blocking($this->fp, (int)$this->blocking); + return true; + } + + /** + * Sets the timeout value on socket descriptor, + * expressed in the sum of seconds and microseconds + * + * @param integer $seconds Seconds. + * @param integer $microseconds Microseconds, optional. + * + * @access public + * @return mixed True on success or false on failure or + * a PEAR_Error instance when not connected + */ + function setTimeout($seconds = null, $microseconds = null) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + if ($seconds === null && $microseconds === null) { + $seconds = (int) $this->timeout; + $microseconds = (int) (($this->timeout - $seconds) * 1000000); + } else { + $this->timeout = $seconds + $microseconds/1000000; + } + + if ($this->timeout > 0) { + return stream_set_timeout($this->fp, (int) $seconds, (int) $microseconds); + } + else { + return false; + } + } + + /** + * Sets the file buffering size on the stream. + * See php's stream_set_write_buffer for more information. + * + * @param integer $size Write buffer size. + * + * @access public + * @return mixed on success or an PEAR_Error object otherwise + */ + function setWriteBuffer($size) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $returned = stream_set_write_buffer($this->fp, $size); + if ($returned == 0) { + return true; + } + return $this->raiseError('Cannot set write buffer.'); + } + + /** + * Returns information about an existing socket resource. + * Currently returns four entries in the result array: + * + *

+ * timed_out (bool) - The socket timed out waiting for data
+ * blocked (bool) - The socket was blocked
+ * eof (bool) - Indicates EOF event
+ * unread_bytes (int) - Number of bytes left in the socket buffer
+ *

+ * + * @access public + * @return mixed Array containing information about existing socket + * resource or a PEAR_Error instance otherwise + */ + function getStatus() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return stream_get_meta_data($this->fp); + } + + /** + * Get a specified line of data + * + * @param int $size Reading ends when size - 1 bytes have been read, + * or a newline or an EOF (whichever comes first). + * If no size is specified, it will keep reading from + * the stream until it reaches the end of the line. + * + * @access public + * @return mixed $size bytes of data from the socket, or a PEAR_Error if + * not connected. If an error occurs, FALSE is returned. + */ + function gets($size = null) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + if (is_null($size)) { + return @fgets($this->fp); + } else { + return @fgets($this->fp, $size); + } + } + + /** + * Read a specified amount of data. This is guaranteed to return, + * and has the added benefit of getting everything in one fread() + * chunk; if you know the size of the data you're getting + * beforehand, this is definitely the way to go. + * + * @param integer $size The number of bytes to read from the socket. + * + * @access public + * @return $size bytes of data from the socket, or a PEAR_Error if + * not connected. + */ + function read($size) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return @fread($this->fp, $size); + } + + /** + * Write a specified amount of data. + * + * @param string $data Data to write. + * @param integer $blocksize Amount of data to write at once. + * NULL means all at once. + * + * @access public + * @return mixed If the socket is not connected, returns an instance of + * PEAR_Error. + * If the write succeeds, returns the number of bytes written. + * If the write fails, returns false. + * If the socket times out, returns an instance of PEAR_Error. + */ + function write($data, $blocksize = null) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + if (is_null($blocksize) && !OS_WINDOWS) { + $written = @fwrite($this->fp, $data); + + // Check for timeout or lost connection + if (!$written) { + $meta_data = $this->getStatus(); + + if (!is_array($meta_data)) { + return $meta_data; // PEAR_Error + } + + if (!empty($meta_data['timed_out'])) { + return $this->raiseError('timed out'); + } + } + + return $written; + } else { + if (is_null($blocksize)) { + $blocksize = 1024; + } + + $pos = 0; + $size = strlen($data); + while ($pos < $size) { + $written = @fwrite($this->fp, substr($data, $pos, $blocksize)); + + // Check for timeout or lost connection + if (!$written) { + $meta_data = $this->getStatus(); + + if (!is_array($meta_data)) { + return $meta_data; // PEAR_Error + } + + if (!empty($meta_data['timed_out'])) { + return $this->raiseError('timed out'); + } + + return $written; + } + + $pos += $written; + } + + return $pos; + } + } + + /** + * Write a line of data to the socket, followed by a trailing newline. + * + * @param string $data Data to write + * + * @access public + * @return mixed fwrite() result, or PEAR_Error when not connected + */ + function writeLine($data) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return fwrite($this->fp, $data . $this->newline); + } + + /** + * Tests for end-of-file on a socket descriptor. + * + * Also returns true if the socket is disconnected. + * + * @access public + * @return bool + */ + function eof() + { + return (!is_resource($this->fp) || feof($this->fp)); + } + + /** + * Reads a byte of data + * + * @access public + * @return 1 byte of data from the socket, or a PEAR_Error if + * not connected. + */ + function readByte() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return ord(@fread($this->fp, 1)); + } + + /** + * Reads a word of data + * + * @access public + * @return 1 word of data from the socket, or a PEAR_Error if + * not connected. + */ + function readWord() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $buf = @fread($this->fp, 2); + return (ord($buf[0]) + (ord($buf[1]) << 8)); + } + + /** + * Reads an int of data + * + * @access public + * @return integer 1 int of data from the socket, or a PEAR_Error if + * not connected. + */ + function readInt() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $buf = @fread($this->fp, 4); + return (ord($buf[0]) + (ord($buf[1]) << 8) + + (ord($buf[2]) << 16) + (ord($buf[3]) << 24)); + } + + /** + * Reads a zero-terminated string of data + * + * @access public + * @return string, or a PEAR_Error if + * not connected. + */ + function readString() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $string = ''; + while (($char = @fread($this->fp, 1)) != "\x00") { + $string .= $char; + } + return $string; + } + + /** + * Reads an IP Address and returns it in a dot formatted string + * + * @access public + * @return Dot formatted string, or a PEAR_Error if + * not connected. + */ + function readIPAddress() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $buf = @fread($this->fp, 4); + return sprintf('%d.%d.%d.%d', ord($buf[0]), ord($buf[1]), + ord($buf[2]), ord($buf[3])); + } + + /** + * Read until either the end of the socket or a newline, whichever + * comes first. Strips the trailing newline from the returned data. + * + * @access public + * @return All available data up to a newline, without that + * newline, or until the end of the socket, or a PEAR_Error if + * not connected. + */ + function readLine() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $line = ''; + + $timeout = time() + $this->timeout; + + while (!feof($this->fp) && (!$this->timeout || time() < $timeout)) { + $line .= @fgets($this->fp, $this->lineLength); + if (substr($line, -1) == "\n") { + return rtrim($line, $this->newline); + } + } + return $line; + } + + /** + * Read until the socket closes, or until there is no more data in + * the inner PHP buffer. If the inner buffer is empty, in blocking + * mode we wait for at least 1 byte of data. Therefore, in + * blocking mode, if there is no data at all to be read, this + * function will never exit (unless the socket is closed on the + * remote end). + * + * @access public + * + * @return string All data until the socket closes, or a PEAR_Error if + * not connected. + */ + function readAll() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $data = ''; + while (!feof($this->fp)) { + $data .= @fread($this->fp, $this->lineLength); + } + return $data; + } + + /** + * Runs the equivalent of the select() system call on the socket + * with a timeout specified by tv_sec and tv_usec. + * + * @param integer $state Which of read/write/error to check for. + * @param integer $tv_sec Number of seconds for timeout. + * @param integer $tv_usec Number of microseconds for timeout. + * + * @access public + * @return False if select fails, integer describing which of read/write/error + * are ready, or PEAR_Error if not connected. + */ + function select($state, $tv_sec, $tv_usec = 0) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $read = null; + $write = null; + $except = null; + if ($state & NET_SOCKET_READ) { + $read[] = $this->fp; + } + if ($state & NET_SOCKET_WRITE) { + $write[] = $this->fp; + } + if ($state & NET_SOCKET_ERROR) { + $except[] = $this->fp; + } + if (false === ($sr = stream_select($read, $write, $except, + $tv_sec, $tv_usec))) { + return false; + } + + $result = 0; + if (count($read)) { + $result |= NET_SOCKET_READ; + } + if (count($write)) { + $result |= NET_SOCKET_WRITE; + } + if (count($except)) { + $result |= NET_SOCKET_ERROR; + } + return $result; + } + + /** + * Turns encryption on/off on a connected socket. + * + * @param bool $enabled Set this parameter to true to enable encryption + * and false to disable encryption. + * @param integer $type Type of encryption. See stream_socket_enable_crypto() + * for values. + * + * @see http://se.php.net/manual/en/function.stream-socket-enable-crypto.php + * @access public + * @return false on error, true on success and 0 if there isn't enough data + * and the user should try again (non-blocking sockets only). + * A PEAR_Error object is returned if the socket is not + * connected + */ + function enableCrypto($enabled, $type) + { + if (version_compare(phpversion(), "5.1.0", ">=")) { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + return @stream_socket_enable_crypto($this->fp, $enabled, $type); + } else { + $msg = 'Net_Socket::enableCrypto() requires php version >= 5.1.0'; + return $this->raiseError($msg); + } + } + + /** + * Z-Push helper for error logging + * removing PEAR dependency + * + * @param string debug message + * @return boolean always false as there was an error + * @access private + */ + function raiseError($message) { + ZLog::Write(LOGLEVEL_ERROR, "Net_Socket error: ". $message); + return false; + } +} diff --git a/sources/include/caldav-client-v2.php b/sources/include/caldav-client-v2.php new file mode 100644 index 0000000..7f79d6f --- /dev/null +++ b/sources/include/caldav-client-v2.php @@ -0,0 +1,1113 @@ + +* @copyright Andrew McMillan +* @license http://www.gnu.org/licenses/lgpl-3.0.txt GNU LGPL version 3 or later +* Update xbgmsharp +*/ + +require_once('XMLDocument.php'); + +/** +* A class for holding basic calendar information +* @package awl +*/ +class CalendarInfo { + public $url, $displayname, $getctag, $id; + + function __construct( $url, $displayname = null, $getctag = null, $id = null ) { + $this->url = $url; + $this->displayname = $displayname; + $this->getctag = $getctag; + $this->id = $id; + } + + function __toString() { + return( '(URL: '.$this->url.' Ctag: '.$this->getctag.' Displayname: '.$this->displayname .')'. "\n" ); + } +} + + +/** +* A class for accessing DAViCal via CalDAV, as a client +* +* @package awl +*/ +class CalDAVClient { + /** + * Server, username, password, calendar + * + * @var string + */ + protected $base_url, $user, $pass, $entry, $protocol, $server, $port, $http_auth; + + /** + * The principal-URL we're using + */ + protected $principal_url; + + /** + * The calendar-URL we're using + */ + protected $calendar_url; + + /** + * The calendar-home-set we're using + */ + protected $calendar_home_set; + + /** + * The calendar_urls we have discovered + */ + protected $calendar_urls; + + /** + * The useragent which is send to the caldav server + * + * @var string + */ + public $user_agent = 'DAViCalClient'; + + protected $headers = array(); + protected $body = ""; + protected $requestMethod = "GET"; + protected $httpRequest = ""; // for debugging http headers sent + protected $xmlRequest = ""; // for debugging xml sent + protected $xmlResponse = ""; // xml received + protected $httpResponseCode = 0; // http response code + protected $httpResponseHeaders = ""; + protected $httpResponseBody = ""; + + protected $parser; // our XML parser object + + private $debug = false; // Whether we are debugging + + /** + * Constructor, initialises the class + * + * @param string $base_url The URL for the calendar server + * @param string $user The name of the user logging in + * @param string $pass The password for that user + */ + function __construct( $base_url, $user, $pass ) { + $this->user = $user; + $this->pass = $pass; + $this->headers = array(); + $this->http_auth = array( + 'method' => 'basic' + ); + + if ( preg_match( '#^(https?)://([a-z0-9.-]+)(:([0-9]+))?(/.*)$#', $base_url, $matches ) ) { + $this->server = $matches[2]; + $this->base_url = $matches[5]; + if ( $matches[1] == 'https' ) { + $this->protocol = 'ssl'; + $this->port = 443; + } + else { + $this->protocol = 'tcp'; + $this->port = 80; + } + if ( $matches[4] != '' ) { + $this->port = intval($matches[4]); + } + } + else { + trigger_error("Invalid URL: '".$base_url."'", E_USER_ERROR); + } + } + + + /** + * Call this to enable / disable debugging. It will return the prior value of the debugging flag. + * @param boolean $new_value The new value for debugging. + * @return boolean The previous value, in case you want to restore it later. + */ + function SetDebug( $new_value ) { + $old_value = $this->debug; + if ( $new_value ) + $this->debug = true; + else + $this->debug = false; + return $old_value; + } + + + + /** + * Adds an If-Match or If-None-Match header + * + * @param bool $match to Match or Not to Match, that is the question! + * @param string $etag The etag to match / not match against. + */ + function SetMatch( $match, $etag = '*' ) { + $this->headers['match'] = sprintf( "%s-Match: \"%s\"", ($match ? "If" : "If-None"), trim($etag,'"')); + } + + /** + * Add a Depth: header. Valid values are 0, 1 or infinity + * + * @param int $depth The depth, default to infinity + */ + function SetDepth( $depth = '0' ) { + $this->headers['depth'] = 'Depth: '. ($depth == '1' ? "1" : ($depth == 'infinity' ? $depth : "0") ); + } + + /** + * Add a Depth: header. Valid values are 1 or infinity + * + * @param int $depth The depth, default to infinity + */ + function SetUserAgent( $user_agent = null ) { + if ( !isset($user_agent) ) $user_agent = $this->user_agent; + $this->user_agent = $user_agent; + } + + /** + * Add a Content-type: header. + * + * @param string $type The content type + */ + function SetContentType( $type ) { + $this->headers['content-type'] = "Content-type: $type"; + } + + /** + * Set the calendar_url we will be using for a while. + * + * @param string $url The calendar_url + */ + function SetCalendar( $url ) { + $this->calendar_url = $url; + } + + /** + * Split response into httpResponse and xmlResponse + * + * @param string Response from server + */ + function ParseResponse( $response ) { + $pos = strpos($response, 'xmlResponse = trim(substr($response, $pos)); + $this->xmlResponse = preg_replace('{>[^>]*$}s', '>',$this->xmlResponse ); + $parser = xml_parser_create_ns('UTF-8'); + xml_parser_set_option ( $parser, XML_OPTION_SKIP_WHITE, 1 ); + xml_parser_set_option ( $parser, XML_OPTION_CASE_FOLDING, 0 ); + + if ( xml_parse_into_struct( $parser, $this->xmlResponse, $this->xmlnodes, $this->xmltags ) === 0 ) { + if ( $this->debug ) printf( "XML parsing error: %s - %s\n", xml_get_error_code($parser), xml_error_string(xml_get_error_code($parser)) ); +// debug_print_backtrace(); +// echo "\nNodes array............................................................\n"; print_r( $this->xmlnodes ); +// echo "\nTags array............................................................\n"; print_r( $this->xmltags ); + if ( $this->debug ) printf( "\nXML Reponse:\n%s\n", $this->xmlResponse ); + } + + xml_parser_free($parser); + } + } + + /** + * Output http request headers + * + * @return HTTP headers + */ + function GetHttpRequest() { + return $this->httpRequest; + } + /** + * Output http response headers + * + * @return HTTP headers + */ + function GetResponseHeaders() { + return $this->httpResponseHeaders; + } + /** + * Output http response body + * + * @return HTTP body + */ + function GetResponseBody() { + return $this->httpResponseBody; + } + /** + * Output xml request + * + * @return raw xml + */ + function GetXmlRequest() { + return $this->xmlRequest; + } + /** + * Output xml response + * + * @return raw xml + */ + function GetXmlResponse() { + return $this->xmlResponse; + } + + /** + * Send a request to the server + * + * @param string $url The URL to make the request to + * + * @return string The content of the response from the server + */ + function DoRequest( $url = null, $switch_auth = false ) { + if(!defined("_FSOCK_TIMEOUT")){ define("_FSOCK_TIMEOUT", 10); } + $headers = array(); + + if ( !isset($url) ) $url = $this->base_url; + $this->request_url = $url; + $url = preg_replace('{^https?://[^/]+}', '', $url); + // URLencode if it isn't already + if ( preg_match( '{[^%?&=+,.-_/a-z0-9]}', $url ) ) { + $url = str_replace(rawurlencode('/'),'/',rawurlencode($url)); + $url = str_replace(rawurlencode('?'),'?',$url); + $url = str_replace(rawurlencode('&'),'&',$url); + $url = str_replace(rawurlencode('='),'=',$url); + $url = str_replace(rawurlencode('+'),'+',$url); + $url = str_replace(rawurlencode(','),',',$url); + } + $headers[] = $this->requestMethod." ". $url . " HTTP/1.1"; + if( 'basic' != $this->http_auth['method'] && !isset( $this->http_auth['stale'] )) { + // then it's digest... + $digest_A1 = md5( $this->user .':' . $this->http_auth['realm'] . ':' . $this->pass ); + $digest_A2 = md5( $this->requestMethod.':'.$url ); + $digest = 'Authorization: Digest username="'.$this->user.'", realm="'.$this->http_auth['realm'].'", nonce="'.$this->http_auth['nonce'].'", uri="'.$url.'", algorithm=MD5, response="'.md5($digest_A1.':'.$this->http_auth['nonce'].':'.$digest_A2).'"'; + // todo: loop through challenges + $headers[] = $digest; + } else { + $headers[] = "Authorization: Basic ".base64_encode($this->user .":". $this->pass ); + } + $headers[] = "Host: ".$this->server .":".$this->port; + + if ( !isset($this->headers['content-type']) ) $this->headers['content-type'] = "Content-type: text/plain"; + foreach( $this->headers as $ii => $head ) { + $headers[] = $head; + } + $headers[] = "Content-Length: " . strlen($this->body); + $headers[] = "User-Agent: " . $this->user_agent; + $headers[] = 'Connection: close'; + $this->httpRequest = join("\r\n",$headers); + $this->xmlRequest = $this->body; + + $this->xmlResponse = ''; + + $fip = fsockopen( $this->protocol . '://' . $this->server, $this->port, $errno, $errstr, _FSOCK_TIMEOUT); //error handling? + if ( !(get_resource_type($fip) == 'stream') ) return false; + if ( !fwrite($fip, $this->httpRequest."\r\n\r\n".$this->body) ) { fclose($fip); return false; } + $response = ""; + while( !feof($fip) ) { $response .= fgets($fip,8192); } + fclose($fip); + + list( $this->httpResponseHeaders, $this->httpResponseBody ) = preg_split( '{\r?\n\r?\n}s', $response, 2 ); + $header_lines = preg_split( '{\r?\n}', $this->httpResponseHeaders ); + if( count( preg_grep( '{^HTTP/1.1 3\d\d.*}i', $header_lines ) ) > 0 && count( $matches = preg_grep( '{^Location: (.*)}i', $header_lines ) ) > 0 ) { + // todo: cache move? + return $this->DoRequest( substr( end( $matches ), 10 ), $switch_auth ); + } + + $can_switch_auth = false; + if( true != $switch_auth && count( preg_grep( '{^HTTP/1.1 401 Unauthorized.*}i', $header_lines ) ) > 0 && 'basic' == $this->http_auth['method'] ) { + $can_switch_auth = true; + } + if( count( $digest = preg_grep( '{^WWW-Authenticate: digest.+}', $header_lines ) ) > 0 ) { + $digest_string = end( $digest ); + $this->http_auth['method'] = 'digest'; + $this->http_auth['challenge'] = array(); + + foreach( array( 'realm', 'domain', 'nonce', 'stale' ) as $c ) { + if( preg_match( '{.+?'.$c.'="([^"]+)".*?}', $digest_string, $matches ) ) { + $this->http_auth[$c] = $matches[1]; + } + } + foreach( array( 'opaque', 'algorithm', 'qop-options', 'auth-param' ) as $c ) { + if( preg_match( '{.+?'.$c.'="([^"]+)".*?}', $digest_string, $matches ) ) { + $this->http_auth['challenge'][$c] = $matches[1]; + } + } + if( isset( $this->http_auth['challenge']['qop-options'] ) ) { + if( preg_match( '{.+?qop-value="([^"]+)".*?}', $digest_string, $matches ) ) { + $this->http_auth['challenge'][$c] = $matches[1]; + } + } + if( $can_switch_auth ) { + return $this->DoRequest( $url, true ); + } + } + if ( preg_match( '{Transfer-Encoding: chunked}i', $this->httpResponseHeaders ) ) $this->Unchunk(); + if ( preg_match('/HTTP\/\d\.\d (\d{3})/', $this->httpResponseHeaders, $status) ) + $this->httpResponseCode = intval($status[1]); + else + $this->httpResponseCode = 0; + + $this->headers = array(); // reset the headers array for our next request + $this->ParseResponse($this->httpResponseBody); + return $response; + } + + + /** + * Unchunk a chunked response + */ + function Unchunk() { + $content = ''; + $chunks = $this->httpResponseBody; + // printf( "\n================================\n%s\n================================\n", $chunks ); + do { + $bytes = 0; + if ( preg_match('{^((\r\n)?\s*([ 0-9a-fA-F]+)(;[^\n]*)?\r?\n)}', $chunks, $matches ) ) { + $octets = $matches[3]; + $bytes = hexdec($octets); + $pos = strlen($matches[1]); + // printf( "Chunk size 0x%s (%d)\n", $octets, $bytes ); + if ( $bytes > 0 ) { + // printf( "---------------------------------\n%s\n---------------------------------\n", substr($chunks,$pos,$bytes) ); + $content .= substr($chunks,$pos,$bytes); + $chunks = substr($chunks,$pos + $bytes + 2); + // printf( "+++++++++++++++++++++++++++++++++\n%s\n+++++++++++++++++++++++++++++++++\n", $chunks ); + } + } + else { + $content .= $chunks; + } + } + while( $bytes > 0 ); + $this->httpResponseBody = $content; + // printf( "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n%s\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n", $content ); + } + + + /** + * Send an OPTIONS request to the server + * + * @param string $url The URL to make the request to + * + * @return array The allowed options + */ + function DoOptionsRequest( $url = null ) { + $this->requestMethod = "OPTIONS"; + $this->body = ""; + $headers = $this->DoRequest($url); + $options_header = preg_replace( '/^.*Allow: ([a-z, ]+)\r?\n.*/is', '$1', $headers ); + $options = array_flip( preg_split( '/[, ]+/', $options_header )); + return $options; + } + + + + /** + * Send an XML request to the server (e.g. PROPFIND, REPORT, MKCALENDAR) + * + * @param string $method The method (PROPFIND, REPORT, etc) to use with the request + * @param string $xml The XML to send along with the request + * @param string $url The URL to make the request to + * + * @return array An array of the allowed methods + */ + function DoXMLRequest( $request_method, $xml, $url = null ) { + $this->body = $xml; + $this->requestMethod = $request_method; + $this->SetContentType("text/xml"); + return $this->DoRequest($url); + } + + + + /** + * Get a single item from the server. + * + * @param string $url The URL to GET + */ + function DoGETRequest( $url ) { + $this->body = ""; + $this->requestMethod = "GET"; + return $this->DoRequest( $url ); + } + + + /** + * Get the HEAD of a single item from the server. + * + * @param string $url The URL to HEAD + */ + function DoHEADRequest( $url ) { + $this->body = ""; + $this->requestMethod = "HEAD"; + return $this->DoRequest( $url ); + } + + + /** + * PUT a text/icalendar resource, returning the etag + * + * @param string $url The URL to make the request to + * @param string $icalendar The iCalendar resource to send to the server + * @param string $etag The etag of an existing resource to be overwritten, or '*' for a new resource. + * + * @return string The content of the response from the server + */ + function DoPUTRequest( $url, $icalendar, $etag = null ) { + $this->body = $icalendar; + + $this->requestMethod = "PUT"; + if ( $etag != null ) { + $this->SetMatch( ($etag != '*'), $etag ); + } + $this->SetContentType('text/calendar; charset=utf-8'); + $this->DoRequest($url); + + $etag = null; + if ( preg_match( '{^ETag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) $etag = $matches[1]; + if ( !isset($etag) || $etag == '' ) { + if ( $this->debug ) printf( "No etag in:\n%s\n", $this->httpResponseHeaders ); + $save_request = $this->httpRequest; + $save_response_headers = $this->httpResponseHeaders; + $this->DoHEADRequest( $url ); + if ( preg_match( '{^Etag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) $etag = $matches[1]; + if ( !isset($etag) || $etag == '' ) { + if ( $this->debug ) printf( "Still No etag in:\n%s\n", $this->httpResponseHeaders ); + } + $this->httpRequest = $save_request; + $this->httpResponseHeaders = $save_response_headers; + } + return $etag; + } + + + /** + * DELETE a text/icalendar resource + * + * @param string $url The URL to make the request to + * @param string $etag The etag of an existing resource to be deleted, or '*' for any resource at that URL. + * + * @return int The HTTP Result Code for the DELETE + */ + function DoDELETERequest( $url, $etag = null ) { + $this->body = ""; + + $this->requestMethod = "DELETE"; + if ( $etag != null ) { + $this->SetMatch( true, $etag ); + } + $this->DoRequest($url); + return $this->httpResponseCode; + } + + + /** + * Get a single item from the server. + * + * @param string $url The URL to PROPFIND on + */ + function DoPROPFINDRequest( $url, $props, $depth = 0 ) { + $this->SetDepth($depth); + $xml = new XMLDocument( array( 'DAV:' => '', 'urn:ietf:params:xml:ns:caldav' => 'C' ) ); + $prop = new XMLElement('prop'); + foreach( $props AS $v ) { + $xml->NSElement($prop,$v); + } + + $this->body = $xml->Render('propfind',$prop ); + + $this->requestMethod = "PROPFIND"; + $this->SetContentType("text/xml"); + $this->DoRequest($url); + return $this->GetXmlResponse(); + } + + + /** + * Get/Set the Principal URL + * + * @param $url string The Principal URL to set + */ + function PrincipalURL( $url = null ) { + if ( isset($url) ) { + $this->principal_url = $url; + } + return $this->principal_url; + } + + + /** + * Get/Set the calendar-home-set URL + * + * @param $url array of string The calendar-home-set URLs to set + */ + function CalendarHomeSet( $urls = null ) { + if ( isset($urls) ) { + if ( ! is_array($urls) ) $urls = array($urls); + $this->calendar_home_set = $urls; + } + return $this->calendar_home_set; + } + + + /** + * Get/Set the calendar-home-set URL + * + * @param $urls array of string The calendar URLs to set + */ + function CalendarUrls( $urls = null ) { + if ( isset($urls) ) { + if ( ! is_array($urls) ) $urls = array($urls); + $this->calendar_urls = $urls; + } + return $this->calendar_urls; + } + + + /** + * Return the first occurrence of an href inside the named tag. + * + * @param string $tagname The tag name to find the href inside of + */ + function HrefValueInside( $tagname ) { + foreach( $this->xmltags[$tagname] AS $k => $v ) { + $j = $v + 1; + if ( $this->xmlnodes[$j]['tag'] == 'DAV::href' ) { + return rawurldecode($this->xmlnodes[$j]['value']); + } + } + return null; + } + + + /** + * Return the href containing this property. Except only if it's inside a status != 200 + * + * @param string $tagname The tag name of the property to find the href for + * @param integer $which Which instance of the tag should we use + */ + function HrefForProp( $tagname, $i = 0 ) { + if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) { + $j = $this->xmltags[$tagname][$i]; + while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' ) { +// printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']); + if ( $this->xmlnodes[$j]['tag'] == 'DAV::status' && $this->xmlnodes[$j]['value'] != 'HTTP/1.1 200 OK' ) return null; + } +// printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']); + if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) { +// printf( "Value[$j]: %s\n", $this->xmlnodes[$j]['value']); + return rawurldecode($this->xmlnodes[$j]['value']); + } + } + else { + if ( $this->debug ) printf( "xmltags[$tagname] or xmltags[$tagname][$i] is not set\n"); + } + return null; + } + + + /** + * Return the href which has a resourcetype of the specified type + * + * @param string $tagname The tag name of the resourcetype to find the href for + * @param integer $which Which instance of the tag should we use + */ + function HrefForResourcetype( $tagname, $i = 0 ) { + if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) { + $j = $this->xmltags[$tagname][$i]; + while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::resourcetype' ); + if ( $j > 0 ) { + while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' ); + if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) { + return rawurldecode($this->xmlnodes[$j]['value']); + } + } + } + return null; + } + + + /** + * Return the ... of a propstat where the status is OK + * + * @param string $nodenum The node number in the xmlnodes which is the href + */ + function GetOKProps( $nodenum ) { + $props = null; + $level = $this->xmlnodes[$nodenum]['level']; + $status = ''; + while ( $this->xmlnodes[++$nodenum]['level'] >= $level ) { + if ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::propstat' ) { + if ( $this->xmlnodes[$nodenum]['type'] == 'open' ) { + $props = array(); + $status = ''; + } + else { + if ( $status == 'HTTP/1.1 200 OK' ) break; + } + } + elseif ( !isset($this->xmlnodes[$nodenum]) || !is_array($this->xmlnodes[$nodenum]) ) { + break; + } + elseif ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::status' ) { + $status = $this->xmlnodes[$nodenum]['value']; + } + else { + $props[] = $this->xmlnodes[$nodenum]; + } + } + return $props; + } + + + /** + * Attack the given URL in an attempt to find a principal URL + * + * @param string $url The URL to find the principal-URL from + */ + function FindPrincipal( $url=null ) { + $xml = $this->DoPROPFINDRequest( $this->base_url, array('resourcetype', 'current-user-principal', 'owner', 'principal-URL', + 'urn:ietf:params:xml:ns:caldav:calendar-home-set'), 1); + + $principal_url = $this->HrefForProp('DAV::principal'); + + if ( !isset($principal_url) ) { + foreach( array('DAV::current-user-principal', 'DAV::principal-URL', 'DAV::owner') AS $href ) { + if ( !isset($principal_url) ) { + $principal_url = $this->HrefValueInside($href); + } + } + } + + return $this->PrincipalURL($principal_url); + } + + + /** + * Attack the given URL in an attempt to find a principal URL + * + * @param string $url The URL to find the calendar-home-set from + */ + function FindCalendarHome( $recursed=false ) { + if ( !isset($this->principal_url) ) { + $this->FindPrincipal(); + } + if ( $recursed ) { + $this->DoPROPFINDRequest( $this->principal_url, array('urn:ietf:params:xml:ns:caldav:calendar-home-set'), 0); + } + + $calendar_home = array(); + foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-home-set'] AS $k => $v ) { + if ( $this->xmlnodes[$v]['type'] != 'open' ) continue; + while( $this->xmlnodes[++$v]['type'] != 'close' && $this->xmlnodes[$v]['tag'] != 'urn:ietf:params:xml:ns:caldav:calendar-home-set' ) { +// printf( "Tag: '%s' = '%s'\n", $this->xmlnodes[$v]['tag'], $this->xmlnodes[$v]['value']); + if ( $this->xmlnodes[$v]['tag'] == 'DAV::href' && isset($this->xmlnodes[$v]['value']) ) + $calendar_home[] = rawurldecode($this->xmlnodes[$v]['value']); + } + } + + if ( !$recursed && count($calendar_home) < 1 ) { + $calendar_home = $this->FindCalendarHome(true); + } + + return $this->CalendarHomeSet($calendar_home); + } + + + /** + * Find the calendars, from the calendar_home_set + */ + function FindCalendars( $recursed=false ) { + if ( !isset($this->calendar_home_set[0]) ) { + $this->FindCalendarHome(); + } + $this->DoPROPFINDRequest( $this->calendar_home_set[0], array('resourcetype','displayname','http://calendarserver.org/ns/:getctag'), 1); + + $calendars = array(); + if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar']) ) { + $calendar_urls = array(); + foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar'] AS $k => $v ) { + $calendar_urls[$this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar', $k)] = 1; + } + + foreach( $this->xmltags['DAV::href'] AS $i => $hnode ) { + $href = rawurldecode($this->xmlnodes[$hnode]['value']); + + if ( !isset($calendar_urls[$href]) ) continue; + +// printf("Seems '%s' is a calendar.\n", $href ); + + $calendar = new CalendarInfo($href); + $ok_props = $this->GetOKProps($hnode); + foreach( $ok_props AS $v ) { +// printf("Looking at: %s[%s]\n", $href, $v['tag'] ); + switch( $v['tag'] ) { + case 'http://calendarserver.org/ns/:getctag': + $calendar->getctag = $v['value']; + break; + case 'DAV::displayname': + $calendar->displayname = $v['value']; + break; + } + } + $calendar->id = rtrim(str_replace($this->calendar_home_set[0], "", $calendar->url), "/"); + $calendars[] = $calendar; + } + } + + return $this->CalendarUrls($calendars); + } + + + /** + * Find the calendars, from the calendar_home_set + */ + function GetCalendarDetails( $url = null ) { + if ( isset($url) ) $this->SetCalendar($url); + + if ( !isset($this->calendar_home_set[0]) ) { + $this->FindCalendarHome(); + } + + $calendar_properties = array( 'resourcetype', 'displayname', 'http://calendarserver.org/ns/:getctag', 'urn:ietf:params:xml:ns:caldav:calendar-timezone', 'supported-report-set' ); + $this->DoPROPFINDRequest( $this->calendar_url, $calendar_properties, 0); + + $hnode = $this->xmltags['DAV::href'][0]; + $href = rawurldecode($this->xmlnodes[$hnode]['value']); + + $calendar = new CalendarInfo($href); + $ok_props = $this->GetOKProps($hnode); + foreach( $ok_props AS $k => $v ) { + $name = preg_replace( '{^.*:}', '', $v['tag'] ); + if ( isset($v['value'] ) ) { + $calendar->{$name} = $v['value']; + } +/* else { + printf( "Calendar property '%s' has no text content\n", $v['tag'] ); + }*/ + } + $calendar->id = rtrim(str_replace($this->calendar_home_set[0], "", $calendar->url), "/"); + + return $calendar; + } + + + /** + * Get all etags for a calendar + */ + function GetCollectionETags( $url = null ) { + if ( isset($url) ) $this->SetCalendar($url); + + $this->DoPROPFINDRequest( $this->calendar_url, array('getetag'), 1); + + $etags = array(); + if ( isset($this->xmltags['DAV::getetag']) ) { + foreach( $this->xmltags['DAV::getetag'] AS $k => $v ) { + $href = $this->HrefForProp('DAV::getetag', $k); + if ( isset($href) && isset($this->xmlnodes[$v]['value']) ) $etags[$href] = $this->xmlnodes[$v]['value']; + } + } + + return $etags; + } + + + /** + * Get a bunch of events for a calendar with a calendar-multiget report + */ + function CalendarMultiget( $event_hrefs, $url = null ) { + + if ( isset($url) ) $this->SetCalendar($url); + + $hrefs = ''; + foreach( $event_hrefs AS $k => $href ) { + $href = str_replace( rawurlencode('/'),'/',rawurlencode($href)); + $hrefs .= ''.$href.''; + } + $this->body = << + + +$hrefs + +EOXML; + + $this->requestMethod = "REPORT"; + $this->SetContentType("text/xml"); + $this->DoRequest( $this->calendar_url ); + + $events = array(); + if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data']) ) { + foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data'] AS $k => $v ) { + $href = $this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar-data', $k); +// echo "Calendar-data:\n"; print_r($this->xmlnodes[$v]); + $events[$href] = $this->xmlnodes[$v]['value']; + } + } + else { + foreach( $event_hrefs AS $k => $href ) { + $this->DoGETRequest($href); + $events[$href] = $this->httpResponseBody; + } + } + + return $events; + } + + + /** + * Given XML for a calendar query, return an array of the events (/todos) in the + * response. Each event in the array will have a 'href', 'etag' and '$response_type' + * part, where the 'href' is relative to the calendar and the '$response_type' contains the + * definition of the calendar data in iCalendar format. + * + * @param string $filter XML fragment which is the element of a calendar-query + * @param string $url The URL of the calendar, or empty/null to use the 'current' calendar_url + * + * @return array An array of the relative URLs, etags, and events from the server. Each element of the array will + * be an array with 'href', 'etag' and 'data' elements, corresponding to the URL, the server-supplied + * etag (which only varies when the data changes) and the calendar data in iCalendar format. + */ + function DoCalendarQuery( $filter, $url = '' ) { + + if ( !empty($url) ) $this->SetCalendar($url); + + $this->body = << + + + + + $filter + +EOXML; + + $this->requestMethod = "REPORT"; + $this->SetContentType("text/xml"); + $this->DoRequest( $this->calendar_url ); + + $report = array(); + foreach( $this->xmlnodes as $k => $v ) { + switch( $v['tag'] ) { + case 'DAV::response': + if ( $v['type'] == 'open' ) { + $response = array(); + } + elseif ( $v['type'] == 'close' ) { + $report[] = $response; + } + break; + case 'DAV::href': + $response['href'] = basename( rawurldecode($v['value']) ); + break; + case 'DAV::getetag': + $response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']); + break; + case 'urn:ietf:params:xml:ns:caldav:calendar-data': + $response['data'] = $v['value']; + break; + } + } + return $report; + } + + + /** + * Get the events in a range from $start to $finish. The dates should be in the + * format yyyymmddThhmmssZ and should be in GMT. The events are returned as an + * array of event arrays. Each event array will have a 'href', 'etag' and 'event' + * part, where the 'href' is relative to the calendar and the event contains the + * definition of the event in iCalendar format. + * + * @param timestamp $start The start time for the period + * @param timestamp $finish The finish time for the period + * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''. + * + * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery() + */ + function GetEvents( $start = null, $finish = null, $relative_url = null ) { + $filter = ""; + if ( isset($start) && isset($finish) ) + $range = ""; + else + $range = ''; + + $filter = << + + + $range + + + +EOFILTER; + + return $this->DoCalendarQuery($filter, $relative_url); + } + + + /** + * Get the todo's in a range from $start to $finish. The dates should be in the + * format yyyymmddThhmmssZ and should be in GMT. The events are returned as an + * array of event arrays. Each event array will have a 'href', 'etag' and 'event' + * part, where the 'href' is relative to the calendar and the event contains the + * definition of the event in iCalendar format. + * + * @param timestamp $start The start time for the period + * @param timestamp $finish The finish time for the period + * @param boolean $completed Whether to include completed tasks + * @param boolean $cancelled Whether to include cancelled tasks + * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''. + * + * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery() + */ + function GetTodos( $start, $finish, $completed = false, $cancelled = false, $relative_url = null ) { + + if ( $start && $finish ) { +$time_range = << +EOTIME; + } + else { + $time_range = ""; + } + + // Warning! May contain traces of double negatives... + $neg_cancelled = ( $cancelled === true ? "no" : "yes" ); + $neg_completed = ( $cancelled === true ? "no" : "yes" ); + + $filter = << + + + + COMPLETED + + + CANCELLED + $time_range + + + +EOFILTER; + + return $this->DoCalendarQuery($filter, $relative_url); + } + + + /** + * Get the calendar entry by UID + * + * @param uid + * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''. + * @param string $component_type The component type inside the VCALENDAR. Default 'VEVENT'. + * + * @return array An array of the relative URL, etag, and calendar data returned from DoCalendarQuery() @see DoCalendarQuery() + */ + function GetEntryByUid( $uid, $relative_url = null, $component_type = 'VEVENT' ) { + $filter = ""; + if ( $uid ) { + $filter = << + + + + $uid + + + + +EOFILTER; + } + + return $this->DoCalendarQuery($filter, $relative_url); + } + + + /** + * Get the calendar entry by HREF + * + * @param string $href The href from a call to GetEvents or GetTodos etc. + * + * @return string The iCalendar of the calendar entry + */ + function GetEntryByHref( $href ) { + $href = str_replace( rawurlencode('/'),'/',rawurlencode($href)); + return $this->DoGETRequest( $href ); + } + +} + +/** +* Usage example +* +* $cal = new CalDAVClient( "http://calendar.example.com/caldav.php/username/calendar/", "username", "password", "calendar" ); +* $options = $cal->DoOptionsRequest(); +* if ( isset($options["PROPFIND"]) ) { +* // Fetch some information about the events in that calendar +* $cal->SetDepth(1); +* $folder_xml = $cal->DoXMLRequest("PROPFIND", '' ); +* } +* // Fetch all events for February +* $events = $cal->GetEvents("20070101T000000Z","20070201T000000Z"); +* foreach ( $events AS $k => $event ) { +* do_something_with_event_data( $event['data'] ); +* } +* $acc = array(); +* $acc["google"] = array( +* "user"=>"kunsttherapie@gmail.com", +* "pass"=>"xxxxx", +* "server"=>"ssl://www.google.com", +* "port"=>"443", +* "uri"=>"https://www.google.com/calendar/dav/kunsttherapie@gmail.com/events/", +* ); +* +* $acc["davical"] = array( +* "user"=>"some_user", +* "pass"=>"big secret", +* "server"=>"calendar.foo.bar", +* "port"=>"80", +* "uri"=>"http://calendar.foo.bar/caldav.php/some_user/home/", +* ); +* //******************************* +* +* $account = $acc["davical"]; +* +* //******************************* +* $cal = new CalDAVClient( $account["uri"], $account["user"], $account["pass"], "", $account["server"], $account["port"] ); +* $options = $cal->DoOptionsRequest(); +* print_r($options); +* +* //******************************* +* //******************************* +* +* $xmlC = << +* +* +* +* +* +* +* +* +* PROPP; +* //if ( isset($options["PROPFIND"]) ) { +* // Fetch some information about the events in that calendar +* // $cal->SetDepth(1); +* // $folder_xml = $cal->DoXMLRequest("PROPFIND", $xmlC); +* // print_r( $folder_xml); +* //} +* +* // Fetch all events for February +* $events = $cal->GetEvents("20090201T000000Z","20090301T000000Z"); +* foreach ( $events as $k => $event ) { +* print_r($event['data']); +* print "\n---------------------------------------------\n"; +* } +* +* //******************************* +* //******************************* +*/ diff --git a/sources/include/iCalendar.php b/sources/include/iCalendar.php new file mode 100644 index 0000000..3a66c5a --- /dev/null +++ b/sources/include/iCalendar.php @@ -0,0 +1,1770 @@ +component will point to the wrapping VCALENDAR component of +* the iCalendar. This will be fine for simple iCalendar usage as sampled below, +* but more complex iCalendar such as a VEVENT with RRULE which has repeat overrides +* will need quite a bit more thought to process correctly. +* +* @example +* To create a new iCalendar from several data values: +* $ical = new iCalendar( array('DTSTART' => $dtstart, 'SUMMARY' => $summary, 'DURATION' => $duration ) ); +* +* @example +* To render it as an iCalendar string: +* echo $ical->Render(); +* +* @example +* To render just the VEVENTs in the iCalendar with a restricted list of properties: +* echo $ical->Render( false, 'VEVENT', array( 'DTSTART', 'DURATION', 'DTEND', 'RRULE', 'SUMMARY') ); +* +* @example +* To parse an existing iCalendar string for manipulation: +* $ical = new iCalendar( array('icalendar' => $icalendar_text ) ); +* +* @example +* To clear any 'VALARM' components in an iCalendar object +* $ical->component->ClearComponents('VALARM'); +* +* @example +* To replace any 'RRULE' property in an iCalendar object +* $ical->component->SetProperties( 'RRULE', $rrule_definition ); +* +* @package awl +* @subpackage iCalendar +* @author Andrew McMillan +* @copyright Catalyst IT Ltd, Morphoss Ltd +* @license http://gnu.org/copyleft/gpl.html GNU GPL v2 or later +* +*/ +require_once('XMLElement.php'); +/* Commented out, only needed by deprecated functions +require_once('AwlQuery.php'); +*/ + +/** +* A Class for representing properties within an iCalendar +* +* @package awl +*/ +class iCalProp { + /**#@+ + * @access private + */ + + /** + * The name of this property + * + * @var string + */ + var $name; + + /** + * An array of parameters to this property, represented as key/value pairs. + * + * @var array + */ + var $parameters; + + /** + * The value of this property. + * + * @var string + */ + var $content; + + /** + * The original value that this was parsed from, if that's the way it happened. + * + * @var string + */ + var $rendered; + + /**#@-*/ + + /** + * The constructor parses the incoming string, which is formatted as per RFC2445 as a + * propname[;param1=pval1[; ... ]]:propvalue + * however we allow ourselves to assume that the RFC2445 content unescaping has already + * happened when iCalComponent::ParseFrom() called iCalComponent::UnwrapComponent(). + * + * @param string $propstring The string from the iCalendar which contains this property. + */ + function iCalProp( $propstring = null ) { + $this->name = ""; + $this->content = ""; + $this->parameters = array(); + unset($this->rendered); + if ( $propstring != null && gettype($propstring) == 'string' ) { + $this->ParseFrom($propstring); + } + } + + + /** + * The constructor parses the incoming string, which is formatted as per RFC2445 as a + * propname[;param1=pval1[; ... ]]:propvalue + * however we allow ourselves to assume that the RFC2445 content unescaping has already + * happened when iCalComponent::ParseFrom() called iCalComponent::UnwrapComponent(). + * + * @param string $propstring The string from the iCalendar which contains this property. + */ + function ParseFrom( $propstring ) { + $this->rendered = (strlen($propstring) < 72 ? $propstring : null); // Only pre-rendered if we didn't unescape it + + $unescaped = preg_replace( '{\\\\[nN]}', "\n", $propstring); + + // Split into two parts on : which is not preceded by a \ + list( $start, $values) = preg_split( '{(?content = preg_replace( "/\\\\([,;:\"\\\\])/", '$1', $values); + + // Split on ; which is not preceded by a \ + $parameters = preg_split( '{(?name = array_shift( $parameters ); + $this->parameters = array(); + foreach( $parameters AS $k => $v ) { + $pos = strpos($v,'='); + $name = substr( $v, 0, $pos); + $value = substr( $v, $pos + 1); + $this->parameters[$name] = $value; + } +// dbg_error_log('iCalendar', " iCalProp::ParseFrom found '%s' = '%s' with %d parameters", $this->name, substr($this->content,0,200), count($this->parameters) ); + } + + + /** + * Get/Set name property + * + * @param string $newname [optional] A new name for the property + * + * @return string The name for the property. + */ + function Name( $newname = null ) { + if ( $newname != null ) { + $this->name = $newname; + if ( isset($this->rendered) ) unset($this->rendered); +// dbg_error_log('iCalendar', " iCalProp::Name(%s)", $this->name ); + } + return $this->name; + } + + + /** + * Get/Set the content of the property + * + * @param string $newvalue [optional] A new value for the property + * + * @return string The value of the property. + */ + function Value( $newvalue = null ) { + if ( $newvalue != null ) { + $this->content = $newvalue; + if ( isset($this->rendered) ) unset($this->rendered); + } + return $this->content; + } + + + /** + * Get/Set parameters in their entirety + * + * @param array $newparams An array of new parameter key/value pairs + * + * @return array The current array of parameters for the property. + */ + function Parameters( $newparams = null ) { + if ( $newparams != null ) { + $this->parameters = $newparams; + if ( isset($this->rendered) ) unset($this->rendered); + } + return $this->parameters; + } + + + /** + * Test if our value contains a string + * + * @param string $search The needle which we shall search the haystack for. + * + * @return string The name for the property. + */ + function TextMatch( $search ) { + if ( isset($this->content) ) { + return (stristr( $this->content, $search ) !== false); + } + return false; + } + + + /** + * Get the value of a parameter + * + * @param string $name The name of the parameter to retrieve the value for + * + * @return string The value of the parameter + */ + function GetParameterValue( $name ) { + if ( isset($this->parameters[$name]) ) return $this->parameters[$name]; + } + + /** + * Set the value of a parameter + * + * @param string $name The name of the parameter to set the value for + * + * @param string $value The value of the parameter + */ + function SetParameterValue( $name, $value ) { + if ( isset($this->rendered) ) unset($this->rendered); + $this->parameters[$name] = $value; + } + + /** + * Render the set of parameters as key1=value1[;key2=value2[; ...]] with + * any colons or semicolons escaped. + */ + function RenderParameters() { + $rendered = ""; + foreach( $this->parameters AS $k => $v ) { + $escaped = preg_replace( "/([;:])/", '\\\\$1', $v); + $rendered .= sprintf( ";%s=%s", $k, $escaped ); + } + return $rendered; + } + + + /** + * Render a suitably escaped RFC2445 content string. + */ + function Render() { + // If we still have the string it was parsed in from, it hasn't been screwed with + // and we can just return that without modification. + if ( isset($this->rendered) ) return $this->rendered; + + $property = preg_replace( '/[;].*$/', '', $this->name ); + $escaped = $this->content; + switch( $property ) { + /** Content escaping does not apply to these properties culled from RFC2445 */ + case 'ATTACH': case 'GEO': case 'PERCENT-COMPLETE': case 'PRIORITY': + case 'DURATION': case 'FREEBUSY': case 'TZOFFSETFROM': case 'TZOFFSETTO': + case 'TZURL': case 'ATTENDEE': case 'ORGANIZER': case 'RECURRENCE-ID': + case 'URL': case 'EXRULE': case 'SEQUENCE': case 'CREATED': + case 'RRULE': case 'REPEAT': case 'TRIGGER': + break; + + case 'COMPLETED': case 'DTEND': + case 'DUE': case 'DTSTART': + case 'DTSTAMP': case 'LAST-MODIFIED': + case 'CREATED': case 'EXDATE': + case 'RDATE': + if ( isset($this->parameters['VALUE']) && $this->parameters['VALUE'] == 'DATE' ) { + $escaped = substr( $escaped, 0, 8); + } + break; + + /** Content escaping applies by default to other properties */ + default: + $escaped = str_replace( '\\', '\\\\', $escaped); + $escaped = preg_replace( '/\r?\n/', '\\n', $escaped); + $escaped = preg_replace( "/([,;])/", '\\\\$1', $escaped); + } + $property = sprintf( "%s%s:", $this->name, $this->RenderParameters() ); + if ( (strlen($property) + strlen($escaped)) <= 72 ) { + $this->rendered = $property . $escaped; + } + else if ( (strlen($property) + strlen($escaped)) > 72 && (strlen($property) < 72) && (strlen($escaped) < 72) ) { + $this->rendered = $property . "\r\n " . $escaped; + } + else { + $this->rendered = preg_replace( '/(.{72})/u', '$1'."\r\n ", $property . $escaped ); + } + return $this->rendered; + } + +} + + +/** +* A Class for representing components within an iCalendar +* +* @package awl +*/ +class iCalComponent { + /**#@+ + * @access private + */ + + /** + * The type of this component, such as 'VEVENT', 'VTODO', 'VTIMEZONE', etc. + * + * @var string + */ + var $type; + + /** + * An array of properties, which are iCalProp objects + * + * @var array + */ + var $properties; + + /** + * An array of (sub-)components, which are iCalComponent objects + * + * @var array + */ + var $components; + + /** + * The rendered result (or what was originally parsed, if there have been no changes) + * + * @var array + */ + var $rendered; + + /**#@-*/ + + /** + * A basic constructor + */ + function iCalComponent( $content = null ) { + $this->type = ""; + $this->properties = array(); + $this->components = array(); + $this->rendered = ""; + if ( $content != null && (gettype($content) == 'string' || gettype($content) == 'array') ) { + $this->ParseFrom($content); + } + } + + + /** + * Apply standard properties for a VCalendar + * @param array $extra_properties Key/value pairs of additional properties + */ + function VCalendar( $extra_properties = null ) { + $this->SetType('VCALENDAR'); + $this->AddProperty('PRODID', '-//davical.org//NONSGML AWL Calendar//EN'); + $this->AddProperty('VERSION', '2.0'); + $this->AddProperty('CALSCALE', 'GREGORIAN'); + if ( is_array($extra_properties) ) { + foreach( $extra_properties AS $k => $v ) { + $this->AddProperty($k,$v); + } + } + } + + /** + * Collect an array of all parameters of our properties which are the specified type + * Mainly used for collecting the full variety of references TZIDs + */ + function CollectParameterValues( $parameter_name ) { + $values = array(); + foreach( $this->components AS $k => $v ) { + $also = $v->CollectParameterValues($parameter_name); + $values = array_merge( $values, $also ); + } + foreach( $this->properties AS $k => $v ) { + $also = $v->GetParameterValue($parameter_name); + if ( isset($also) && $also != "" ) { +// dbg_error_log( 'iCalendar', "::CollectParameterValues(%s) : Found '%s'", $parameter_name, $also); + $values[$also] = 1; + } + } + return $values; + } + + + /** + * Parse the text $content into sets of iCalProp & iCalComponent within this iCalComponent + * @param string $content The raw RFC2445-compliant iCalendar component, including BEGIN:TYPE & END:TYPE + */ + function ParseFrom( $content ) { + $this->rendered = $content; + $content = $this->UnwrapComponent($content); + + $type = false; + $subtype = false; + $finish = null; + $subfinish = null; + + $length = strlen($content); + $linefrom = 0; + while( $linefrom < $length ) { + $lineto = strpos( $content, "\n", $linefrom ); + if ( $lineto === false ) { + $lineto = strpos( $content, "\r", $linefrom ); + } + if ( $lineto > 0 ) { + $line = substr( $content, $linefrom, $lineto - $linefrom); + $linefrom = $lineto + 1; + } + else { + $line = substr( $content, $linefrom ); + $linefrom = $length; + } + if ( preg_match('/^\s*$/', $line ) ) continue; + $line = rtrim( $line, "\r\n" ); +// dbg_error_log( 'iCalendar', "::ParseFrom: Parsing line: $line"); + + if ( $type === false ) { + if ( preg_match( '/^BEGIN:(.+)$/', $line, $matches ) ) { + // We have found the start of the main component + $type = $matches[1]; + $finish = "END:$type"; + $this->type = $type; + dbg_error_log( 'iCalendar', "::ParseFrom: Start component of type '%s'", $type); + } + else { + dbg_error_log( 'iCalendar', "::ParseFrom: Ignoring crap before start of component: $line"); + // unset($lines[$k]); // The content has crap before the start + if ( $line != "" ) $this->rendered = null; + } + } + else if ( $type == null ) { + dbg_error_log( 'iCalendar', "::ParseFrom: Ignoring crap after end of component"); + if ( $line != "" ) $this->rendered = null; + } + else if ( $line == $finish ) { + dbg_error_log( 'iCalendar', "::ParseFrom: End of component"); + $type = null; // We have reached the end of our component + } + else { + if ( $subtype === false && preg_match( '/^BEGIN:(.+)$/', $line, $matches ) ) { + // We have found the start of a sub-component + $subtype = $matches[1]; + $subfinish = "END:$subtype"; + $subcomponent = $line . "\r\n"; + dbg_error_log( 'iCalendar', "::ParseFrom: Found a subcomponent '%s'", $subtype); + } + else if ( $subtype ) { + // We are inside a sub-component + $subcomponent .= $this->WrapComponent($line); + if ( $line == $subfinish ) { + dbg_error_log( 'iCalendar', "::ParseFrom: End of subcomponent '%s'", $subtype); + // We have found the end of a sub-component + $this->components[] = new iCalComponent($subcomponent); + $subtype = false; + } +// else +// dbg_error_log( 'iCalendar', "::ParseFrom: Inside a subcomponent '%s'", $subtype ); + } + else { +// dbg_error_log( 'iCalendar', "::ParseFrom: Parse property of component"); + // It must be a normal property line within a component. + $this->properties[] = new iCalProp($line); + } + } + } + } + + + /** + * This unescapes the (CRLF + linear space) wrapping specified in RFC2445. According + * to RFC2445 we should always end with CRLF but the CalDAV spec says that normalising + * XML parsers often muck with it and may remove the CR. We accept either case. + */ + function UnwrapComponent( $content ) { + return preg_replace('/\r?\n[ \t]/', '', $content ); + } + + /** + * This imposes the (CRLF + linear space) wrapping specified in RFC2445. According + * to RFC2445 we should always end with CRLF but the CalDAV spec says that normalising + * XML parsers often muck with it and may remove the CR. We output RFC2445 compliance. + * + * In order to preserve pre-existing wrapping in the component, we split the incoming + * string on line breaks before running wordwrap over each component of that. + */ + function WrapComponent( $content ) { + $strs = preg_split( "/\r?\n/", $content ); + $wrapped = ""; + foreach ($strs as $str) { + $wrapped .= preg_replace( '/(.{72})/u', '$1'."\r\n ", $str ) ."\r\n"; + } + return $wrapped; + } + + /** + * Return the type of component which this is + */ + function GetType() { + return $this->type; + } + + + /** + * Set the type of component which this is + */ + function SetType( $type ) { + if ( isset($this->rendered) ) unset($this->rendered); + $this->type = $type; + return $this->type; + } + + + /** + * Get all properties, or the properties matching a particular type + */ + function GetProperties( $type = null ) { + $properties = array(); + foreach( $this->properties AS $k => $v ) { + if ( $type == null || $v->Name() == $type ) { + $properties[$k] = $v; + } + } + return $properties; + } + + + /** + * Get the value of the first property matching the name. Obviously this isn't + * so useful for properties which may occur multiply, but most don't. + * + * @param string $type The type of property we are after. + * @return string The value of the property, or null if there was no such property. + */ + function GetPValue( $type ) { + foreach( $this->properties AS $k => $v ) { + if ( $v->Name() == $type ) return $v->Value(); + } + return null; + } + + + /** + * Set the value of all properties matching the name. + * + * @param string $type The type/name of property we are after + * @param string $value The value of the property + */ + function SetPValue( $type, $value ) { + for ( $i = 0; $i < count($this->properties); $i++ ) { + if ( $this->properties[$i]->Name() == $type ) { + if ( isset($this->rendered) ) unset($this->rendered); + $this->properties[$i]->Value($value); + } + } + } + + + /** + * Set the value of all the parameters matching the name. Component -> Property -> Parameter + * + * @param string $component_type Type of the component + * @param string $property_name Type/Name of the property + * @param string $parameter_name Type/Name of the parameter + * @param string $value New value of the parameter + */ + function SetCPParameterValue( $component_type, $property_name, $parameter_name, $value ) { + for ( $j = 0; $j < count($this->components); $j++ ) { + if ( $this->components[$j]->GetType() == $component_type ) { + for ( $i = 0; $i < count($this->components[$j]->properties); $i++ ) { + if ( $this->components[$j]->properties[$i]->Name() == $property_name ) { + if ( isset($this->components[$j]->rendered) ) unset($this->components[$j]->rendered); + $this->components[$j]->properties[$i]->SetParameterValue($parameter_name, $value); + } + } + } + } + } + + + /** + * Get the value of the specified parameter for the first property matching the + * name. Obviously this isn't so useful for properties which may occur multiply, but most don't. + * + * @param string $type The type of property we are after. + * @param string $type The name of the parameter we are after. + * @return string The value of the parameter for the property, or null in the case that there was no such property, or no such parameter. + */ + function GetPParamValue( $type, $parameter_name ) { + foreach( $this->properties AS $k => $v ) { + if ( $v->Name() == $type ) return $v->GetParameterValue($parameter_name); + } + return null; + } + + + /** + * Clear all properties, or the properties matching a particular type + * @param string $type The type of property - omit for all properties + */ + function ClearProperties( $type = null ) { + if ( $type != null ) { + // First remove all the existing ones of that type + foreach( $this->properties AS $k => $v ) { + if ( $v->Name() == $type ) { + unset($this->properties[$k]); + if ( isset($this->rendered) ) unset($this->rendered); + } + } + $this->properties = array_values($this->properties); + } + else { + if ( isset($this->rendered) ) unset($this->rendered); + $this->properties = array(); + } + } + + + /** + * Set all properties, or the ones matching a particular type + */ + function SetProperties( $new_properties, $type = null ) { + if ( isset($this->rendered) && count($new_properties) > 0 ) unset($this->rendered); + $this->ClearProperties($type); + foreach( $new_properties AS $k => $v ) { + $this->AddProperty($v); + } + } + + + /** + * Adds a new property + * + * @param iCalProp $new_property The new property to append to the set, or a string with the name + * @param string $value The value of the new property (default: param 1 is an iCalProp with everything + * @param array $parameters The key/value parameter pairs (default: none, or param 1 is an iCalProp with everything) + */ + function AddProperty( $new_property, $value = null, $parameters = null ) { + if ( isset($this->rendered) ) unset($this->rendered); + if ( isset($value) && gettype($new_property) == 'string' ) { + $new_prop = new iCalProp(); + $new_prop->Name($new_property); + $new_prop->Value($value); + if ( $parameters != null ) $new_prop->Parameters($parameters); + dbg_error_log('iCalendar'," Adding new property '%s'", $new_prop->Render() ); + $this->properties[] = $new_prop; + } + else if ( gettype($new_property) ) { + $this->properties[] = $new_property; + } + } + + + /** + * Get all sub-components, or at least get those matching a type + * @return array an array of the sub-components + */ + function &FirstNonTimezone( $type = null ) { + foreach( $this->components AS $k => $v ) { + if ( $v->GetType() != 'VTIMEZONE' ) return $this->components[$k]; + } + $result = false; + return $result; + } + + + /** + * Return true if the person identified by the email address is down as an + * organizer for this meeting. + * @param string $email The e-mail address of the person we're seeking. + * @return boolean true if we found 'em, false if we didn't. + */ + function IsOrganizer( $email ) { + if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:$email'; + $props = $this->GetPropertiesByPath('!VTIMEZONE/ORGANIZER'); + foreach( $props AS $k => $prop ) { + if ( $prop->Value() == $email ) return true; + } + return false; + } + + + /** + * Return true if the person identified by the email address is down as an + * attendee or organizer for this meeting. + * @param string $email The e-mail address of the person we're seeking. + * @return boolean true if we found 'em, false if we didn't. + */ + function IsAttendee( $email ) { + if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:$email'; + if ( $this->IsOrganizer($email) ) return true; /** an organizer is an attendee, as far as we're concerned */ + $props = $this->GetPropertiesByPath('!VTIMEZONE/ATTENDEE'); + foreach( $props AS $k => $prop ) { + if ( $prop->Value() == $email ) return true; + } + return false; + } + + + /** + * Get all sub-components, or at least get those matching a type, or failling to match, + * should the second parameter be set to false. + * + * @param string $type The type to match (default: All) + * @param boolean $normal_match Set to false to invert the match (default: true) + * @return array an array of the sub-components + */ + function GetComponents( $type = null, $normal_match = true ) { + $components = $this->components; + if ( $type != null ) { + foreach( $components AS $k => $v ) { + if ( ($v->GetType() != $type) === $normal_match ) { + unset($components[$k]); + } + } + $components = array_values($components); + } + return $components; + } + + + /** + * Clear all components, or the components matching a particular type + * @param string $type The type of component - omit for all components + */ + function ClearComponents( $type = null ) { + if ( $type != null ) { + // First remove all the existing ones of that type + foreach( $this->components AS $k => $v ) { + if ( $v->GetType() == $type ) { + unset($this->components[$k]); + if ( isset($this->rendered) ) unset($this->rendered); + } + else { + if ( ! $this->components[$k]->ClearComponents($type) ) { + if ( isset($this->rendered) ) unset($this->rendered); + } + } + } + return isset($this->rendered); + } + else { + if ( isset($this->rendered) ) unset($this->rendered); + $this->components = array(); + } + } + + + /** + * Sets some or all sub-components of the component to the supplied new components + * + * @param array of iCalComponent $new_components The new components to replace the existing ones + * @param string $type The type of components to be replaced. Defaults to null, which means all components will be replaced. + */ + function SetComponents( $new_component, $type = null ) { + if ( isset($this->rendered) ) unset($this->rendered); + if ( count($new_component) > 0 ) $this->ClearComponents($type); + foreach( $new_component AS $k => $v ) { + $this->components[] = $v; + } + } + + + /** + * Adds a new subcomponent + * + * @param iCalComponent $new_component The new component to append to the set + */ + function AddComponent( $new_component ) { + if ( is_array($new_component) && count($new_component) == 0 ) return; + if ( isset($this->rendered) ) unset($this->rendered); + if ( is_array($new_component) ) { + foreach( $new_component AS $k => $v ) { + $this->components[] = $v; + } + } + else { + $this->components[] = $new_component; + } + } + + + /** + * Mask components, removing any that are not of the types in the list + * @param array $keep An array of component types to be kept + */ + function MaskComponents( $keep ) { + foreach( $this->components AS $k => $v ) { + if ( ! in_array( $v->GetType(), $keep ) ) { + unset($this->components[$k]); + if ( isset($this->rendered) ) unset($this->rendered); + } + else { + $v->MaskComponents($keep); + } + } + } + + + /** + * Mask properties, removing any that are not in the list + * @param array $keep An array of property names to be kept + * @param array $component_list An array of component types to check within + */ + function MaskProperties( $keep, $component_list=null ) { + foreach( $this->components AS $k => $v ) { + $v->MaskProperties($keep, $component_list); + } + + if ( !isset($component_list) || in_array($this->GetType(),$component_list) ) { + foreach( $this->components AS $k => $v ) { + if ( ! in_array( $v->GetType(), $keep ) ) { + unset($this->components[$k]); + if ( isset($this->rendered) ) unset($this->rendered); + } + } + } + } + + + /** + * Clone this component (and subcomponents) into a confidential version of it. A confidential + * event will be scrubbed of any identifying characteristics other than time/date, repeat, uid + * and a summary which is just a translated 'Busy'. + */ + function CloneConfidential() { + $confidential = clone($this); + $keep_properties = array( 'DTSTAMP', 'DTSTART', 'RRULE', 'DURATION', 'DTEND', 'DUE', 'UID', 'CLASS', 'TRANSP', 'CREATED', 'LAST-MODIFIED' ); + $resource_components = array( 'VEVENT', 'VTODO', 'VJOURNAL' ); + $confidential->MaskComponents(array( 'VTIMEZONE', 'VEVENT', 'VTODO', 'VJOURNAL' )); + $confidential->MaskProperties($keep_properties, $resource_components ); + if ( in_array( $confidential->GetType(), $resource_components ) ) { + $confidential->AddProperty( 'SUMMARY', translate('Busy') ); + } + foreach( $confidential->components AS $k => $v ) { + if ( in_array( $v->GetType(), $resource_components ) ) { + $v->AddProperty( 'SUMMARY', translate('Busy') ); + } + } + + return $confidential; + } + + + /** + * Renders the component, possibly restricted to only the listed properties + */ + function Render( $restricted_properties = null) { + + $unrestricted = (!isset($restricted_properties) || count($restricted_properties) == 0); + + if ( isset($this->rendered) && $unrestricted ) + return $this->rendered; + + $rendered = "BEGIN:$this->type\r\n"; + foreach( $this->properties AS $k => $v ) { + if ( method_exists($v, 'Render') ) { + if ( $unrestricted || isset($restricted_properties[$v]) ) $rendered .= $v->Render() . "\r\n"; + } + } + foreach( $this->components AS $v ) { $rendered .= $v->Render(); } + $rendered .= "END:$this->type\r\n"; + + $rendered = preg_replace('{(?rendered = $rendered; + + return $rendered; + } + + + /** + * Return an array of properties matching the specified path + * + * @return array An array of iCalProp within the tree which match the path given, in the form + * [/]COMPONENT[/...]/PROPERTY in a syntax kind of similar to our poor man's XML queries. We + * also allow COMPONENT and PROPERTY to be !COMPONENT and !PROPERTY for ++fun. + * + * @note At some point post PHP4 this could be re-done with an iterator, which should be more efficient for common use cases. + */ + function GetPropertiesByPath( $path ) { + $properties = array(); + dbg_error_log( 'iCalendar', "GetPropertiesByPath: Querying within '%s' for path '%s'", $this->type, $path ); + if ( !preg_match( '#(/?)(!?)([^/]+)(/?.*)$#', $path, $matches ) ) return $properties; + + $adrift = ($matches[1] == ''); + $normal = ($matches[2] == ''); + $ourtest = $matches[3]; + $therest = $matches[4]; + dbg_error_log( 'iCalendar', "GetPropertiesByPath: Matches: %s -- %s -- %s -- %s\n", $matches[1], $matches[2], $matches[3], $matches[4] ); + if ( $ourtest == '*' || (($ourtest == $this->type) === $normal) && $therest != '' ) { + if ( preg_match( '#^/(!?)([^/]+)$#', $therest, $matches ) ) { + $normmatch = ($matches[1] ==''); + $proptest = $matches[2]; + foreach( $this->properties AS $k => $v ) { + if ( $proptest == '*' || (($v->Name() == $proptest) === $normmatch ) ) { + $properties[] = $v; + } + } + } + else { + /** + * There is more to the path, so we recurse into that sub-part + */ + foreach( $this->components AS $k => $v ) { + $properties = array_merge( $properties, $v->GetPropertiesByPath($therest) ); + } + } + } + + if ( $adrift ) { + /** + * Our input $path was not rooted, so we recurse further + */ + foreach( $this->components AS $k => $v ) { + $properties = array_merge( $properties, $v->GetPropertiesByPath($path) ); + } + } + dbg_error_log('iCalendar', "GetPropertiesByPath: Found %d within '%s' for path '%s'\n", count($properties), $this->type, $path ); + return $properties; + } + +} + +/** +************************************************************************************ +* Everything below here is deprecated and should be avoided in favour +* of using, improving and enhancing the more sensible structures above. +************************************************************************************ +*/ + +/** +* A Class for handling Events on a calendar (DEPRECATED) +* +* @package awl +*/ +class iCalendar { // DEPRECATED + /**#@+ + * @access private + */ + + /** + * The component-ised version of the iCalendar + * @var component iCalComponent + */ + var $component; + + /** + * An array of arbitrary properties, containing arbitrary arrays of arbitrary properties + * @var properties array + */ + var $properties; + + /** + * An array of the lines of this iCalendar resource + * @var lines array + */ + var $lines; + + /** + * The typical location name for the standard timezone such as "Pacific/Auckland" + * @var tz_locn string + */ + var $tz_locn; + + /** + * The type of iCalendar data VEVENT/VTODO/VJOURNAL + * @var type string + */ + var $type; + + /**#@-*/ + + /** + * @DEPRECATED: This class will be removed soon. + * The constructor takes an array of args. If there is an element called 'icalendar' + * then that will be parsed into the iCalendar object. Otherwise the array elements + * are converted into properties of the iCalendar object directly. + */ + function iCalendar( $args ) { + global $c; + + deprecated('iCalendar::iCalendar'); + $this->tz_locn = ""; + if ( !isset($args) || !(is_array($args) || is_object($args)) ) return; + if ( is_object($args) ) { + settype($args,'array'); + } + + $this->component = new iCalComponent(); + if ( isset($args['icalendar']) ) { + $this->component->ParseFrom($args['icalendar']); + $this->lines = preg_split('/\r?\n/', $args['icalendar'] ); + $this->SaveTimeZones(); + $first =& $this->component->FirstNonTimezone(); + if ( $first ) { + $this->type = $first->GetType(); + $this->properties = $first->GetProperties(); + } + else { + $this->properties = array(); + } + $this->properties['VCALENDAR'] = array('***ERROR*** This class is being referenced in an unsupported way!'); + return; + } + + if ( isset($args['type'] ) ) { + $this->type = $args['type']; + unset( $args['type'] ); + } + else { + $this->type = 'VEVENT'; // Default to event + } + $this->component->SetType('VCALENDAR'); + $this->component->SetProperties( + array( + new iCalProp('PRODID:-//davical.org//NONSGML AWL Calendar//EN'), + new iCalProp('VERSION:2.0'), + new iCalProp('CALSCALE:GREGORIAN') + ) + ); + $first = new iCalComponent(); + $first->SetType($this->type); + $this->properties = array(); + + foreach( $args AS $k => $v ) { + dbg_error_log( 'iCalendar', ":Initialise: %s to >>>%s<<<", $k, $v ); + $property = new iCalProp(); + $property->Name($k); + $property->Value($v); + $this->properties[] = $property; + } + $first->SetProperties($this->properties); + $this->component->SetComponents( array($first) ); + + $this->properties['VCALENDAR'] = array('***ERROR*** This class is being referenced in an unsupported way!'); + + /** + * @todo Need to handle timezones!!! + */ + if ( $this->tz_locn == "" ) { + $this->tz_locn = $this->Get("tzid"); + if ( (!isset($this->tz_locn) || $this->tz_locn == "") && isset($c->local_tzid) ) { + $this->tz_locn = $c->local_tzid; + } + } + } + + + /** + * @DEPRECATED: This class will be removed soon. + * Save any timezones by TZID in the PostgreSQL database for future re-use. + */ + function SaveTimeZones() { + global $c; + + deprecated('iCalendar::SaveTimeZones'); + $this->tzid_list = array_keys($this->component->CollectParameterValues('TZID')); + if ( ! isset($this->tzid) && count($this->tzid_list) > 0 ) { + dbg_error_log( 'iCalendar', "::TZID_List[0] = '%s', count=%d", $this->tzid_list[0], count($this->tzid_list) ); + $this->tzid = $this->tzid_list[0]; + } + + $timezones = $this->component->GetComponents('VTIMEZONE'); + if ( $timezones === false || count($timezones) == 0 ) return; + $this->vtimezone = $timezones[0]->Render(); // Backward compatibility + + $tzid = $this->Get('TZID'); + if ( isset($c->save_time_zone_defs) && $c->save_time_zone_defs ) { + foreach( $timezones AS $k => $tz ) { + $tzid = $tz->GetPValue('TZID'); + + $qry = new AwlQuery( "SELECT tz_locn FROM time_zone WHERE tz_id = ?;", $tzid ); + if ( $qry->Exec('iCalendar') && $qry->rows() == 1 ) { + $row = $qry->Fetch(); + if ( !isset($first_tzid) ) $first_tzid = $row->tz_locn; + continue; + } + + if ( $tzid != "" && $qry->rows() == 0 ) { + + $tzname = $tz->GetPValue('X-LIC-LOCATION'); + if ( !isset($tzname) ) $tzname = olson_from_tzstring($tzid); + + $qry2 = new AwlQuery( "INSERT INTO time_zone (tz_id, tz_locn, tz_spec) VALUES( ?, ?, ? );", + $tzid, $tzname, $tz->Render() ); + $qry2->Exec('iCalendar'); + } + } + } + if ( ! isset($this->tzid) && isset($first_tzid) ) $this->tzid = $first_tzid; + + if ( (!isset($this->tz_locn) || $this->tz_locn == '') && isset($first_tzid) && $first_tzid != '' ) { + $tzname = preg_replace('#^(.*[^a-z])?([a-z]+/[a-z]+)$#i','$2', $first_tzid ); + if ( preg_match( '#\S+/\S+#', $tzname) ) { + $this->tz_locn = $tzname; + } + dbg_error_log( 'iCalendar', " TZCrap1: TZID '%s', Location '%s', Perhaps: %s", $tzid, $this->tz_locn, $tzname ); + } + + if ( (!isset($this->tz_locn) || $this->tz_locn == "") && isset($c->local_tzid) ) { + $this->tz_locn = $c->local_tzid; + } + if ( ! isset($this->tzid) && isset($this->tz_locn) ) $this->tzid = $this->tz_locn; + } + + + /** + * An array of property names that we should always want when rendering an iCalendar + * + * @DEPRECATED: This class will be removed soon. + * @todo Remove this function. + */ + function DefaultPropertyList() { + dbg_error_log( "LOG", " iCalendar: Call to deprecated method '%s'", 'DefaultPropertyList' ); + return array( "UID" => 1, "DTSTAMP" => 1, "DTSTART" => 1, "DURATION" => 1, + "LAST-MODIFIED" => 1,"CLASS" => 1, "TRANSP" => 1, "SEQUENCE" => 1, + "DUE" => 1, "SUMMARY" => 1, "RRULE" => 1 ); + } + + /** + * A function to extract the contents of a BEGIN:SOMETHING to END:SOMETHING (perhaps multiply) + * and return just that bit (or, of course, those bits :-) + * + * @var string The type of thing(s) we want returned. + * @var integer The number of SOMETHINGS we want to get. + * + * @return string A string from BEGIN:SOMETHING to END:SOMETHING, possibly multiple of these + * + * @DEPRECATED: This class will be removed soon. + * @todo Remove this function. + */ + function JustThisBitPlease( $type, $count=1 ) { + deprecated('iCalendar::JustThisBitPlease' ); + $answer = ""; + $intags = false; + $start = "BEGIN:$type"; + $finish = "END:$type"; + dbg_error_log( 'iCalendar', ":JTBP: Looking for %d subsets of type %s", $count, $type ); + reset($this->lines); + foreach( $this->lines AS $k => $v ) { + if ( !$intags && $v == $start ) { + $answer .= $v . "\n"; + $intags = true; + } + else if ( $intags && $v == $finish ) { + $answer .= $v . "\n"; + $intags = false; + } + else if ( $intags ) { + $answer .= $v . "\n"; + } + } + return $answer; + } + + + /** + * Function to parse lines from BEGIN:SOMETHING to END:SOMETHING into a nested array structure + * + * @var string The "SOMETHING" from the BEGIN:SOMETHING line we just met + * @return arrayref An array of the things we found between (excluding) the BEGIN & END, some of which might be sub-arrays + * + * @DEPRECATED: This class will be removed soon. + * @todo Remove this function. + */ + function &ParseSomeLines( $type ) { + deprecated('iCalendar::ParseSomeLines' ); + $props = array(); + $properties =& $props; + while( isset($this->lines[$this->_current_parse_line]) ) { + $i = $this->_current_parse_line++; + $line =& $this->lines[$i]; + dbg_error_log( 'iCalendar', ":Parse:%s LINE %03d: >>>%s<<<", $type, $i, $line ); + if ( $this->parsing_vtimezone ) { + $this->vtimezone .= $line."\n"; + } + if ( preg_match( '/^(BEGIN|END):([^:]+)$/', $line, $matches ) ) { + if ( $matches[1] == 'END' && $matches[2] == $type ) { + if ( $type == 'VTIMEZONE' ) { + $this->parsing_vtimezone = false; + } + return $properties; + } + else if( $matches[1] == 'END' ) { + dbg_error_log("ERROR"," iCalendar: parse error: Unexpected END:%s when we were looking for END:%s", $matches[2], $type ); + return $properties; + } + else if( $matches[1] == 'BEGIN' ) { + $subtype = $matches[2]; + if ( $subtype == 'VTIMEZONE' ) { + $this->parsing_vtimezone = true; + $this->vtimezone = $line."\n"; + } + if ( !isset($properties['INSIDE']) ) $properties['INSIDE'] = array(); + $properties['INSIDE'][] = $subtype; + if ( !isset($properties[$subtype]) ) $properties[$subtype] = array(); + $properties[$subtype][] = $this->ParseSomeLines($subtype); + } + } + else { + // Parse the property + @list( $property, $value ) = explode(':', $line, 2 ); + if ( strpos( $property, ';' ) > 0 ) { + $parameterlist = explode(';', $property ); + $property = array_shift($parameterlist); + foreach( $parameterlist AS $pk => $pv ) { + if ( $pv == "VALUE=DATE" ) { + $value .= 'T000000'; + } + elseif ( preg_match('/^([^;:=]+)=([^;:=]+)$/', $pv, $matches) ) { + switch( $matches[1] ) { + case 'TZID': $properties['TZID'] = $matches[2]; break; + default: + dbg_error_log( 'iCalendar', " FYI: Ignoring Resource '%s', Property '%s', Parameter '%s', Value '%s'", $type, $property, $matches[1], $matches[2] ); + } + } + } + } + if ( $this->parsing_vtimezone && (!isset($this->tz_locn) || $this->tz_locn == "") && $property == 'X-LIC-LOCATION' ) { + $this->tz_locn = $value; + } + $properties[strtoupper($property)] = $this->RFC2445ContentUnescape($value); + } + } + return $properties; + } + + + /** + * Build the iCalendar object from a text string which is a single iCalendar resource + * + * @var string The RFC2445 iCalendar resource to be parsed + * + * @DEPRECATED: This class will be removed soon. + * @todo Remove this function. + */ + function BuildFromText( $icalendar ) { + deprecated('iCalendar::BuildFromText' ); + /** + * This unescapes the (CRLF + linear space) wrapping specified in RFC2445. According + * to RFC2445 we should always end with CRLF but the CalDAV spec says that normalising + * XML parsers often muck with it and may remove the CR. + */ + $icalendar = preg_replace('/\r?\n[ \t]/', '', $icalendar ); + + $this->lines = preg_split('/\r?\n/', $icalendar ); + + $this->_current_parse_line = 0; + $this->properties = $this->ParseSomeLines(''); + + /** + * Our 'type' is the type of non-timezone inside a VCALENDAR + */ + if ( isset($this->properties['VCALENDAR'][0]['INSIDE']) ) { + foreach ( $this->properties['VCALENDAR'][0]['INSIDE'] AS $k => $v ) { + if ( $v == 'VTIMEZONE' ) continue; + $this->type = $v; + break; + } + } + + } + + + /** + * Returns a content string with the RFC2445 escaping removed + * + * @param string $escaped The incoming string to be escaped. + * @return string The string with RFC2445 content escaping removed. + * + * @DEPRECATED: This class will be removed soon. + * @todo Remove this function. + */ + function RFC2445ContentUnescape( $escaped ) { + deprecated( 'RFC2445ContentUnescape' ); + $unescaped = str_replace( '\\n', "\n", $escaped); + $unescaped = str_replace( '\\N', "\n", $unescaped); + $unescaped = preg_replace( "/\\\\([,;:\"\\\\])/", '$1', $unescaped); + return $unescaped; + } + + + + /** + * Do what must be done with time zones from on file. Attempt to turn + * them into something that PostgreSQL can understand... + * + * @DEPRECATED: This class will be removed soon. + * @todo Remove this function. + */ + function DealWithTimeZones() { + global $c; + + deprecated('iCalendar::DealWithTimeZones' ); + $tzid = $this->Get('TZID'); + if ( isset($c->save_time_zone_defs) && $c->save_time_zone_defs ) { + $qry = new AwlQuery( "SELECT tz_locn FROM time_zone WHERE tz_id = ?;", $tzid ); + if ( $qry->Exec('iCalendar') && $qry->rows() == 1 ) { + $row = $qry->Fetch(); + $this->tz_locn = $row->tz_locn; + } + dbg_error_log( 'iCalendar', " TZCrap2: TZID '%s', DB Rows=%d, Location '%s'", $tzid, $qry->rows(), $this->tz_locn ); + } + + if ( (!isset($this->tz_locn) || $this->tz_locn == '') && $tzid != '' ) { + /** + * In case there was no X-LIC-LOCATION defined, let's hope there is something in the TZID + * that we can use. We are looking for a string like "Pacific/Auckland" if possible. + */ + $tzname = preg_replace('#^(.*[^a-z])?([a-z]+/[a-z]+)$#i','$1',$tzid ); + /** + * Unfortunately this kind of thing will never work well :-( + * + if ( strstr( $tzname, ' ' ) ) { + $words = preg_split('/\s/', $tzname ); + $tzabbr = ''; + foreach( $words AS $i => $word ) { + $tzabbr .= substr( $word, 0, 1); + } + $this->tz_locn = $tzabbr; + } + */ + if ( preg_match( '#\S+/\S+#', $tzname) ) { + $this->tz_locn = $tzname; + } + dbg_error_log( 'iCalendar', " TZCrap3: TZID '%s', Location '%s', Perhaps: %s", $tzid, $this->tz_locn, $tzname ); + } + + if ( $tzid != '' && isset($c->save_time_zone_defs) && $c->save_time_zone_defs && $qry->rows() != 1 && isset($this->vtimezone) && $this->vtimezone != "" ) { + $qry2 = new AwlQuery( "INSERT INTO time_zone (tz_id, tz_locn, tz_spec) VALUES( ?, ?, ? );", + $tzid, $this->tz_locn, $this->vtimezone ); + $qry2->Exec('iCalendar'); + } + + if ( (!isset($this->tz_locn) || $this->tz_locn == "") && isset($c->local_tzid) ) { + $this->tz_locn = $c->local_tzid; + } + } + + + /** + * Get the value of a property in the first non-VTIMEZONE + * @DEPRECATED: This class will be removed soon. + */ + function Get( $key ) { + deprecated('iCalendar::Get' ); + if ( strtoupper($key) == 'TZID' ) { + // backward compatibility hack + dbg_error_log( 'iCalendar', " Get(TZID): TZID '%s', Location '%s'", (isset($this->tzid)?$this->tzid:"[not set]"), $this->tz_locn ); + if ( isset($this->tzid) ) return $this->tzid; + return $this->tz_locn; + } + /** + * The property we work on is the first non-VTIMEZONE we find. + */ + $component =& $this->component->FirstNonTimezone(); + if ( $component === false ) return null; + return $component->GetPValue(strtoupper($key)); + } + + + /** + * Set the value of a property + * @DEPRECATED: This class will be removed soon. + */ + function Set( $key, $value ) { + deprecated('iCalendar::Set' ); + if ( $value == "" ) return; + $key = strtoupper($key); + $property = new iCalProp(); + $property->Name($key); + $property->Value($value); + if (isset($this->component->rendered) ) unset( $this->component->rendered ); + $component =& $this->component->FirstNonTimezone(); + $component->SetProperties( array($property), $key); + return $this->Get($key); + } + + + /** + * @DEPRECATED: This class will be removed soon. + * Add a new property/value, regardless of whether it exists already + * + * @param string $key The property key + * @param string $value The property value + * @param string $parameters Any parameters to set for the property, as an array of key/value pairs + */ + function Add( $key, $value, $parameters = null ) { + deprecated('iCalendar::Add' ); + if ( $value == "" ) return; + $key = strtoupper($key); + $property = new iCalProp(); + $property->Name($key); + $property->Value($value); + if ( isset($parameters) && is_array($parameters) ) { + $property->parameters = $parameters; + } + $component =& $this->component->FirstNonTimezone(); + $component->AddProperty($property); + if (isset($this->component->rendered) ) unset( $this->component->rendered ); + } + + + /** + * @DEPRECATED: This class will be removed soon. + * Get all sub-components, or at least get those matching a type, or failling to match, + * should the second parameter be set to false. + * + * @param string $type The type to match (default: All) + * @param boolean $normal_match Set to false to invert the match (default: true) + * @return array an array of the sub-components + */ + function GetComponents( $type = null, $normal_match = true ) { + deprecated('iCalendar::GetComponents' ); + return $this->component->GetComponents($type,$normal_match); + } + + + /** + * @DEPRECATED: This class will be removed soon. + * Clear all components, or the components matching a particular type + * @param string $type The type of component - omit for all components + */ + function ClearComponents( $type = null ) { + deprecated('iCalendar::ClearComponents' ); + $this->component->ClearComponents($type); + } + + + /** + * @DEPRECATED: This class will be removed soon. + * Sets some or all sub-components of the component to the supplied new components + * + * @param array of iCalComponent $new_components The new components to replace the existing ones + * @param string $type The type of components to be replaced. Defaults to null, which means all components will be replaced. + */ + function SetComponents( $new_component, $type = null ) { + deprecated('iCalendar::SetComponents' ); + $this->component->SetComponents( $new_component, $type ); + } + + + /** + * @DEPRECATED: This class will be removed soon. + * Adds a new subcomponent + * + * @param iCalComponent $new_component The new component to append to the set + */ + function AddComponent( $new_component ) { + deprecated('iCalendar::AddComponent' ); + $this->component->AddComponent($new_component); + } + + + /** + * @DEPRECATED: This class will be removed soon. + * Mask components, removing any that are not of the types in the list + * @param array $keep An array of component types to be kept + */ + function MaskComponents( $keep ) { + deprecated('iCalendar::MaskComponents' ); + $this->component->MaskComponents($keep); + } + + + /** + * @DEPRECATED: This class will be removed soon. + * Returns a PostgreSQL Date Format string suitable for returning HTTP (RFC2068) dates + * Preferred is "Sun, 06 Nov 1994 08:49:37 GMT" so we do that. + */ + static function HttpDateFormat() { + return "'Dy, DD Mon IYYY HH24:MI:SS \"GMT\"'"; + } + + + /** + * @DEPRECATED: This class will be removed soon. + * Returns a PostgreSQL Date Format string suitable for returning iCal dates + */ + static function SqlDateFormat() { + return "'YYYYMMDD\"T\"HH24MISS'"; + } + + + /** + * @DEPRECATED: This class will be removed soon. + * Returns a PostgreSQL Date Format string suitable for returning dates which + * have been cast to UTC + */ + static function SqlUTCFormat() { + return "'YYYYMMDD\"T\"HH24MISS\"Z\"'"; + } + + + /** + * @DEPRECATED: This class will be removed soon. + * Returns a PostgreSQL Date Format string suitable for returning iCal durations + * - this doesn't work for negative intervals, but events should not have such! + */ + static function SqlDurationFormat() { + return "'\"PT\"HH24\"H\"MI\"M\"'"; + } + + /** + * @DEPRECATED: This class will be removed soon. + * Returns a suitably escaped RFC2445 content string. + * + * @param string $name The incoming name[;param] prefixing the string. + * @param string $value The incoming string to be escaped. + * + * @deprecated This function is deprecated and will be removed eventually. + * @todo Remove this function. + */ + function RFC2445ContentEscape( $name, $value ) { + deprecated('iCalendar::RFC2445ContentEscape' ); + $property = preg_replace( '/[;].*$/', '', $name ); + switch( $property ) { + /** Content escaping does not apply to these properties culled from RFC2445 */ + case 'ATTACH': case 'GEO': case 'PERCENT-COMPLETE': case 'PRIORITY': + case 'COMPLETED': case 'DTEND': case 'DUE': case 'DTSTART': + case 'DURATION': case 'FREEBUSY': case 'TZOFFSETFROM': case 'TZOFFSETTO': + case 'TZURL': case 'ATTENDEE': case 'ORGANIZER': case 'RECURRENCE-ID': + case 'URL': case 'EXDATE': case 'EXRULE': case 'RDATE': + case 'RRULE': case 'REPEAT': case 'TRIGGER': case 'CREATED': + case 'DTSTAMP': case 'LAST-MODIFIED': case 'SEQUENCE': + break; + + /** Content escaping applies by default to other properties */ + default: + $value = str_replace( '\\', '\\\\', $value); + $value = preg_replace( '/\r?\n/', '\\n', $value); + $value = preg_replace( "/([,;:\"])/", '\\\\$1', $value); + } + $result = preg_replace( '/(.{72})/u', '$1'."\r\n ", $name.':'.$value ) ."\r\n"; + return $result; + } + + /** + * @DEPRECATED: This class will be removed soon. + * Return all sub-components of the given type, which are part of the + * component we pass in as an array of lines. + * + * @param array $component The component to be parsed + * @param string $type The type of sub-components to be extracted + * @param int $count The number of sub-components to extract (default: 9999) + * + * @return array The sub-component lines + */ + function ExtractSubComponent( $component, $type, $count=9999 ) { + deprecated('iCalendar::ExtractSubComponent' ); + $answer = array(); + $intags = false; + $start = "BEGIN:$type"; + $finish = "END:$type"; + dbg_error_log( 'iCalendar', ":ExtractSubComponent: Looking for %d subsets of type %s", $count, $type ); + reset($component); + foreach( $component AS $k => $v ) { + if ( !$intags && $v == $start ) { + $answer[] = $v; + $intags = true; + } + else if ( $intags && $v == $finish ) { + $answer[] = $v; + $intags = false; + } + else if ( $intags ) { + $answer[] = $v; + } + } + return $answer; + } + + + /** + * @DEPRECATED: This class will be removed soon. + * Extract a particular property from the provided component. In doing so we + * assume that the content was unescaped when iCalComponent::ParseFrom() + * called iCalComponent::UnwrapComponent(). + * + * @param array $component An array of lines of this component + * @param string $type The type of parameter + * + * @return array An array of iCalProperty objects + */ + function ExtractProperty( $component, $type, $count=9999 ) { + deprecated('iCalendar::ExtractProperty' ); + $answer = array(); + dbg_error_log( 'iCalendar', ":ExtractProperty: Looking for %d properties of type %s", $count, $type ); + reset($component); + foreach( $component AS $k => $v ) { + if ( preg_match( "/$type"."[;:]/i", $v ) ) { + $answer[] = new iCalProp($v); + dbg_error_log( 'iCalendar', ":ExtractProperty: Found property %s", $type ); + if ( --$count < 1 ) return $answer; + } + } + return $answer; + } + + + /** + * @DEPRECATED: This class will be removed soon. + * Applies the filter conditions, possibly recursively, to the value which will be either + * a single property, or an array of lines of the component under test. + * + * @todo Eventually we need to handle all of these possibilities, which will mean writing + * several routines: + * - Get Property from Component + * - Get Parameter from Property + * - Test TimeRange + * For the moment we will leave these, until there is a perceived need. + * + * @param array $filter An array of XMLElement defining the filter(s) + * @param mixed $value Either a string which is the single property, or an array of lines, for the component. + * @return boolean Whether the filter passed / failed. + */ + function ApplyFilter( $filter, $value ) { + deprecated('iCalendar::ApplyFilter' ); + foreach( $filter AS $k => $v ) { + $tag = $v->GetTag(); + $value_type = gettype($value); + $value_defined = (isset($value) && $value_type == 'string') || ($value_type == 'array' && count($value) > 0 ); + if ( $tag == 'urn:ietf:params:xml:ns:caldav:is-not-defined' && $value_defined ) { + dbg_error_log( 'iCalendar', ":ApplyFilter: Value is set ('%s'), want unset, for filter %s", count($value), $tag ); + return false; + } + elseif ( $tag == 'urn:ietf:params:xml:ns:caldav:is-defined' && !$value_defined ) { + dbg_error_log( 'iCalendar', ":ApplyFilter: Want value, but it is not set for filter %s", $tag ); + return false; + } + else { + dbg_error_log( 'iCalendar', ":ApplyFilter: Have values for '%s' filter", $tag ); + switch( $tag ) { + case 'urn:ietf:params:xml:ns:caldav:time-range': + /** todo:: While this is unimplemented here at present, most time-range tests should occur at the SQL level. */ + break; + case 'urn:ietf:params:xml:ns:caldav:text-match': + $search = $v->GetContent(); + // In this case $value will either be a string, or an array of iCalProp objects + // since TEXT-MATCH does not apply to COMPONENT level - only property/parameter + if ( !is_string($value) ) { + if ( is_array($value) ) { + $match = false; + foreach( $value AS $k1 => $v1 ) { + // $v1 MUST be an iCalProp object + if ( $match = $v1->TextMatch($search)) break; + } + } + else { + dbg_error_log( 'iCalendar', ":ApplyFilter: TEXT-MATCH will only work on strings or arrays of iCalProp. %s unsupported", gettype($value) ); + return true; // We return _true_ in this case, so the client sees the item + } + } + else { + $match = (stristr( $value, $search ) !== false); + } + $negate = $v->GetAttribute("negate-condition"); + if ( isset($negate) && strtolower($negate) == "yes" ) $match = !$match; +// dbg_error_log( 'iCalendar', ":ApplyFilter: TEXT-MATCH returning %s", ($match?"yes":"no") ); + return $match; + break; + case 'urn:ietf:params:xml:ns:caldav:comp-filter': + $subfilter = $v->GetContent(); + $component = $this->ExtractSubComponent($value,$v->GetAttribute("name")); + if ( ! $this->ApplyFilter($subfilter,$component) ) return false; + break; + case 'urn:ietf:params:xml:ns:caldav:prop-filter': + $subfilter = $v->GetContent(); + $properties = $this->ExtractProperty($value,$v->GetAttribute("name")); + if ( ! $this->ApplyFilter($subfilter,$properties) ) return false; + break; + case 'urn:ietf:params:xml:ns:caldav:param-filter': + $subfilter = $v->GetContent(); + $parameter = $this->ExtractParameter($value,$v->GetAttribute("NAME")); + if ( ! $this->ApplyFilter($subfilter,$parameter) ) return false; + break; + } + } + } + return true; + } + + /** + * @DEPRECATED: This class will be removed soon. + * Test a PROP-FILTER or COMP-FILTER and return a true/false + * COMP-FILTER (is-defined | is-not-defined | (time-range?, prop-filter*, comp-filter*)) + * PROP-FILTER (is-defined | is-not-defined | ((time-range | text-match)?, param-filter*)) + * + * @param array $filter An array of XMLElement defining the filter + * + * @return boolean Whether or not this iCalendar passes the test + */ + function TestFilter( $filters ) { + deprecated('iCalendar::TestFilter' ); + +// dbg_error_log('iCalendar', ':TestFilter we have %d filters to test', count($filters) ); + foreach( $filters AS $k => $v ) { + $tag = $v->GetTag(); +// dbg_error_log('iCalendar', ':TestFilter working on tag "%s" %s"', $k, $tag ); + $name = $v->GetAttribute("name"); + $filter = $v->GetContent(); + if ( $tag == "urn:ietf:params:xml:ns:caldav:prop-filter" ) { + $value = $this->ExtractProperty($this->lines,$name); + } + else { + $value = $this->ExtractSubComponent($this->lines,$v->GetAttribute("name")); + } + if ( count($value) == 0 ) unset($value); + if ( ! $this->ApplyFilter($filter,$value) ) return false; + } + return true; + } + + /** + * @DEPRECATED: This class will be removed soon. + * Returns the header we always use at the start of our iCalendar resources + * + * @todo Remove this function. + */ + static function iCalHeader() { + deprecated('iCalendar::iCalHeader' ); + return <<component->Render(); + } + else { + $components = $this->component->GetComponents($type); + $rendered = ""; + foreach( $components AS $k => $v ) { + $rendered .= $v->Render($restrict_properties); + } + return $rendered; + } + } + +} diff --git a/sources/include/mimeDecode.php b/sources/include/mimeDecode.php index 80bab66..dd38e50 100644 --- a/sources/include/mimeDecode.php +++ b/sources/include/mimeDecode.php @@ -356,7 +356,7 @@ class Mail_mimeDecode if ($this->_rfc822_bodies) { $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; $charset = isset($return->ctype_parameters['charset']) ? $return->ctype_parameters['charset'] : $this->_charset; - $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding, $charset) : $body); + $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding, $charset, false) : $body); } $obj = new Mail_mimeDecode($body); @@ -371,7 +371,7 @@ class Mail_mimeDecode $content_transfer_encoding['value'] = '7bit'; // if there is no explicit charset, then don't try to convert to default charset, and make sure that only text mimetypes are converted $charset = (isset($return->ctype_parameters['charset']) && ((isset($return->ctype_primary) && $return->ctype_primary == 'text') || !isset($return->ctype_primary)) )? $return->ctype_parameters['charset']: ''; - $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $content_transfer_encoding['value'], $charset) : $body) : null; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $content_transfer_encoding['value'], $charset, false) : $body) : null; break; } @@ -711,8 +711,11 @@ class Mail_mimeDecode // Remove white space between encoded-words $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input); + $encodedwords = false; + $charset = ''; // For each encoded-word... while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) { + $encodedwords = true; $encoded = $matches[1]; $charset = $matches[2]; @@ -727,12 +730,17 @@ class Mail_mimeDecode case 'q': $text = str_replace('_', ' ', $text); preg_match_all('/=([a-f0-9]{2})/i', $text, $matches); - foreach($matches[1] as $value) + foreach($matches[1] as $value) { $text = str_replace('='.$value, chr(hexdec($value)), $text); + } break; } - $input = str_replace($encoded, $this->_fromCharset($charset, $text), $input); + $input = str_replace($encoded, $this->_autoconvert_encoding($text, $charset), $input); + } + + if (!$encodedwords) { + $input = $this->_autoconvert_encoding($input, $charset); } return $input; @@ -744,33 +752,76 @@ class Mail_mimeDecode * * @param string Input body to decode * @param string Encoding type to use. + * @param string Charset + * @param boolean Must try to autodetect the real charset used * @return string Decoded body * @access private */ - function _decodeBody($input, $encoding = '7bit', $charset = '') + function _decodeBody($input, $encoding = '7bit', $charset = '', $detectCharset = true) { switch (strtolower($encoding)) { - case '7bit': - return $this->_fromCharset($charset, $input);; - break; - - case '8bit': - return $this->_fromCharset($charset, $input); - break; - case 'quoted-printable': - return $this->_fromCharset($charset, $this->_quotedPrintableDecode($input)); + $input_decoded = $this->_quotedPrintableDecode($input); + return $detectCharset ? $this->_autoconvert_encoding($input_decoded, $charset) : $input_decoded; break; case 'base64': - return $this->_fromCharset($charset, base64_decode($input)); + $input_decoded = base64_decode($input); + return $detectCharset ? $this->_autoconvert_encoding($input_decoded, $charset) : $input_decoded; break; + case '7bit': + case '8bit': default: - return $input; + return $detectCharset ? $this->_autoconvert_encoding($input, $charset) : $input; + break; } } + /** + * Error handler dummy for _autoconvert_encoding + * + * @param integer $errno + * @param string $errstr + * @return boolean true + * @access public static + */ + static function _iconv_notice_handler($errno, $errstr) { + return true; + } + + /** + * Autoconvert the text from any encoding. THIS WILL NEVER WORK 100%. + * Will ignore the E_NOTICE for iconv when detecting ilegal charsets + * + * @param string $input Input string to convert + * @param string $supposed_encoding Encoding that the text is possibly using + * @return string Converted string + * @access private + */ + function _autoconvert_encoding($input, $supposed_encoding = "UTF-8") { + $input_converted = $input; + + if (function_exists("mb_detect_order")) { + $mb_order = array_merge(array($supposed_encoding), mb_detect_order()); + set_error_handler('Mail_mimeDecode::_iconv_notice_handler'); + try { + $input_converted = iconv(mb_detect_encoding($input, $mb_order, true), $this->_charset, $input); + } + catch(Exception $ex) { + $this->raiseError($ex->getMessage()); + } + restore_error_handler(); + + if (strlen($input_converted) == 0 && strlen($input) != 0) { + ZLog::Write(LOGLEVEL_INFO, "Mail_mimeDecode() - Text cannot be correctly decoded, using original text. Expect encoding errors"); + $input_converted = mb_convert_encoding($input, 'UTF-8', 'UTF-8'); + } + } + + return $input_converted; + } + /** * Given a quoted-printable string, this * function will decode and return it. @@ -865,6 +916,34 @@ class Mail_mimeDecode return $files; } + /** + * Get all parts in the message with specified type and concatenate them together, unless the + * Content-Disposition is 'attachment', in which case the text is apparently an attachment + * + * @param string $message mimedecode message(part) + * @param string $message message subtype + * @param string &$body body reference + * + * @return void + * @access public + */ + static function getBodyRecursive($message, $subtype, &$body) { + if(!isset($message->ctype_primary)) return; + if(strcasecmp($message->ctype_primary,"text")==0 && strcasecmp($message->ctype_secondary,$subtype)==0 && isset($message->body)) + $body .= $message->body; + + if(strcasecmp($message->ctype_primary,"multipart")==0 && isset($message->parts) && is_array($message->parts)) { + foreach($message->parts as $part) { + // Check testing/samples/m1009.txt + // Content-Type: text/plain; charset=us-ascii; name="hareandtoroise.txt" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="hareandtoroise.txt" + // We don't want to show that file text (outlook doesn't show it), so if we have content-disposition we don't apply recursivity + if(!isset($part->disposition)) { + Mail_mimeDecode::getBodyRecursive($part, $subtype, $body); + } + } + } + } + /** * getSendArray() returns the arguments required for Mail::send() * used to build the arguments for a mail::send() call @@ -1026,25 +1105,6 @@ class Mail_mimeDecode return $return; } - /** - * Z-Push helper to decode text - * - * @param string current charset of input - * @param string input - * @return string XML version of input - * @access private - */ - function _fromCharset($charset, $input) { - if($charset == '' || (strtolower($charset) == $this->_charset)) - return $input; - - // all ISO-8859-1 are converted as if they were Windows-1252 - see Mantis #456 - if (strtolower($charset) == 'iso-8859-1') - $charset = 'Windows-1252'; - - return @iconv($charset, $this->_charset. "//TRANSLIT", $input); - } - /** * Z-Push helper for error logging * removing PEAR dependency diff --git a/sources/include/mimePart.php b/sources/include/mimePart.php new file mode 100644 index 0000000..1c3f689 --- /dev/null +++ b/sources/include/mimePart.php @@ -0,0 +1,1270 @@ + + * Copyright (c) 2003-2006, PEAR + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * - Neither the name of the authors, nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes + * @author Cipriano Groenendal + * @author Sean Coates + * @author Aleksander Machniak + * @copyright 2003-2006 PEAR + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version CVS: $Id$ + * @link http://pear.php.net/package/Mail_mime + */ + + +/** + * Z-Push changes + * + * removed PEAR dependency by implementing own raiseError() + * + * Reference implementation used: + * http://download.pear.php.net/package/Mail_Mime-1.8.9.tgz + * + * + */ + +/** + * The Mail_mimePart class is used to create MIME E-mail messages + * + * This class enables you to manipulate and build a mime email + * from the ground up. The Mail_Mime class is a userfriendly api + * to this class for people who aren't interested in the internals + * of mime mail. + * This class however allows full control over the email. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes + * @author Cipriano Groenendal + * @author Sean Coates + * @author Aleksander Machniak + * @copyright 2003-2006 PEAR + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/Mail_mime + */ +class Mail_mimePart +{ + /** + * The encoding type of this part + * + * @var string + * @access private + */ + var $_encoding; + + /** + * An array of subparts + * + * @var array + * @access private + */ + var $_subparts; + + /** + * The output of this part after being built + * + * @var string + * @access private + */ + var $_encoded; + + /** + * Headers for this part + * + * @var array + * @access private + */ + var $_headers; + + /** + * The body of this part (not encoded) + * + * @var string + * @access private + */ + var $_body; + + /** + * The location of file with body of this part (not encoded) + * + * @var string + * @access private + */ + var $_body_file; + + /** + * The end-of-line sequence + * + * @var string + * @access private + */ + var $_eol = "\r\n"; + + + /** + * Constructor. + * + * Sets up the object. + * + * @param string $body The body of the mime part if any. + * @param array $params An associative array of optional parameters: + * content_type - The content type for this part eg multipart/mixed + * encoding - The encoding to use, 7bit, 8bit, + * base64, or quoted-printable + * charset - Content character set + * cid - Content ID to apply + * disposition - Content disposition, inline or attachment + * filename - Filename parameter for content disposition + * description - Content description + * name_encoding - Encoding of the attachment name (Content-Type) + * By default filenames are encoded using RFC2231 + * Here you can set RFC2047 encoding (quoted-printable + * or base64) instead + * filename_encoding - Encoding of the attachment filename (Content-Disposition) + * See 'name_encoding' + * headers_charset - Charset of the headers e.g. filename, description. + * If not set, 'charset' will be used + * eol - End of line sequence. Default: "\r\n" + * headers - Hash array with additional part headers. Array keys can be + * in form of : + * body_file - Location of file with part's body (instead of $body) + * + * @access public + */ + function Mail_mimePart($body = '', $params = array()) + { + if (!empty($params['eol'])) { + $this->_eol = $params['eol']; + } else if (defined('MAIL_MIMEPART_CRLF')) { // backward-copat. + $this->_eol = MAIL_MIMEPART_CRLF; + } + + // Additional part headers + if (!empty($params['headers']) && is_array($params['headers'])) { + $headers = $params['headers']; + } + + foreach ($params as $key => $value) { + switch ($key) { + case 'encoding': + $this->_encoding = $value; + $headers['Content-Transfer-Encoding'] = $value; + break; + + case 'cid': + $headers['Content-ID'] = '<' . $value . '>'; + break; + + case 'location': + $headers['Content-Location'] = $value; + break; + + case 'body_file': + $this->_body_file = $value; + break; + + // for backward compatibility + case 'dfilename': + $params['filename'] = $value; + break; + } + } + + // Default content-type + if (empty($params['content_type'])) { + $params['content_type'] = 'text/plain'; + } + + // Content-Type + $headers['Content-Type'] = $params['content_type']; + if (!empty($params['charset'])) { + $charset = "charset={$params['charset']}"; + // place charset parameter in the same line, if possible + if ((strlen($headers['Content-Type']) + strlen($charset) + 16) <= 76) { + $headers['Content-Type'] .= '; '; + } else { + $headers['Content-Type'] .= ';' . $this->_eol . ' '; + } + $headers['Content-Type'] .= $charset; + + // Default headers charset + if (!isset($params['headers_charset'])) { + $params['headers_charset'] = $params['charset']; + } + } + + // header values encoding parameters + $h_charset = !empty($params['headers_charset']) ? $params['headers_charset'] : 'US-ASCII'; + $h_language = !empty($params['language']) ? $params['language'] : null; + $h_encoding = !empty($params['name_encoding']) ? $params['name_encoding'] : null; + + + if (!empty($params['filename'])) { + $headers['Content-Type'] .= ';' . $this->_eol; + $headers['Content-Type'] .= $this->_buildHeaderParam( + 'name', $params['filename'], $h_charset, $h_language, $h_encoding + ); + } + + // Content-Disposition + if (!empty($params['disposition'])) { + $headers['Content-Disposition'] = $params['disposition']; + if (!empty($params['filename'])) { + $headers['Content-Disposition'] .= ';' . $this->_eol; + $headers['Content-Disposition'] .= $this->_buildHeaderParam( + 'filename', $params['filename'], $h_charset, $h_language, + !empty($params['filename_encoding']) ? $params['filename_encoding'] : null + ); + } + + // add attachment size + $size = $this->_body_file ? filesize($this->_body_file) : strlen($body); + if ($size) { + $headers['Content-Disposition'] .= ';' . $this->_eol . ' size=' . $size; + } + } + + if (!empty($params['description'])) { + $headers['Content-Description'] = $this->encodeHeader( + 'Content-Description', $params['description'], $h_charset, $h_encoding, + $this->_eol + ); + } + + // Search and add existing headers' parameters + foreach ($headers as $key => $value) { + $items = explode(':', $key); + if (count($items) == 2) { + $header = $items[0]; + $param = $items[1]; + if (isset($headers[$header])) { + $headers[$header] .= ';' . $this->_eol; + } + $headers[$header] .= $this->_buildHeaderParam( + $param, $value, $h_charset, $h_language, $h_encoding + ); + unset($headers[$key]); + } + } + + // Default encoding + if (!isset($this->_encoding)) { + $this->_encoding = '7bit'; + } + + // Assign stuff to member variables + $this->_encoded = array(); + $this->_headers = $headers; + $this->_body = $body; + } + + /** + * Encodes and returns the email. Also stores + * it in the encoded member variable + * + * @param string $boundary Pre-defined boundary string + * + * @return An associative array containing two elements, + * body and headers. The headers element is itself + * an indexed array. On error returns PEAR error object. + * @access public + */ + function encode($boundary=null) + { + $encoded =& $this->_encoded; + + if (count($this->_subparts)) { + $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime()); + $eol = $this->_eol; + + $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; + + $encoded['body'] = ''; + + for ($i = 0; $i < count($this->_subparts); $i++) { + $encoded['body'] .= '--' . $boundary . $eol; + $tmp = $this->_subparts[$i]->encode(); + if ($this->_isError($tmp)) { + return $tmp; + } + foreach ($tmp['headers'] as $key => $value) { + $encoded['body'] .= $key . ': ' . $value . $eol; + } + $encoded['body'] .= $eol . $tmp['body'] . $eol; + } + + $encoded['body'] .= '--' . $boundary . '--' . $eol; + + } else if ($this->_body) { + $encoded['body'] = $this->_getEncodedData($this->_body, $this->_encoding); + } else if ($this->_body_file) { + // Temporarily reset magic_quotes_runtime for file reads and writes + if ($magic_quote_setting = get_magic_quotes_runtime()) { + @ini_set('magic_quotes_runtime', 0); + } + $body = $this->_getEncodedDataFromFile($this->_body_file, $this->_encoding); + if ($magic_quote_setting) { + @ini_set('magic_quotes_runtime', $magic_quote_setting); + } + + if ($this->_isError($body)) { + return $body; + } + $encoded['body'] = $body; + } else { + $encoded['body'] = ''; + } + + // Add headers to $encoded + $encoded['headers'] =& $this->_headers; + + return $encoded; + } + + /** + * Encodes and saves the email into file. File must exist. + * Data will be appended to the file. + * + * @param string $filename Output file location + * @param string $boundary Pre-defined boundary string + * @param boolean $skip_head True if you don't want to save headers + * + * @return array An associative array containing message headers + * or PEAR error object + * @access public + * @since 1.6.0 + */ + function encodeToFile($filename, $boundary=null, $skip_head=false) + { + if (file_exists($filename) && !is_writable($filename)) { + $err = $this->_raiseError('File is not writeable: ' . $filename); + return $err; + } + + if (!($fh = fopen($filename, 'ab'))) { + $err = $this->_raiseError('Unable to open file: ' . $filename); + return $err; + } + + // Temporarily reset magic_quotes_runtime for file reads and writes + if ($magic_quote_setting = get_magic_quotes_runtime()) { + @ini_set('magic_quotes_runtime', 0); + } + + $res = $this->_encodePartToFile($fh, $boundary, $skip_head); + + fclose($fh); + + if ($magic_quote_setting) { + @ini_set('magic_quotes_runtime', $magic_quote_setting); + } + + return $this->_isError($res) ? $res : $this->_headers; + } + + /** + * Encodes given email part into file + * + * @param string $fh Output file handle + * @param string $boundary Pre-defined boundary string + * @param boolean $skip_head True if you don't want to save headers + * + * @return array True on sucess or PEAR error object + * @access private + */ + function _encodePartToFile($fh, $boundary=null, $skip_head=false) + { + $eol = $this->_eol; + + if (count($this->_subparts)) { + $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime()); + $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; + } + + if (!$skip_head) { + foreach ($this->_headers as $key => $value) { + fwrite($fh, $key . ': ' . $value . $eol); + } + $f_eol = $eol; + } else { + $f_eol = ''; + } + + if (count($this->_subparts)) { + for ($i = 0; $i < count($this->_subparts); $i++) { + fwrite($fh, $f_eol . '--' . $boundary . $eol); + $res = $this->_subparts[$i]->_encodePartToFile($fh); + if ($this->_isError($res)) { + return $res; + } + $f_eol = $eol; + } + + fwrite($fh, $eol . '--' . $boundary . '--' . $eol); + + } else if ($this->_body) { + fwrite($fh, $f_eol . $this->_getEncodedData($this->_body, $this->_encoding)); + } else if ($this->_body_file) { + fwrite($fh, $f_eol); + $res = $this->_getEncodedDataFromFile( + $this->_body_file, $this->_encoding, $fh + ); + if ($this->_isError($res)) { + return $res; + } + } + + return true; + } + + /** + * Adds a subpart to current mime part and returns + * a reference to it + * + * @param string $body The body of the subpart, if any. + * @param array $params The parameters for the subpart, same + * as the $params argument for constructor. + * + * @return Mail_mimePart A reference to the part you just added. In PHP4, it is + * crucial if using multipart/* in your subparts that + * you use =& in your script when calling this function, + * otherwise you will not be able to add further subparts. + * @access public + */ + function &addSubpart($body, $params) + { + $this->_subparts[] = $part = new Mail_mimePart($body, $params); + return $part; + } + + /** + * Returns encoded data based upon encoding passed to it + * + * @param string $data The data to encode. + * @param string $encoding The encoding type to use, 7bit, base64, + * or quoted-printable. + * + * @return string + * @access private + */ + function _getEncodedData($data, $encoding) + { + switch ($encoding) { + case 'quoted-printable': + return $this->_quotedPrintableEncode($data); + break; + + case 'base64': + return rtrim(chunk_split(base64_encode($data), 76, $this->_eol)); + break; + + case '8bit': + case '7bit': + default: + return $data; + } + } + + /** + * Returns encoded data based upon encoding passed to it + * + * @param string $filename Data file location + * @param string $encoding The encoding type to use, 7bit, base64, + * or quoted-printable. + * @param resource $fh Output file handle. If set, data will be + * stored into it instead of returning it + * + * @return string Encoded data or PEAR error object + * @access private + */ + function _getEncodedDataFromFile($filename, $encoding, $fh=null) + { + if (!is_readable($filename)) { + $err = $this->_raiseError('Unable to read file: ' . $filename); + return $err; + } + + if (!($fd = fopen($filename, 'rb'))) { + $err = $this->_raiseError('Could not open file: ' . $filename); + return $err; + } + + $data = ''; + + switch ($encoding) { + case 'quoted-printable': + while (!feof($fd)) { + $buffer = $this->_quotedPrintableEncode(fgets($fd)); + if ($fh) { + fwrite($fh, $buffer); + } else { + $data .= $buffer; + } + } + break; + + case 'base64': + while (!feof($fd)) { + // Should read in a multiple of 57 bytes so that + // the output is 76 bytes per line. Don't use big chunks + // because base64 encoding is memory expensive + $buffer = fread($fd, 57 * 9198); // ca. 0.5 MB + $buffer = base64_encode($buffer); + $buffer = chunk_split($buffer, 76, $this->_eol); + if (feof($fd)) { + $buffer = rtrim($buffer); + } + + if ($fh) { + fwrite($fh, $buffer); + } else { + $data .= $buffer; + } + } + break; + + case '8bit': + case '7bit': + default: + while (!feof($fd)) { + $buffer = fread($fd, 1048576); // 1 MB + if ($fh) { + fwrite($fh, $buffer); + } else { + $data .= $buffer; + } + } + } + + fclose($fd); + + if (!$fh) { + return $data; + } + } + + /** + * Encodes data to quoted-printable standard. + * + * @param string $input The data to encode + * @param int $line_max Optional max line length. Should + * not be more than 76 chars + * + * @return string Encoded data + * + * @access private + */ + function _quotedPrintableEncode($input , $line_max = 76) + { + $eol = $this->_eol; + /* + // imap_8bit() is extremely fast, but doesn't handle properly some characters + if (function_exists('imap_8bit') && $line_max == 76) { + $input = preg_replace('/\r?\n/', "\r\n", $input); + $input = imap_8bit($input); + if ($eol != "\r\n") { + $input = str_replace("\r\n", $eol, $input); + } + return $input; + } + */ + $lines = preg_split("/\r?\n/", $input); + $escape = '='; + $output = ''; + + while (list($idx, $line) = each($lines)) { + $newline = ''; + $i = 0; + + while (isset($line[$i])) { + $char = $line[$i]; + $dec = ord($char); + $i++; + + if (($dec == 32) && (!isset($line[$i]))) { + // convert space at eol only + $char = '=20'; + } elseif ($dec == 9 && isset($line[$i])) { + ; // Do nothing if a TAB is not on eol + } elseif (($dec == 61) || ($dec < 32) || ($dec > 126)) { + $char = $escape . sprintf('%02X', $dec); + } elseif (($dec == 46) && (($newline == '') + || ((strlen($newline) + strlen("=2E")) >= $line_max)) + ) { + // Bug #9722: convert full-stop at bol, + // some Windows servers need this, won't break anything (cipri) + // Bug #11731: full-stop at bol also needs to be encoded + // if this line would push us over the line_max limit. + $char = '=2E'; + } + + // Note, when changing this line, also change the ($dec == 46) + // check line, as it mimics this line due to Bug #11731 + // EOL is not counted + if ((strlen($newline) + strlen($char)) >= $line_max) { + // soft line break; " =\r\n" is okay + $output .= $newline . $escape . $eol; + $newline = ''; + } + $newline .= $char; + } // end of for + $output .= $newline . $eol; + unset($lines[$idx]); + } + // Don't want last crlf + $output = substr($output, 0, -1 * strlen($eol)); + return $output; + } + + /** + * Encodes the parameter of a header. + * + * @param string $name The name of the header-parameter + * @param string $value The value of the paramter + * @param string $charset The characterset of $value + * @param string $language The language used in $value + * @param string $encoding Parameter encoding. If not set, parameter value + * is encoded according to RFC2231 + * @param int $maxLength The maximum length of a line. Defauls to 75 + * + * @return string + * + * @access private + */ + function _buildHeaderParam($name, $value, $charset=null, $language=null, + $encoding=null, $maxLength=75 + ) { + // RFC 2045: + // value needs encoding if contains non-ASCII chars or is longer than 78 chars + if (!preg_match('#[^\x20-\x7E]#', $value)) { + $token_regexp = '#([^\x21\x23-\x27\x2A\x2B\x2D' + . '\x2E\x30-\x39\x41-\x5A\x5E-\x7E])#'; + if (!preg_match($token_regexp, $value)) { + // token + if (strlen($name) + strlen($value) + 3 <= $maxLength) { + return " {$name}={$value}"; + } + } else { + // quoted-string + $quoted = addcslashes($value, '\\"'); + if (strlen($name) + strlen($quoted) + 5 <= $maxLength) { + return " {$name}=\"{$quoted}\""; + } + } + } + + // RFC2047: use quoted-printable/base64 encoding + if ($encoding == 'quoted-printable' || $encoding == 'base64') { + return $this->_buildRFC2047Param($name, $value, $charset, $encoding); + } + + // RFC2231: + $encValue = preg_replace_callback( + '/([^\x21\x23\x24\x26\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E])/', + array($this, '_encodeReplaceCallback'), $value + ); + $value = "$charset'$language'$encValue"; + + $header = " {$name}*={$value}"; + if (strlen($header) <= $maxLength) { + return $header; + } + + $preLength = strlen(" {$name}*0*="); + $maxLength = max(16, $maxLength - $preLength - 3); + $maxLengthReg = "|(.{0,$maxLength}[^\%][^\%])|"; + + $headers = array(); + $headCount = 0; + while ($value) { + $matches = array(); + $found = preg_match($maxLengthReg, $value, $matches); + if ($found) { + $headers[] = " {$name}*{$headCount}*={$matches[0]}"; + $value = substr($value, strlen($matches[0])); + } else { + $headers[] = " {$name}*{$headCount}*={$value}"; + $value = ''; + } + $headCount++; + } + + $headers = implode(';' . $this->_eol, $headers); + return $headers; + } + + /** + * Encodes header parameter as per RFC2047 if needed + * + * @param string $name The parameter name + * @param string $value The parameter value + * @param string $charset The parameter charset + * @param string $encoding Encoding type (quoted-printable or base64) + * @param int $maxLength Encoded parameter max length. Default: 76 + * + * @return string Parameter line + * @access private + */ + function _buildRFC2047Param($name, $value, $charset, + $encoding='quoted-printable', $maxLength=76 + ) { + // WARNING: RFC 2047 says: "An 'encoded-word' MUST NOT be used in + // parameter of a MIME Content-Type or Content-Disposition field", + // but... it's supported by many clients/servers + $quoted = ''; + + if ($encoding == 'base64') { + $value = base64_encode($value); + $prefix = '=?' . $charset . '?B?'; + $suffix = '?='; + + // 2 x SPACE, 2 x '"', '=', ';' + $add_len = strlen($prefix . $suffix) + strlen($name) + 6; + $len = $add_len + strlen($value); + + while ($len > $maxLength) { + // We can cut base64-encoded string every 4 characters + $real_len = floor(($maxLength - $add_len) / 4) * 4; + $_quote = substr($value, 0, $real_len); + $value = substr($value, $real_len); + + $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' '; + $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';' + $len = strlen($value) + $add_len; + } + $quoted .= $prefix . $value . $suffix; + + } else { + // quoted-printable + $value = $this->encodeQP($value); + $prefix = '=?' . $charset . '?Q?'; + $suffix = '?='; + + // 2 x SPACE, 2 x '"', '=', ';' + $add_len = strlen($prefix . $suffix) + strlen($name) + 6; + $len = $add_len + strlen($value); + + while ($len > $maxLength) { + $length = $maxLength - $add_len; + // don't break any encoded letters + if (preg_match("/^(.{0,$length}[^\=][^\=])/", $value, $matches)) { + $_quote = $matches[1]; + } + + $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' '; + $value = substr($value, strlen($_quote)); + $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';' + $len = strlen($value) + $add_len; + } + + $quoted .= $prefix . $value . $suffix; + } + + return " {$name}=\"{$quoted}\""; + } + + /** + * Encodes a header as per RFC2047 + * + * @param string $name The header name + * @param string $value The header data to encode + * @param string $charset Character set name + * @param string $encoding Encoding name (base64 or quoted-printable) + * @param string $eol End-of-line sequence. Default: "\r\n" + * + * @return string Encoded header data (without a name) + * @access public + * @since 1.6.1 + */ + function encodeHeader($name, $value, $charset='ISO-8859-1', + $encoding='quoted-printable', $eol="\r\n" + ) { + // Structured headers + $comma_headers = array( + 'from', 'to', 'cc', 'bcc', 'sender', 'reply-to', + 'resent-from', 'resent-to', 'resent-cc', 'resent-bcc', + 'resent-sender', 'resent-reply-to', + 'mail-reply-to', 'mail-followup-to', + 'return-receipt-to', 'disposition-notification-to', + ); + $other_headers = array( + 'references', 'in-reply-to', 'message-id', 'resent-message-id', + ); + + $name = strtolower($name); + + if (in_array($name, $comma_headers)) { + $separator = ','; + } else if (in_array($name, $other_headers)) { + $separator = ' '; + } + + if (!$charset) { + $charset = 'ISO-8859-1'; + } + + // Structured header (make sure addr-spec inside is not encoded) + if (!empty($separator)) { + // Simple e-mail address regexp + $email_regexp = '([^\s<]+|("[^\r\n"]+"))@\S+'; + + $parts = Mail_mimePart::_explodeQuotedString("[\t$separator]", $value); + $value = ''; + + foreach ($parts as $part) { + $part = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $part); + $part = trim($part); + + if (!$part) { + continue; + } + if ($value) { + $value .= $separator == ',' ? $separator . ' ' : ' '; + } else { + $value = $name . ': '; + } + + // let's find phrase (name) and/or addr-spec + if (preg_match('/^<' . $email_regexp . '>$/', $part)) { + $value .= $part; + } else if (preg_match('/^' . $email_regexp . '$/', $part)) { + // address without brackets and without name + $value .= $part; + } else if (preg_match('/<*' . $email_regexp . '>*$/', $part, $matches)) { + // address with name (handle name) + $address = $matches[0]; + $word = str_replace($address, '', $part); + $word = trim($word); + // check if phrase requires quoting + if ($word) { + // non-ASCII: require encoding + if (preg_match('#([^\s\x21-\x7E]){1}#', $word)) { + if ($word[0] == '"' && $word[strlen($word)-1] == '"') { + // de-quote quoted-string, encoding changes + // string to atom + $search = array("\\\"", "\\\\"); + $replace = array("\"", "\\"); + $word = str_replace($search, $replace, $word); + $word = substr($word, 1, -1); + } + // find length of last line + if (($pos = strrpos($value, $eol)) !== false) { + $last_len = strlen($value) - $pos; + } else { + $last_len = strlen($value); + } + $word = Mail_mimePart::encodeHeaderValue( + $word, $charset, $encoding, $last_len, $eol + ); + } else if (($word[0] != '"' || $word[strlen($word)-1] != '"') + && preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $word) + ) { + // ASCII: quote string if needed + $word = '"'.addcslashes($word, '\\"').'"'; + } + } + $value .= $word.' '.$address; + } else { + // addr-spec not found, don't encode (?) + $value .= $part; + } + + // RFC2822 recommends 78 characters limit, use 76 from RFC2047 + $value = wordwrap($value, 76, $eol . ' '); + } + + // remove header name prefix (there could be EOL too) + $value = preg_replace( + '/^'.$name.':('.preg_quote($eol, '/').')* /', '', $value + ); + } else { + // Unstructured header + // non-ASCII: require encoding + if (preg_match('#([^\s\x21-\x7E]){1}#', $value)) { + if ($value[0] == '"' && $value[strlen($value)-1] == '"') { + // de-quote quoted-string, encoding changes + // string to atom + $search = array("\\\"", "\\\\"); + $replace = array("\"", "\\"); + $value = str_replace($search, $replace, $value); + $value = substr($value, 1, -1); + } + $value = Mail_mimePart::encodeHeaderValue( + $value, $charset, $encoding, strlen($name) + 2, $eol + ); + } else if (strlen($name.': '.$value) > 78) { + // ASCII: check if header line isn't too long and use folding + $value = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $value); + $tmp = wordwrap($name.': '.$value, 78, $eol . ' '); + $value = preg_replace('/^'.$name.':\s*/', '', $tmp); + // hard limit 998 (RFC2822) + $value = wordwrap($value, 998, $eol . ' ', true); + } + } + + return $value; + } + + /** + * Explode quoted string + * + * @param string $delimiter Delimiter expression string for preg_match() + * @param string $string Input string + * + * @return array String tokens array + * @access private + */ + function _explodeQuotedString($delimiter, $string) + { + $result = array(); + $strlen = strlen($string); + + for ($q=$p=$i=0; $i < $strlen; $i++) { + if ($string[$i] == "\"" + && (empty($string[$i-1]) || $string[$i-1] != "\\") + ) { + $q = $q ? false : true; + } else if (!$q && preg_match("/$delimiter/", $string[$i])) { + $result[] = substr($string, $p, $i - $p); + $p = $i + 1; + } + } + + $result[] = substr($string, $p); + return $result; + } + + /** + * Encodes a header value as per RFC2047 + * + * @param string $value The header data to encode + * @param string $charset Character set name + * @param string $encoding Encoding name (base64 or quoted-printable) + * @param int $prefix_len Prefix length. Default: 0 + * @param string $eol End-of-line sequence. Default: "\r\n" + * + * @return string Encoded header data + * @access public + * @since 1.6.1 + */ + function encodeHeaderValue($value, $charset, $encoding, $prefix_len=0, $eol="\r\n") + { + // #17311: Use multibyte aware method (requires mbstring extension) + if ($result = Mail_mimePart::encodeMB($value, $charset, $encoding, $prefix_len, $eol)) { + return $result; + } + + // Generate the header using the specified params and dynamicly + // determine the maximum length of such strings. + // 75 is the value specified in the RFC. + $encoding = $encoding == 'base64' ? 'B' : 'Q'; + $prefix = '=?' . $charset . '?' . $encoding .'?'; + $suffix = '?='; + $maxLength = 75 - strlen($prefix . $suffix); + $maxLength1stLine = $maxLength - $prefix_len; + + if ($encoding == 'B') { + // Base64 encode the entire string + $value = base64_encode($value); + + // We can cut base64 every 4 characters, so the real max + // we can get must be rounded down. + $maxLength = $maxLength - ($maxLength % 4); + $maxLength1stLine = $maxLength1stLine - ($maxLength1stLine % 4); + + $cutpoint = $maxLength1stLine; + $output = ''; + + while ($value) { + // Split translated string at every $maxLength + $part = substr($value, 0, $cutpoint); + $value = substr($value, $cutpoint); + $cutpoint = $maxLength; + // RFC 2047 specifies that any split header should + // be separated by a CRLF SPACE. + if ($output) { + $output .= $eol . ' '; + } + $output .= $prefix . $part . $suffix; + } + $value = $output; + } else { + // quoted-printable encoding has been selected + $value = Mail_mimePart::encodeQP($value); + + // This regexp will break QP-encoded text at every $maxLength + // but will not break any encoded letters. + $reg1st = "|(.{0,$maxLength1stLine}[^\=][^\=])|"; + $reg2nd = "|(.{0,$maxLength}[^\=][^\=])|"; + + if (strlen($value) > $maxLength1stLine) { + // Begin with the regexp for the first line. + $reg = $reg1st; + $output = ''; + while ($value) { + // Split translated string at every $maxLength + // But make sure not to break any translated chars. + $found = preg_match($reg, $value, $matches); + + // After this first line, we need to use a different + // regexp for the first line. + $reg = $reg2nd; + + // Save the found part and encapsulate it in the + // prefix & suffix. Then remove the part from the + // $value_out variable. + if ($found) { + $part = $matches[0]; + $len = strlen($matches[0]); + $value = substr($value, $len); + } else { + $part = $value; + $value = ''; + } + + // RFC 2047 specifies that any split header should + // be separated by a CRLF SPACE + if ($output) { + $output .= $eol . ' '; + } + $output .= $prefix . $part . $suffix; + } + $value = $output; + } else { + $value = $prefix . $value . $suffix; + } + } + + return $value; + } + + /** + * Encodes the given string using quoted-printable + * + * @param string $str String to encode + * + * @return string Encoded string + * @access public + * @since 1.6.0 + */ + function encodeQP($str) + { + // Bug #17226 RFC 2047 restricts some characters + // if the word is inside a phrase, permitted chars are only: + // ASCII letters, decimal digits, "!", "*", "+", "-", "/", "=", and "_" + + // "=", "_", "?" must be encoded + $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/'; + $str = preg_replace_callback( + $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $str + ); + + return str_replace(' ', '_', $str); + } + + /** + * Encodes the given string using base64 or quoted-printable. + * This method makes sure that encoded-word represents an integral + * number of characters as per RFC2047. + * + * @param string $str String to encode + * @param string $charset Character set name + * @param string $encoding Encoding name (base64 or quoted-printable) + * @param int $prefix_len Prefix length. Default: 0 + * @param string $eol End-of-line sequence. Default: "\r\n" + * + * @return string Encoded string + * @access public + * @since 1.8.0 + */ + function encodeMB($str, $charset, $encoding, $prefix_len=0, $eol="\r\n") + { + if (!function_exists('mb_substr') || !function_exists('mb_strlen')) { + return; + } + + $encoding = $encoding == 'base64' ? 'B' : 'Q'; + // 75 is the value specified in the RFC + $prefix = '=?' . $charset . '?'.$encoding.'?'; + $suffix = '?='; + $maxLength = 75 - strlen($prefix . $suffix); + + // A multi-octet character may not be split across adjacent encoded-words + // So, we'll loop over each character + // mb_stlen() with wrong charset will generate a warning here and return null + $length = mb_strlen($str, $charset); + $result = ''; + $line_length = $prefix_len; + + if ($encoding == 'B') { + // base64 + $start = 0; + $prev = ''; + + for ($i=1; $i<=$length; $i++) { + // See #17311 + $chunk = mb_substr($str, $start, $i-$start, $charset); + $chunk = base64_encode($chunk); + $chunk_len = strlen($chunk); + + if ($line_length + $chunk_len == $maxLength || $i == $length) { + if ($result) { + $result .= "\n"; + } + $result .= $chunk; + $line_length = 0; + $start = $i; + } else if ($line_length + $chunk_len > $maxLength) { + if ($result) { + $result .= "\n"; + } + if ($prev) { + $result .= $prev; + } + $line_length = 0; + $start = $i - 1; + } else { + $prev = $chunk; + } + } + } else { + // quoted-printable + // see encodeQP() + $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/'; + + for ($i=0; $i<=$length; $i++) { + $char = mb_substr($str, $i, 1, $charset); + // RFC recommends underline (instead of =20) in place of the space + // that's one of the reasons why we're not using iconv_mime_encode() + if ($char == ' ') { + $char = '_'; + $char_len = 1; + } else { + $char = preg_replace_callback( + $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $char + ); + $char_len = strlen($char); + } + + if ($line_length + $char_len > $maxLength) { + if ($result) { + $result .= "\n"; + } + $line_length = 0; + } + + $result .= $char; + $line_length += $char_len; + } + } + + if ($result) { + $result = $prefix + .str_replace("\n", $suffix.$eol.' '.$prefix, $result).$suffix; + } + + return $result; + } + + /** + * Callback function to replace extended characters (\x80-xFF) with their + * ASCII values (RFC2047: quoted-printable) + * + * @param array $matches Preg_replace's matches array + * + * @return string Encoded character string + * @access private + */ + function _qpReplaceCallback($matches) + { + return sprintf('=%02X', ord($matches[1])); + } + + /** + * Callback function to replace extended characters (\x80-xFF) with their + * ASCII values (RFC2231) + * + * @param array $matches Preg_replace's matches array + * + * @return string Encoded character string + * @access private + */ + function _encodeReplaceCallback($matches) + { + return sprintf('%%%02X', ord($matches[1])); + } + + /** + * PEAR::isError implementation + * + * @param mixed $data Object + * + * @return bool True if object is an instance of PEAR_Error + * @access private + */ + function _isError($data) + { + // PEAR::isError() is not PHP 5.4 compatible (see Bug #19473) + //if (is_object($data) && is_a($data, 'PEAR_Error')) { + // return true; + //} + + //return false; + return $data === false; + } + + /** + * Z-Push helper for error logging + * removing PEAR dependency + * + * @param string debug message + * @return boolean always false as there was an error + * @access private + */ + function _raiseError($message) { + ZLog::Write(LOGLEVEL_ERROR, "mimePart error: ". $message); + return false; + } +} // End of class diff --git a/sources/include/z_RFC822.php b/sources/include/z_RFC822.php index d5909e3..52c9c60 100644 --- a/sources/include/z_RFC822.php +++ b/sources/include/z_RFC822.php @@ -1,37 +1,48 @@ | -// | Chuck Hagenbuch | -// +-----------------------------------------------------------------------+ +/** + * RFC 822 Email address list validation Utility + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2001-2010, Richard Heyes + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail + * @author Richard Heyes + * @author Chuck Hagenbuch * @author Chuck Hagenbuch - * @version $Revision: 1.23 $ + * @version $Revision: 294749 $ * @license BSD * @package Mail */ @@ -183,6 +194,7 @@ class Mail_RFC822 { $this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address); while ($this->address = $this->_splitAddresses($this->address)); + if ($this->address === false || isset($this->error)) { //require_once 'PEAR.php'; return $this->raiseError($this->error); @@ -342,22 +354,39 @@ class Mail_RFC822 { } /** - * Checks if a string has an unclosed quotes or not. + * Checks if a string has unclosed quotes or not. * * @access private - * @param string $string The string to check. - * @return boolean True if there are unclosed quotes inside the string, false otherwise. + * @param string $string The string to check. + * @return boolean True if there are unclosed quotes inside the string, + * false otherwise. */ function _hasUnclosedQuotes($string) { - $string = explode('"', $string); - $string_cnt = count($string); + $string = trim($string); + $iMax = strlen($string); + $in_quote = false; + $i = $slashes = 0; - for ($i = 0; $i < (count($string) - 1); $i++) - if (substr($string[$i], -1) == '\\') - $string_cnt--; + for (; $i < $iMax; ++$i) { + switch ($string[$i]) { + case '\\': + ++$slashes; + break; - return ($string_cnt % 2 === 0); + case '"': + if ($slashes % 2 == 0) { + $in_quote = !$in_quote; + } + // Fall through to default action below. + + default: + $slashes = 0; + break; + } + } + + return $in_quote; } /** @@ -511,6 +540,7 @@ class Mail_RFC822 { { // Splits on one or more Tab or space. $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY); + $phrase_parts = array(); while (count($parts) > 0){ $phrase_parts[] = $this->_splitCheck($parts, ' '); @@ -553,6 +583,7 @@ class Mail_RFC822 { // Validation has been turned off; assume the atom is okay. return true; } + // Check for any char from ASCII 0 - ASCII 127 if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) { return false; @@ -567,6 +598,7 @@ class Mail_RFC822 { if (preg_match('/[\\x00-\\x1F]+/', $atom)) { return false; } + return true; } @@ -615,8 +647,8 @@ class Mail_RFC822 { $comment = $this->_splitCheck($parts, ')'); $comments[] = $comment; - // +1 is for the trailing ) - $_mailbox = substr($_mailbox, strpos($_mailbox, $comment)+strlen($comment)+1); + // +2 is for the brackets + $_mailbox = substr($_mailbox, strpos($_mailbox, '('.$comment)+strlen($comment)+2); } else { break; } @@ -642,7 +674,6 @@ class Mail_RFC822 { } if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) { - return false; } diff --git a/sources/include/z_RTF.php b/sources/include/z_RTF.php new file mode 100644 index 0000000..5f5f818 --- /dev/null +++ b/sources/include/z_RTF.php @@ -0,0 +1,707 @@ + + http://josefine.ben.tuwien.ac.at/~mfischer/ + + Latest versions of this class can always be found at + http://josefine.ben.tuwien.ac.at/~mfischer/developing/php/rtf/rtfclass.phps + Testing suite is available at + http://josefine.ben.tuwien.ac.at/~mfischer/developing/php/rtf/ + + License: GPLv2 + + Specification: + http://msdn.microsoft.com/library/default.asp?URL=/library/specs/rtfspec.htm + + General Notes: + ============== + Unknown or unspupported control symbols are silently gnored + + Group stacking is still not supported :( + group stack logic implemented; however not really used yet + ===================================================================================================== + + It was modified by me (Andreas Brodowski) to allow compressed RTF being uncompressed by code I ported from + Java to PHP and adapted according the needs of Z-Push. + + Currently it is being used to detect empty RTF Streams from Nokia Phones in MfE Clients + + It needs to be used by other backend writers that needs to have notes in calendar, appointment or tasks + objects to be written to their databases since devices send them usually in RTF Format... With Zarafa + you can write them directly to DB and Zarafa is doing the conversion job. Other Groupware systems usually + don't have this possibility... + +*/ + +class rtf { + var $LZRTF_HDR_DATA = "{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript \\fdecor MS Sans SerifSymbolArialTimes New RomanCourier{\\colortbl\\red0\\green0\\blue0\n\r\\par \\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx"; + var $LZRTF_HDR_LEN = 207; + var $CRC32_TABLE = array( 0x00000000,0x77073096,0xEE0E612C,0x990951BA,0x076DC419,0x706AF48F,0xE963A535,0x9E6495A3, + 0x0EDB8832,0x79DCB8A4,0xE0D5E91E,0x97D2D988,0x09B64C2B,0x7EB17CBD,0xE7B82D07,0x90BF1D91, + 0x1DB71064,0x6AB020F2,0xF3B97148,0x84BE41DE,0x1ADAD47D,0x6DDDE4EB,0xF4D4B551,0x83D385C7, + 0x136C9856,0x646BA8C0,0xFD62F97A,0x8A65C9EC,0x14015C4F,0x63066CD9,0xFA0F3D63,0x8D080DF5, + 0x3B6E20C8,0x4C69105E,0xD56041E4,0xA2677172,0x3C03E4D1,0x4B04D447,0xD20D85FD,0xA50AB56B, + 0x35B5A8FA,0x42B2986C,0xDBBBC9D6,0xACBCF940,0x32D86CE3,0x45DF5C75,0xDCD60DCF,0xABD13D59, + 0x26D930AC,0x51DE003A,0xC8D75180,0xBFD06116,0x21B4F4B5,0x56B3C423,0xCFBA9599,0xB8BDA50F, + 0x2802B89E,0x5F058808,0xC60CD9B2,0xB10BE924,0x2F6F7C87,0x58684C11,0xC1611DAB,0xB6662D3D, + 0x76DC4190,0x01DB7106,0x98D220BC,0xEFD5102A,0x71B18589,0x06B6B51F,0x9FBFE4A5,0xE8B8D433, + 0x7807C9A2,0x0F00F934,0x9609A88E,0xE10E9818,0x7F6A0DBB,0x086D3D2D,0x91646C97,0xE6635C01, + 0x6B6B51F4,0x1C6C6162,0x856530D8,0xF262004E,0x6C0695ED,0x1B01A57B,0x8208F4C1,0xF50FC457, + 0x65B0D9C6,0x12B7E950,0x8BBEB8EA,0xFCB9887C,0x62DD1DDF,0x15DA2D49,0x8CD37CF3,0xFBD44C65, + 0x4DB26158,0x3AB551CE,0xA3BC0074,0xD4BB30E2,0x4ADFA541,0x3DD895D7,0xA4D1C46D,0xD3D6F4FB, + 0x4369E96A,0x346ED9FC,0xAD678846,0xDA60B8D0,0x44042D73,0x33031DE5,0xAA0A4C5F,0xDD0D7CC9, + 0x5005713C,0x270241AA,0xBE0B1010,0xC90C2086,0x5768B525,0x206F85B3,0xB966D409,0xCE61E49F, + 0x5EDEF90E,0x29D9C998,0xB0D09822,0xC7D7A8B4,0x59B33D17,0x2EB40D81,0xB7BD5C3B,0xC0BA6CAD, + 0xEDB88320,0x9ABFB3B6,0x03B6E20C,0x74B1D29A,0xEAD54739,0x9DD277AF,0x04DB2615,0x73DC1683, + 0xE3630B12,0x94643B84,0x0D6D6A3E,0x7A6A5AA8,0xE40ECF0B,0x9309FF9D,0x0A00AE27,0x7D079EB1, + 0xF00F9344,0x8708A3D2,0x1E01F268,0x6906C2FE,0xF762575D,0x806567CB,0x196C3671,0x6E6B06E7, + 0xFED41B76,0x89D32BE0,0x10DA7A5A,0x67DD4ACC,0xF9B9DF6F,0x8EBEEFF9,0x17B7BE43,0x60B08ED5, + 0xD6D6A3E8,0xA1D1937E,0x38D8C2C4,0x4FDFF252,0xD1BB67F1,0xA6BC5767,0x3FB506DD,0x48B2364B, + 0xD80D2BDA,0xAF0A1B4C,0x36034AF6,0x41047A60,0xDF60EFC3,0xA867DF55,0x316E8EEF,0x4669BE79, + 0xCB61B38C,0xBC66831A,0x256FD2A0,0x5268E236,0xCC0C7795,0xBB0B4703,0x220216B9,0x5505262F, + 0xC5BA3BBE,0xB2BD0B28,0x2BB45A92,0x5CB36A04,0xC2D7FFA7,0xB5D0CF31,0x2CD99E8B,0x5BDEAE1D, + 0x9B64C2B0,0xEC63F226,0x756AA39C,0x026D930A,0x9C0906A9,0xEB0E363F,0x72076785,0x05005713, + 0x95BF4A82,0xE2B87A14,0x7BB12BAE,0x0CB61B38,0x92D28E9B,0xE5D5BE0D,0x7CDCEFB7,0x0BDBDF21, + 0x86D3D2D4,0xF1D4E242,0x68DDB3F8,0x1FDA836E,0x81BE16CD,0xF6B9265B,0x6FB077E1,0x18B74777, + 0x88085AE6,0xFF0F6A70,0x66063BCA,0x11010B5C,0x8F659EFF,0xF862AE69,0x616BFFD3,0x166CCF45, + 0xA00AE278,0xD70DD2EE,0x4E048354,0x3903B3C2,0xA7672661,0xD06016F7,0x4969474D,0x3E6E77DB, + 0xAED16A4A,0xD9D65ADC,0x40DF0B66,0x37D83BF0,0xA9BCAE53,0xDEBB9EC5,0x47B2CF7F,0x30B5FFE9, + 0xBDBDF21C,0xCABAC28A,0x53B39330,0x24B4A3A6,0xBAD03605,0xCDD70693,0x54DE5729,0x23D967BF, + 0xB3667A2E,0xC4614AB8,0x5D681B02,0x2A6F2B94,0xB40BBE37,0xC30C8EA1,0x5A05DF1B,0x2D02EF8D, + ); + + var $rtf; // rtf core stream + var $rtf_len; // length in characters of the stream (get performace due avoiding calling strlen everytime) + var $err = array(); // array of error message, no entities on no error + + var $wantXML = false; // convert to XML + var $wantHTML = false; // convert to HTML + var $wantASCII = false; // convert to HTML + + // the only variable which should be accessed from the outside + var $out; // output data stream (depends on which $wantXXXXX is set to true + var $outstyles; // htmlified styles (generated after parsing if wantHTML + var $styles; // if wantHTML, stylesheet definitions are put in here + + // internal parser variables -------------------------------- + // control word variables + var $cword; // holds the current (or last) control word, depending on $cw + var $cw; // are we currently parsing a control word ? + var $cfirst; // could this be the first character ? so watch out for control symbols + + var $flags = array(); // parser flags + + var $queue; // every character which is no sepcial char, not belongs to a control word/symbol; is generally considered being 'plain' + + var $stack = array(); // group stack + + /* keywords which don't follw the specification (used by Word '97 - 2000) */ + // not yet used + var $control_exception = array( + "clFitText", + "clftsWidth(-?[0-9]+)?", + "clNoWrap(-?[0-9]+)?", + "clwWidth(-?[0-9]+)?", + "tdfrmtxtBottom(-?[0-9]+)?", + "tdfrmtxtLeft(-?[0-9]+)?", + "tdfrmtxtRight(-?[0-9]+)?", + "tdfrmtxtTop(-?[0-9]+)?", + "trftsWidthA(-?[0-9]+)?", + "trftsWidthB(-?[0-9]+)?", + "trftsWidth(-?[0-9]+)?", + "trwWithA(-?[0-9]+)?", + "trwWithB(-?[0-9]+)?", + "trwWith(-?[0-9]+)?", + "spectspecifygen(-?[0-9]+)?", + ); + + var $charset_table = array( + "0" => "ANSI", + "1" => "Default", + "2" => "Symbol", + "77" => "Mac", + "128" => "Shift Jis", + "129" => "Hangul", + "130" => "Johab", + "134" => "GB2312", + "136" => "Big5", + "161" => "Greek", + "162" => "Turkish", + "163" => "Vietnamese", + "177" => "Hebrew", + "178" => "Arabic", + "179" => "Arabic Traditional", + "180" => "Arabic user", + "181" => "Hebrew user", + "186" => "Baltic", + "204" => "Russian", + "222" => "Thai", + "238" => "Eastern European", + "255" => "PC 437", + "255" => "OEM", + ); + + /* note: the only conversion table used */ + var $fontmodifier_table = array( + "bold" => "b", + "italic" => "i", + "underlined" => "u", + "strikethru" => "strike", + ); + + + function rtf() { + $this->rtf_len = 0; + $this->rtf = ''; + + $this->out = ''; + } + + // loadrtf - load the raw rtf data to be converted by this class + // data = the raw rtf + function loadrtf($data) { + if (($this->rtf = $this->uncompress($data))) { + $this->rtf_len = strlen($this->rtf); + }; + if($this->rtf_len == 0) { + debugLog("No data in stream found"); + return false; + }; + return true; + } + + function output($typ) { + switch($typ) { + case "ascii": $this->wantASCII = true; break; + case "xml": $this->wantXML = true; break; + case "html": $this->wantHTML = true; break; + default: break; + } + } + + // uncompress - uncompress compressed rtf data + // src = the compressed raw rtf in LZRTF format + function uncompress($src) { + $header = unpack("LcSize/LuSize/Lmagic/Lcrc32",substr($src,0,16)); + $in = 16; + if ($header['cSize'] != strlen($src)-4) { + debugLog("Stream too short"); + return false; + } + + if ($header['crc32'] != $this->LZRTFCalcCRC32($src,16,(($header['cSize']+4))-16)) { + debugLog("CRC MISMATCH"); + return false; + } + + if ($header['magic'] == 0x414c454d) { // uncompressed RTF - return as is. + $dest = substr($src,$in,$header['uSize']); + } else if ($header['magic'] == 0x75465a4c) { // compressed RTF - uncompress. + $dst = $this->LZRTF_HDR_DATA; + $out = $this->LZRTF_HDR_LEN; + $oblen = $this->LZRTF_HDR_LEN + $header['uSize']; + $flagCount = 0; + $flags = 0; + while ($out<$oblen) { + $flags = ($flagCount++ % 8 == 0) ? ord($src{$in++}) : $flags >> 1; + if (($flags & 1) == 1) { + $offset = ord($src{$in++}); + $length = ord($src{$in++}); + $offset = ($offset << 4) | ($length >> 4); + $length = ($length & 0xF) + 2; + $offset = (int)($out / 4096) * 4096 + $offset; + if ($offset >= $out) $offset -= 4096; + $end = $offset + $length; + while ($offset < $end) { + $dst{$out++} = $dst{$offset++}; + }; + } else { + $dst{$out++} = $src{$in++}; + } + } + $src = $dst; + $dest = substr($src,$this->LZRTF_HDR_LEN,$header['uSize']); + } else { // unknown magic - returfn false (please report if this ever happens) + debugLog("Unknown Magic"); + return false; + } + + return $dest; + } + + // LZRTFCalcCRC32 - calculates the CRC32 of the LZRTF data part + // buf = the whole rtf data part + // off = start point of crc calculation + // len = length of data to calculate CRC for + // function is necessary since in RTF there is no XOR 0xffffffff being done (said to be 0x00 unsafe CRC32 calculation + function LZRTFCalcCRC32($buf, $off, $len) { + $c=0; + $end = $off + $len; + for($i=$off;$i < $end;$i++) { + $c=$this->CRC32_TABLE[($c ^ ord($buf{$i})) & 0xFF] ^ (($c >> 8) & 0x00ffffff); + } + return $c; + } + + function parserInit() { /* Default values according to the specs */ + $this->flags = array( + "fontsize" => 24, + "beginparagraph" => true, + ); + } + + function parseControl($control, $parameter) { + switch ($control) { + case "fonttbl": // font table definition start + $this->flags["fonttbl"] = true; // signal fonttable control words they are allowed to behave as expected + break; + case "f": // define or set font + if($this->flags["fonttbl"]) { // if its set, the fonttable definition is written to; else its read from + $this->flags["fonttbl_current_write"] = $parameter; + } else { + $this->flags["fonttbl_current_read"] = $parameter; + } + break; + case "fcharset": // this is for preparing flushQueue; it then moves the Queue to $this->fonttable .. instead to formatted output + $this->flags["fonttbl_want_fcharset"] = $parameter; + break; + case "fs": // sets the current fontsize; is used by stylesheets (which are therefore generated on the fly + $this->flags["fontsize"] = $parameter; + break; + + case "qc": // handle center alignment + $this->flags["alignment"] = "center"; + break; + case "qr": // handle right alignment + $this->flags["alignment"] = "right"; + break; + + case "pard": // reset paragraph settings (only alignment) + $this->flags["alignment"] = ""; + break; + case "par": // define new paragraph (for now, thats a simple break in html) begin new line + $this->flags["beginparagraph"] = true; + if($this->wantHTML) { + $this->out .= ""; + } + if($this->wantASCII) { + $this->out .= "\n"; + } + break; + case "bnone": // bold + $parameter = "0"; + case "b": + // haven'y yet figured out WHY I need a (string)-cast here ... hm + if((string)$parameter == "0") + $this->flags["bold"] = false; + else + $this->flags["bold"] = true; + break; + case "ulnone": // underlined + $parameter = "0"; + case "ul": + if((string)$parameter == "0") + $this->flags["underlined"] = false; + else + $this->flags["underlined"] = true; + break; + case "inone": // italic + $parameter = "0"; + case "i": + if((string)$parameter == "0") + $this->flags["italic"] = false; + else + $this->flags["italic"] = true; + break; + case "strikenone": // strikethru + $parameter = "0"; + case "strike": + if((string)$parameter == "0") + $this->flags["strikethru"] = false; + else + $this->flags["strikethru"] = true; + break; + case "plain": // reset all font modifiers and fontsize to 12 + $this->flags["bold"] = false; + $this->flags["italic"] = false; + $this->flags["underlined"] = false; + $this->flags["strikethru"] = false; + $this->flags["fontsize"] = 12; + + $this->flags["subscription"] = false; + $this->flags["superscription"] = false; + break; + case "subnone": // subscription + $parameter = "0"; + case "sub": + if((string)$parameter == "0") + $this->flags["subscription"] = false; + else + $this->flags["subscription"] = true; + break; + case "supernone": // superscription + $parameter = "0"; + case "super": + if((string)$parameter == "0") + $this->flags["superscription"] = false; + else + $this->flags["superscription"] = true; + break; + } + } + + /* + Dispatch the control word to the output stream + */ + + function flushControl() { + if(preg_match("/^([A-Za-z]+)(-?[0-9]*) ?$/", $this->cword, $match)) { + $this->parseControl($match[1], $match[2]); + if($this->wantXML) { + $this->out.=" 0) + $this->out.=" param=\"".$match[2]."\""; + $this->out.="/>"; + } + } + } + + /* + If output stream supports comments, dispatch it + */ + + function flushComment($comment) { + if($this->wantXML || $this->wantHTML) { + $this->out.=""; + } + } + + /* + Dispatch start/end of logical rtf groups (not every output type needs it; merely debugging purpose) + */ + + function flushGroup($state) { + if($state == "open") { /* push onto the stack */ + array_push($this->stack, $this->flags); + + if($this->wantXML) + $this->out.=""; + } + if($state == "close") { /* pop from the stack */ + $this->last_flags = $this->flags; + $this->flags = array_pop($this->stack); + + $this->flags["fonttbl_current_write"] = ""; // on group close, no more fontdefinition will be written to this id + // this is not really the right way to do it ! + // of course a '}' not necessarily donates a fonttable end; a fonttable + // group at least *can* contain sub-groups + // therefore an stacked approach is heavily needed + $this->flags["fonttbl"] = false; // no matter what you do, if a group closes, its fonttbl definition is closed too + + if($this->wantXML) + $this->out.=""; + } + } + + function flushHead() { + if($this->wantXML) + $this->out.=""; + } + + function flushBottom() { + if($this->wantXML) + $this->out.=""; + } + + function checkHtmlSpanContent($command) { + reset($this->fontmodifier_table); + while(list($rtf, $html) = each($this->fontmodifier_table)) { + if($this->flags[$rtf] == true) { + if($command == "start") + $this->out .= "<".$html.">"; + else + $this->out .= ""; + } + } + } + + /* + flush text in queue + */ + function flushQueue() { + if(strlen($this->queue)) { + // processing logic + if (isset($this->flags["fonttbl_want_fcharset"]) && + preg_match("/^[0-9]+$/", $this->flags["fonttbl_want_fcharset"])) { + $this->fonttable[$this->flags["fonttbl_want_fcharset"]]["charset"] = $this->queue; + $this->flags["fonttbl_want_fcharset"] = ""; + $this->queue = ""; + } + + // output logic + if (strlen($this->queue)) { + /* + Everything which passes this is (or, at leat, *should*) be only outputted plaintext + Thats why we can safely add the css-stylesheet when using wantHTML + */ + if($this->wantXML) + $this->out.= "".$this->queue.""; + else if($this->wantHTML) { + // only output html if a valid (for now, just numeric;) fonttable is given + if (!isset($this->flags["fonttbl_current_read"])) $this->flags["fonttbl_current_read"] = ""; + if(preg_match("/^[0-9]+$/", $this->flags["fonttbl_current_read"])) { + if($this->flags["beginparagraph"] == true) { + $this->flags["beginparagraph"] = false; + $this->out .= "
flags["alignment"]) { + case "right": + $this->out .= "right"; + break; + case "center": + $this->out .= "center"; + break; + case "left": + default: + $this->out .= "left"; + } + $this->out .= "\">"; + } + + /* define new style for that span */ + $this->styles["f".$this->flags["fonttbl_current_read"]."s".$this->flags["fontsize"]] = "font-family:".$this->fonttable[$this->flags["fonttbl_current_read"]]["charset"]." font-size:".$this->flags["fontsize"].";"; + /* write span start */ + $this->out .= "flags["fonttbl_current_read"]."s".$this->flags["fontsize"]."\">"; + + /* check if the span content has a modifier */ + $this->checkHtmlSpanContent("start"); + /* write span content */ + $this->out .= $this->queue; + /* close modifiers */ + $this->checkHtmlSpanContent("stop"); + /* close span */ + ""; + } + } + $this->queue = ""; + } + } + } + + /* + handle special charactes like \'ef + */ + + function flushSpecial($special) { + if(strlen($special) == 2) { + if($this->wantASCII) + $this->out .= chr(hexdec('0x'.$special)); + else if($this->wantXML) + $this->out .= ""; + else if($this->wantHTML){ + $this->out .= ""; + switch($special) { + case "c1": $this->out .= "Á"; break; + case "e1": $this->out .= "á"; break; + case "c0": $this->out .= "À"; break; + case "e0": $this->out .= "à"; break; + case "c9": $this->out .= "É"; break; + case "e9": $this->out .= "é"; break; + case "c8": $this->out .= "È"; break; + case "e8": $this->out .= "è"; break; + case "cd": $this->out .= "Í"; break; + case "ed": $this->out .= "í"; break; + case "cc": $this->out .= "Ì"; break; + case "ec": $this->out .= "ì"; break; + case "d3": $this->out .= "Ó"; break; + case "f3": $this->out .= "ó"; break; + case "d2": $this->out .= "Ò"; break; + case "f2": $this->out .= "ò"; break; + case "da": $this->out .= "Ú"; break; + case "fa": $this->out .= "ú"; break; + case "d9": $this->out .= "Ù"; break; + case "f9": $this->out .= "ù"; break; + case "80": $this->out .= "€"; break; + case "d1": $this->out .= "Ñ"; break; + case "f1": $this->out .= "ñ"; break; + case "c7": $this->out .= "Ç"; break; + case "e7": $this->out .= "ç"; break; + case "dc": $this->out .= "Ü"; break; + case "fc": $this->out .= "ü"; break; + case "bf": $this->out .= "¿"; break; + case "a1": $this->out .= "¡"; break; + case "b7": $this->out .= "·"; break; + case "a9": $this->out .= "©"; break; + case "ae": $this->out .= "®"; break; + case "ba": $this->out .= "º"; break; + case "aa": $this->out .= "ª"; break; + case "b2": $this->out .= "²"; break; + case "b3": $this->out .= "³"; break; + } + } + } + } + + /* + Output errors at end + */ + function flushErrors() { + if(count($this->err) > 0) { + if($this->wantXML) { + $this->out .= ""; + while(list($num,$value) = each($this->err)) { + $this->out .= "".$value.""; + } + $this->out .= ""; + } + } + } + + function makeStyles() { + $this->outstyles = "\n"; + } + + function parse() { + + $this->parserInit(); + + $i = 0; + $this->cw= false; // flag if control word is currently parsed + $this->cfirst = false; // first control character ? + $this->cword = ""; // last or current control word (depends on $this->cw + + $this->queue = ""; // plain text data found during parsing + + $this->flushHead(); + + while($i < $this->rtf_len) { + switch($this->rtf[$i]) { + case "{": + if($this->cw) { + $this->flushControl(); + $this->cw = false; + $this->cfirst = false; + } else + $this->flushQueue(); + + $this->flushGroup("open"); + break; + case "}": + if($this->cw) { + $this->flushControl(); + $this->cw = false; + $this->cfirst = false; + } else + $this->flushQueue(); + + $this->flushGroup("close"); + break; + case "\\": + if($this->cfirst) { // catches '\\' + $this->queue .= "\\"; // replaced single quotes + $this->cfirst = false; + $this->cw = false; + break; + } + if($this->cw) { + $this->flushControl(); + } else + $this->flushQueue(); + $this->cw = true; + $this->cfirst = true; + $this->cword = ""; + break; + default: + if((ord($this->rtf[$i]) == 10) || (ord($this->rtf[$i]) == 13)) break; // eat line breaks + if($this->cw) { // active control word ? + /* + Watch the RE: there's an optional space at the end which IS part of + the control word (but actually its ignored by flushControl) + */ + if(preg_match("/^[a-zA-Z0-9-]?$/", $this->rtf[$i])) { // continue parsing + $this->cword .= $this->rtf[$i]; + $this->cfirst = false; + } else { + /* + Control word could be a 'control symbol', like \~ or \* etc. + */ + $specialmatch = false; + if($this->cfirst) { + if($this->rtf[$i] == '\'') { // expect to get some special chars + $this->flushQueue(); + $this->flushSpecial($this->rtf[$i+1].$this->rtf[$i+2]); + $i+=2; + $specialmatch = true; + $this->cw = false; + $this->cfirst = false; + $this->cword = ""; + } else + if(preg_match("/^[{}\*]$/", $this->rtf[$i])) { + $this->flushComment("control symbols not yet handled"); + $specialmatch = true; + } + $this->cfirst = false; + } else { + if($this->rtf[$i] == ' ') { // space delimtes control words, so just discard it and flush the controlword + $this->cw = false; + $this->flushControl(); + break; + } + } + if(!$specialmatch) { + $this->flushControl(); + $this->cw = false; + $this->cfirst = false; + /* + The current character is a delimeter, but is NOT + part of the control word so we hop one step back + in the stream and process it again + */ + $i--; + } + } + } else { + // < and > need translation before putting into queue when XML or HTML is wanted + if(($this->wantHTML) || ($this->wantXML)) { + switch($this->rtf[$i]) { + case "<": + $this->queue .= "<"; + break; + case ">": + $this->queue .= ">"; + break; + default: + $this->queue .= $this->rtf[$i]; + break; + } + } else + $this->queue .= $this->rtf[$i]; + } + + } + $i++; + } + $this->flushQueue(); + $this->flushErrors(); + $this->flushBottom(); + + if($this->wantHTML) { + $this->makeStyles(); + } + } +} + + +?> diff --git a/sources/include/z_caldav.php b/sources/include/z_caldav.php new file mode 100644 index 0000000..204e573 --- /dev/null +++ b/sources/include/z_caldav.php @@ -0,0 +1,972 @@ + +* but using cURL instead of home-brew request construction. cURL code re-used +* from carddav.php by Jean-Louis Dupond. Additional bugfixes to +* caldav-client-v2.php by xbgmsharp . +* +* Copyright Andrew McMillan (original caldav-client-v2.php), Jean-Louis Dupond (cURL code), xbgmsharp (bugfixes) +* Copyright Thorsten Köster +* License GNU LGPL version 3 or later (http://www.gnu.org/licenses/lgpl-3.0.txt) +*/ + +require_once('XMLDocument.php'); + +/** +* A class for holding basic calendar information +*/ +class CalendarInfo { + public $url; + public $displayname; + public $getctag; + public $id; + + function __construct( $url, $displayname = null, $getctag = null, $id = null ) { + $this->url = $url; + $this->displayname = $displayname; + $this->getctag = $getctag; + $this->id = $id; + } + + function __toString() { + return( '(URL: '.$this->url.' Ctag: '.$this->getctag.' Displayname: '.$this->displayname .')'. "\n" ); + } +} + + +/** +* A class for accessing DAViCal via CalDAV, as a client +* +* @package awl +*/ +class CalDAVClient { + /** + * Server, username, password, calendar + * + * @var string + */ + protected $server, $base_url, $user, $pass, $auth; + + /** + * The principal-URL we're using + */ + protected $principal_url; + + /** + * The calendar-URL we're using + */ + protected $calendar_url; + + /** + * The calendar-home-set we're using + */ + protected $calendar_home_set; + + /** + * The calendar_urls we have discovered + */ + protected $calendar_urls; + + /** + * The useragent which is send to the caldav server + * + * @var string + */ + const USERAGENT = 'ModifiedDAViCalClient'; + + protected $headers = array(); + protected $xmlResponse = ""; // xml received + protected $httpResponseCode = 0; // http response code + protected $httpResponseHeaders = ""; + protected $httpResponseBody = ""; + + protected $parser; // our XML parser object + + /** + * CardDAV server connection (curl handle) + * + * @var resource + */ + private $curl; + + + private $synctoken = array(); + + /** + * Constructor, initialises the class + * + * @param string $caldav_url The URL for the calendar server + * @param string $user The name of the user logging in + * @param string $pass The password for that user + */ + function __construct( $caldav_url, $user, $pass ) { + $this->user = $user; + $this->pass = $pass; + $this->auth = $user . ':' . $pass; + $this->headers = array(); + + $parsed_url = parse_url($caldav_url); + if ($parsed_url == FALSE) { + ZLog::Write(LOGLEVEL_ERROR, sprintf('Couldn\'t parse URL: %s', $caldav_url)); + } else + $this->server = $parsed_url['scheme'] . '://' . $parsed_url['host'] . ':' . $parsed_url['port']; + $this->base_url = $parsed_url['path']; +// $this->base_url .= !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; +// $this->base_url .= !empty($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; + + if (substr($this->base_url, -1, 1) !== '/') { + $this->base_url = $this->base_url . '/'; + } + } + + + /** + * Adds an If-Match or If-None-Match header + * + * @param bool $match to Match or Not to Match, that is the question! + * @param string $etag The etag to match / not match against. + */ + function SetMatch( $match, $etag = '*' ) { + $this->headers['match'] = sprintf( "%s-Match: \"%s\"", ($match ? "If" : "If-None"), trim($etag,'"')); + } + + /** + * Add a Depth: header. Valid values are 0, 1 or infinity + * + * @param int $depth The depth, default to infinity + */ + function SetDepth( $depth = '0' ) { + $this->headers['depth'] = 'Depth: '. ($depth == '1' ? "1" : ($depth == 'infinity' ? $depth : "0") ); + } + + /** + * Set the calendar_url we will be using for a while. + * + * @param string $url The calendar_url + */ + function SetCalendar( $url ) { + $this->calendar_url = $url; + } + + /** + * Split response into httpResponse and xmlResponse + * + * @param string Response from server + */ + function ParseResponse( $response ) { + $pos = strpos($response, 'xmlResponse = trim(substr($response, $pos)); + $this->xmlResponse = preg_replace('{>[^>]*$}s', '>',$this->xmlResponse ); + $parser = xml_parser_create_ns('UTF-8'); + xml_parser_set_option ( $parser, XML_OPTION_SKIP_WHITE, 1 ); + xml_parser_set_option ( $parser, XML_OPTION_CASE_FOLDING, 0 ); + + if ( xml_parse_into_struct( $parser, $this->xmlResponse, $this->xmlnodes, $this->xmltags ) === 0 ) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("XML parsing error: %s - %s", xml_get_error_code($parser), xml_error_string(xml_get_error_code($parser)))); +// debug_print_backtrace(); +// echo "\nNodes array............................................................\n"; print_r( $this->xmlnodes ); +// echo "\nTags array............................................................\n"; print_r( $this->xmltags ); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("XML Reponse:\n%s\n", $this->xmlResponse)); + } + + xml_parser_free($parser); + } + } + + + public function curl_init() { + if (empty($this->curl)) { + $this->curl = curl_init(); + curl_setopt($this->curl, CURLOPT_HEADER, true); + curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($this->curl, CURLOPT_USERAGENT, self::USERAGENT); + + if ($this->auth !== null) { + curl_setopt($this->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + curl_setopt($this->curl, CURLOPT_USERPWD, $this->auth); + } + } + } + + + /** + * Send a request to the server + * + * @param string $url The URL to make the request to + * + * @return string The content of the response from the server + */ + function DoRequest($url, $method, $content = null, $content_type = "text/plain") { + $this->curl_init(); + + if ( !isset($url) ) $url = $this->base_url; + $url = preg_replace('{^https?://[^/]+}', '', $url); + $url = $this->server . $url; + + curl_setopt($this->curl, CURLOPT_URL, $url); + curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $method); + + if ($content !== null) + { + curl_setopt($this->curl, CURLOPT_POST, true); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, $content); + } + else + { + curl_setopt($this->curl, CURLOPT_POST, false); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, null); + } + + $headers = array(); + $headers['content-type'] = 'Content-type: ' . $content_type; + foreach( $this->headers as $ii => $head ) { + $headers[$ii] = $head; + } + curl_setopt($this->curl, CURLOPT_HTTPHEADER, $headers); + + $this->xmlResponse = ''; + + //ZLog::Write(LOGLEVEL_DEBUG, sprintf("Request:\n%s\n", $content)); + $response = curl_exec($this->curl); + //ZLog::Write(LOGLEVEL_DEBUG, sprintf("Reponse:\n%s\n", $response)); + $header_size = curl_getinfo($this->curl, CURLINFO_HEADER_SIZE); + $this->httpResponseCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE); + $this->httpResponseHeaders = trim(substr($response, 0, $header_size)); + $this->httpResponseBody = substr($response, $header_size); + + $this->headers = array(); // reset the headers array for our next request + $this->ParseResponse($this->httpResponseBody); + return $response; + } + + + /** + * Send an OPTIONS request to the server + * + * @param string $url The URL to make the request to + * + * @return array The allowed options + */ + function DoOptionsRequest( $url = null ) { + $headers = $this->DoRequest($url, "OPTIONS"); + $options_header = preg_replace( '/^.*Allow: ([a-z, ]+)\r?\n.*/is', '$1', $headers ); + $options = array_flip( preg_split( '/[, ]+/', $options_header )); + return $options; + } + + + /** + * Send an XML request to the server (e.g. PROPFIND, REPORT, MKCALENDAR) + * + * @param string $method The method (PROPFIND, REPORT, etc) to use with the request + * @param string $xml The XML to send along with the request + * @param string $url The URL to make the request to + * + * @return array An array of the allowed methods + */ + function DoXMLRequest( $request_method, $xml, $url = null ) { + return $this->DoRequest($url, $request_method, $xml, "text/xml"); + } + + + /** + * Get a single item from the server. + * + * @param string $url The URL to GET + */ + function DoGETRequest( $url ) { + return $this->DoRequest($url, "GET"); + } + + + /** + * Get the HEAD of a single item from the server. + * + * @param string $url The URL to HEAD + */ + function DoHEADRequest( $url ) { + return $this->DoRequest($url, "HEAD"); + } + + + /** + * PUT a text/icalendar resource, returning the etag + * + * @param string $url The URL to make the request to + * @param string $icalendar The iCalendar resource to send to the server + * @param string $etag The etag of an existing resource to be overwritten, or '*' for a new resource. + * + * @return string The content of the response from the server + */ + function DoPUTRequest( $url, $icalendar, $etag = null ) { + if ( $etag != null ) { + $this->SetMatch( ($etag != '*'), $etag ); + } + $this->DoRequest($url, "PUT", $icalendar, 'text/calendar; encoding="utf-8"'); + + $etag = null; + if ( preg_match( '{^ETag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) { + $etag = $matches[1]; + } + if ( !isset($etag) || $etag == '' ) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("No etag in:\n%s\n", $this->httpResponseHeaders)); + $save_request = $this->httpRequest; + $save_response_headers = $this->httpResponseHeaders; + $this->DoHEADRequest( $url ); + if ( preg_match( '{^Etag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) { + $etag = $matches[1]; + } + if ( !isset($etag) || $etag == '' ) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Still No etag in:\n%s\n", $this->httpResponseHeaders)); + } + $this->httpRequest = $save_request; + $this->httpResponseHeaders = $save_response_headers; + } + return $etag; + } + + + /** + * DELETE a text/icalendar resource + * + * @param string $url The URL to make the request to + * @param string $etag The etag of an existing resource to be deleted, or '*' for any resource at that URL. + * + * @return int The HTTP Result Code for the DELETE + */ + function DoDELETERequest( $url, $etag = null ) { + if ( $etag != null ) { + $this->SetMatch( true, $etag ); + } + $this->DoRequest($url, "DELETE"); + return $this->httpResponseCode; + } + + + /** + * Get a single item from the server. + * + * @param string $url The URL to PROPFIND on + */ + function DoPROPFINDRequest( $url, $props, $depth = 0 ) { + $this->SetDepth($depth); + $xml = new XMLDocument( array( 'DAV:' => '', 'urn:ietf:params:xml:ns:caldav' => 'C' ) ); + $prop = new XMLElement('prop'); + foreach( $props AS $v ) { + $xml->NSElement($prop,$v); + } + + $this->DoRequest($url, "PROPFIND", $xml->Render('propfind',$prop), "text/xml"); + return $this->xmlResponse; + } + + + /** + * Get/Set the Principal URL + * + * @param $url string The Principal URL to set + */ + function PrincipalURL( $url = null ) { + if ( isset($url) ) { + $this->principal_url = $url; + } + return $this->principal_url; + } + + + /** + * Get/Set the calendar-home-set URL + * + * @param $url array of string The calendar-home-set URLs to set + */ + function CalendarHomeSet( $urls = null ) { + if ( isset($urls) ) { + if ( !is_array($urls) ) { + $urls = array($urls); + } + $this->calendar_home_set = $urls; + } + return $this->calendar_home_set; + } + + + /** + * Get/Set the calendar-home-set URL + * + * @param $urls array of string The calendar URLs to set + */ + function CalendarUrls( $urls = null ) { + if ( isset($urls) ) { + if ( !is_array($urls) ) { + $urls = array($urls); + } + $this->calendar_urls = $urls; + } + return $this->calendar_urls; + } + + + /** + * Return the first occurrence of an href inside the named tag. + * + * @param string $tagname The tag name to find the href inside of + */ + function HrefValueInside( $tagname ) { + foreach( $this->xmltags[$tagname] AS $k => $v ) { + $j = $v + 1; + if ( $this->xmlnodes[$j]['tag'] == 'DAV::href' ) { + return rawurldecode($this->xmlnodes[$j]['value']); + } + } + return null; + } + + + /** + * Return the href containing this property. Except only if it's inside a status != 200 + * + * @param string $tagname The tag name of the property to find the href for + * @param integer $which Which instance of the tag should we use + */ + function HrefForProp( $tagname, $i = 0 ) { + if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) { + $j = $this->xmltags[$tagname][$i]; + while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' ) { +// printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']); + if ( $this->xmlnodes[$j]['tag'] == 'DAV::status' && $this->xmlnodes[$j]['value'] != 'HTTP/1.1 200 OK' ) { + return null; + } + } +// printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']); + if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) { +// printf( "Value[$j]: %s\n", $this->xmlnodes[$j]['value']); + return rawurldecode($this->xmlnodes[$j]['value']); + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("xmltags[$tagname] or xmltags[$tagname][$i] is not set.")); + } + return null; + } + + + /** + * Return the href which has a resourcetype of the specified type + * + * @param string $tagname The tag name of the resourcetype to find the href for + * @param integer $which Which instance of the tag should we use + */ + function HrefForResourcetype( $tagname, $i = 0 ) { + if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) { + $j = $this->xmltags[$tagname][$i]; + while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::resourcetype' ); + if ( $j > 0 ) { + while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' ); + if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) { + return rawurldecode($this->xmlnodes[$j]['value']); + } + } + } + return null; + } + + + /** + * Return the ... of a propstat where the status is OK + * + * @param string $nodenum The node number in the xmlnodes which is the href + */ + function GetOKProps( $nodenum ) { + $props = null; + $level = $this->xmlnodes[$nodenum]['level']; + $status = ''; + while ( $this->xmlnodes[++$nodenum]['level'] >= $level ) { + if ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::propstat' ) { + if ( $this->xmlnodes[$nodenum]['type'] == 'open' ) { + $props = array(); + $status = ''; + } else { + if ( $status == 'HTTP/1.1 200 OK' ) { + break; + } + } + } elseif ( !isset($this->xmlnodes[$nodenum]) || !is_array($this->xmlnodes[$nodenum]) ) { + break; + } elseif ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::status' ) { + $status = $this->xmlnodes[$nodenum]['value']; + } else { + $props[] = $this->xmlnodes[$nodenum]; + } + } + return $props; + } + + + /** + * Attack the given URL in an attempt to find a principal URL + * + * @param string $url The URL to find the principal-URL from + */ + function FindPrincipal( $url=null ) { + $xml = $this->DoPROPFINDRequest( $url, array('resourcetype', 'current-user-principal', 'owner', 'principal-URL', 'urn:ietf:params:xml:ns:caldav:calendar-home-set'), 1); + + $principal_url = $this->HrefForProp('DAV::principal'); + + if ( !isset($principal_url) ) { + foreach( array('DAV::current-user-principal', 'DAV::principal-URL', 'DAV::owner') AS $href ) { + if ( !isset($principal_url) ) { + $principal_url = $this->HrefValueInside($href); + } + } + } + + return $this->PrincipalURL($principal_url); + } + + + /** + * Attack the given URL in an attempt to find a principal URL + * + * @param string $url The URL to find the calendar-home-set from + */ + function FindCalendarHome( $recursed=false ) { + if ( !isset($this->principal_url) ) { + $this->FindPrincipal(); + } + if ( $recursed ) { + $this->DoPROPFINDRequest( $this->principal_url, array('urn:ietf:params:xml:ns:caldav:calendar-home-set'), 0); + } + + $calendar_home = array(); + foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-home-set'] AS $k => $v ) { + if ( $this->xmlnodes[$v]['type'] != 'open' ) { + continue; + } + while( $this->xmlnodes[++$v]['type'] != 'close' && $this->xmlnodes[$v]['tag'] != 'urn:ietf:params:xml:ns:caldav:calendar-home-set' ) { +// printf( "Tag: '%s' = '%s'\n", $this->xmlnodes[$v]['tag'], $this->xmlnodes[$v]['value']); + if ( $this->xmlnodes[$v]['tag'] == 'DAV::href' && isset($this->xmlnodes[$v]['value']) ) { + $calendar_home[] = rawurldecode($this->xmlnodes[$v]['value']); + } + } + } + + if ( !$recursed && count($calendar_home) < 1 ) { + $calendar_home = $this->FindCalendarHome(true); + } + + return $this->CalendarHomeSet($calendar_home); + } + + + /** + * Find the calendars, from the calendar_home_set + */ + function FindCalendars( $recursed=false ) { + if ( !isset($this->calendar_home_set[0]) ) { + $this->FindCalendarHome(); + } + $this->DoPROPFINDRequest( $this->calendar_home_set[0], array('resourcetype','displayname','http://calendarserver.org/ns/:getctag'), 1); + + $calendars = array(); + if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar']) ) { + $calendar_urls = array(); + foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar'] AS $k => $v ) { + $calendar_urls[$this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar', $k)] = 1; + } + + foreach( $this->xmltags['DAV::href'] AS $i => $hnode ) { + $href = rawurldecode($this->xmlnodes[$hnode]['value']); + + if ( !isset($calendar_urls[$href]) ) { + continue; + } + +// printf("Seems '%s' is a calendar.\n", $href ); + + $calendar = new CalendarInfo($href); + $ok_props = $this->GetOKProps($hnode); + foreach( $ok_props AS $v ) { +// printf("Looking at: %s[%s]\n", $href, $v['tag'] ); + switch( $v['tag'] ) { + case 'http://calendarserver.org/ns/:getctag': + $calendar->getctag = $v['value']; + break; + case 'DAV::displayname': + $calendar->displayname = $v['value']; + break; + } + } + $calendar->id = rtrim(str_replace($this->calendar_home_set[0], "", $calendar->url), "/"); + $calendars[] = $calendar; + } + } + + return $this->CalendarUrls($calendars); + } + + + /** + * Find the calendars, from the calendar_home_set + */ + function GetCalendarDetails( $url = null ) { + if ( isset($url) ) { + $this->SetCalendar($url); + } + if ( !isset($this->calendar_home_set[0]) ) { + $this->FindCalendarHome(); + } + + $calendar_properties = array( 'resourcetype', 'displayname', 'http://calendarserver.org/ns/:getctag', 'urn:ietf:params:xml:ns:caldav:calendar-timezone', 'supported-report-set' ); + $this->DoPROPFINDRequest( $this->calendar_url, $calendar_properties, 0); + + $hnode = $this->xmltags['DAV::href'][0]; + $href = rawurldecode($this->xmlnodes[$hnode]['value']); + + $calendar = new CalendarInfo($href); + $ok_props = $this->GetOKProps($hnode); + foreach( $ok_props AS $k => $v ) { + $name = preg_replace( '{^.*:}', '', $v['tag'] ); + if ( isset($v['value'] ) ) { + $calendar->{$name} = $v['value']; + } /* else { + printf( "Calendar property '%s' has no text content\n", $v['tag'] ); + }*/ + } + $calendar->id = rtrim(str_replace($this->calendar_home_set[0], "", $calendar->url), "/"); + + return $calendar; + } + + + /** + * Get all etags for a calendar + */ + function GetCollectionETags( $url = null ) { + if ( isset($url) ) { + $this->SetCalendar($url); + } + + $this->DoPROPFINDRequest( $this->calendar_url, array('getetag'), 1); + + $etags = array(); + if ( isset($this->xmltags['DAV::getetag']) ) { + foreach( $this->xmltags['DAV::getetag'] AS $k => $v ) { + $href = $this->HrefForProp('DAV::getetag', $k); + if ( isset($href) && isset($this->xmlnodes[$v]['value']) ) { + $etags[$href] = $this->xmlnodes[$v]['value']; + } + } + } + + return $etags; + } + + + /** + * Get a bunch of events for a calendar with a calendar-multiget report + */ + function CalendarMultiget( $event_hrefs, $url = null ) { + if ( isset($url) ) { + $this->SetCalendar($url); + } + + $hrefs = ''; + foreach( $event_hrefs AS $k => $href ) { + $href = str_replace( rawurlencode('/'),'/',rawurlencode($href)); + $hrefs .= ''.$href.''; + } + $body = << + + +$hrefs + +EOXML; + + $this->DoRequest($this->calendar_url, "REPORT", $body, "text/xml"); + + $events = array(); + if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data']) ) { + foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data'] AS $k => $v ) { + $href = $this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar-data', $k); +// echo "Calendar-data:\n"; print_r($this->xmlnodes[$v]); + $events[$href] = $this->xmlnodes[$v]['value']; + } + } else { + foreach( $event_hrefs AS $k => $href ) { + $this->DoGETRequest($href); + $events[$href] = $this->httpResponseBody; + } + } + + return $events; + } + + + /** + * Given XML for a calendar query, return an array of the events (/todos) in the + * response. Each event in the array will have a 'href', 'etag' and '$response_type' + * part, where the 'href' is relative to the calendar and the '$response_type' contains the + * definition of the calendar data in iCalendar format. + * + * @param string $filter XML fragment which is the element of a calendar-query + * @param string $url The URL of the calendar, or empty/null to use the 'current' calendar_url + * + * @return array An array of the relative URLs, etags, and events from the server. Each element of the array will + * be an array with 'href', 'etag' and 'data' elements, corresponding to the URL, the server-supplied + * etag (which only varies when the data changes) and the calendar data in iCalendar format. + */ + function DoCalendarQuery( $filter, $url = null ) { + if ( !empty($url) ) { + $this->SetCalendar($url); + } + + $body = << + + + + + + $filter + +EOXML; + + $this->SetDepth(1); + $this->DoRequest($this->calendar_url, "REPORT", $body, "text/xml"); + + $report = array(); + foreach( $this->xmlnodes as $k => $v ) { + switch( $v['tag'] ) { + case 'DAV::response': + if ( $v['type'] == 'open' ) { + $response = array(); + } elseif ( $v['type'] == 'close' ) { + $report[] = $response; + } + break; + case 'DAV::href': + $response['href'] = basename( rawurldecode($v['value']) ); + break; + case 'DAV::getetag': + $response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']); + break; + case 'urn:ietf:params:xml:ns:caldav:calendar-data': + $response['data'] = $v['value']; + break; + } + } + return $report; + } + + + /** + * Get the events in a range from $start to $finish. The dates should be in the + * format yyyymmddThhmmssZ and should be in GMT. The events are returned as an + * array of event arrays. Each event array will have a 'href', 'etag' and 'event' + * part, where the 'href' is relative to the calendar and the event contains the + * definition of the event in iCalendar format. + * + * @param timestamp $start The start time for the period + * @param timestamp $finish The finish time for the period + * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''. + * + * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery() + */ + function GetEvents( $start = null, $finish = null, $relative_url = null ) { + $filter = ""; + if ( isset($start) && isset($finish) ) { + $range = ""; + } else { + $range = ''; + } + + $filter = << + + + $range + + + +EOFILTER; + + return $this->DoCalendarQuery($filter, $relative_url); + } + + + /** + * Get the todo's in a range from $start to $finish. The dates should be in the + * format yyyymmddThhmmssZ and should be in GMT. The events are returned as an + * array of event arrays. Each event array will have a 'href', 'etag' and 'event' + * part, where the 'href' is relative to the calendar and the event contains the + * definition of the event in iCalendar format. + * + * @param timestamp $start The start time for the period + * @param timestamp $finish The finish time for the period + * @param boolean $completed Whether to include completed tasks + * @param boolean $cancelled Whether to include cancelled tasks + * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''. + * + * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery() + */ + function GetTodos( $start, $finish, $completed = false, $cancelled = false, $relative_url = null ) { + + if ( $start && $finish ) { + $time_range = << +EOTIME; + } else { + $time_range = ""; + } + + // Warning! May contain traces of double negatives... + $neg_cancelled = ( $cancelled === true ? "no" : "yes" ); + $neg_completed = ( $cancelled === true ? "no" : "yes" ); + + $filter = << + + + + COMPLETED + + + CANCELLED + $time_range + + + +EOFILTER; + + return $this->DoCalendarQuery($filter, $relative_url); + } + + + /** + * Get the calendar entry by UID + * + * @param uid + * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''. + * @param string $component_type The component type inside the VCALENDAR. Default 'VEVENT'. + * + * @return array An array of the relative URL, etag, and calendar data returned from DoCalendarQuery() @see DoCalendarQuery() + */ + function GetEntryByUid( $uid, $relative_url = null, $component_type = 'VEVENT' ) { + $filter = ""; + if ( $uid ) { + $filter = << + + + + $uid + + + + +EOFILTER; + } + + return $this->DoCalendarQuery($filter, $relative_url); + } + + + /** + * Get the calendar entry by HREF + * + * @param string $href The href from a call to GetEvents or GetTodos etc. + * + * @return string The iCalendar of the calendar entry + */ + function GetEntryByHref( $href ) { + $href = str_replace( rawurlencode('/'),'/',rawurlencode($href)); + return $this->DoGETRequest( $href ); + } + + + /** + * Do a Sync operation. This is the fastest way to detect changes. + * + * @param string $url URL for the calendar + * @param boolean $initial It's the first synchronization + * @param boolean $support_dav_sync The CalDAV server supports sync-collection + * + * @return array of responses + */ + public function GetSync($relative_url = null, $initial = true, $support_dav_sync = false) { + if (!empty($relative_url)) { + $this->SetCalendar($relative_url); + } + + if ($support_dav_sync) { + $token = ($initial ? "" : $this->synctoken[$this->calendar_url]); + + $body = << + + $token + 1 + + + + + +EOXML; + } + else { + $body = << + + + + + + + + + +EOXML; + } + + $this->SetDepth(1); + $this->DoRequest($this->calendar_url, "REPORT", $body, "text/xml"); + + $report = array(); + foreach( $this->xmlnodes as $k => $v ) { + switch( $v['tag'] ) { + case 'DAV::response': + if ( $v['type'] == 'open' ) { + $response = array(); + } elseif ( $v['type'] == 'close' ) { + $report[] = $response; + } + break; + case 'DAV::href': + $response['href'] = basename( rawurldecode($v['value']) ); + break; + case 'DAV::getlastmodified': + if (isset($v['value'])) { + $response['getlastmodified'] = $v['value']; + } + else { + $response['getlastmodified'] = ''; + } + break; + case 'DAV::getetag': + $response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']); + break; + case 'DAV::sync-token': + $this->synctoken[$this->calendar_url] = $v['value']; + break; + } + } + return $report; + } + +} diff --git a/sources/include/z_carddav.php b/sources/include/z_carddav.php new file mode 100644 index 0000000..5858f46 --- /dev/null +++ b/sources/include/z_carddav.php @@ -0,0 +1,892 @@ +set_auth('username', 'password'); + * echo $carddav->get(); + * + * + * Simple vCard query + * ------------------ + * $carddav = new carddav_backend('https://davical.example.com/user/contacts/'); + * $carddav->set_auth('username', 'password'); + * echo $carddav->get_vcard('0126FFB4-2EB74D0A-302EA17F'); + * + * + * XML vCard query + * ------------------ + * $carddav = new carddav_backend('https://davical.example.com/user/contacts/'); + * $carddav->set_auth('username', 'password'); + * echo $carddav->get_xml_vcard('0126FFB4-2EB74D0A-302EA17F'); + * + * + * Check CardDAV server connection + * ------------------------------- + * $carddav = new carddav_backend('https://davical.example.com/user/contacts/'); + * $carddav->set_auth('username', 'password'); + * var_dump($carddav->check_connection()); + * + * + * CardDAV delete query + * -------------------- + * $carddav = new carddav_backend('https://davical.example.com/user/contacts/'); + * $carddav->set_auth('username', 'password'); + * $carddav->delete('0126FFB4-2EB74D0A-302EA17F'); + * + * + * CardDAV add query + * -------------------- + * $vcard = 'BEGIN:VCARD + * VERSION:3.0 + * UID:1f5ea45f-b28a-4b96-25as-ed4f10edf57b + * FN:Christian Putzke + * N:Christian;Putzke;;; + * EMAIL;TYPE=OTHER:christian.putzke@graviox.de + * END:VCARD'; + * + * $carddav = new carddav_backend('https://davical.example.com/user/contacts/'); + * $carddav->set_auth('username', 'password'); + * $vcard_id = $carddav->add($vcard); + * + * + * CardDAV update query + * -------------------- + * $vcard = 'BEGIN:VCARD + * VERSION:3.0 + * UID:1f5ea45f-b28a-4b96-25as-ed4f10edf57b + * FN:Christian Putzke + * N:Christian;Putzke;;; + * EMAIL;TYPE=OTHER:christian.putzke@graviox.de + * END:VCARD'; + * + * $carddav = new carddav_backend('https://davical.example.com/user/contacts/'); + * $carddav->set_auth('username', 'password'); + * $carddav->update($vcard, '0126FFB4-2EB74D0A-302EA17F'); + * + * + * CardDAV debug + * ------------- + * $carddav = new carddav_backend('https://davical.example.com/user/contacts/'); + * $carddav->enable_debug(); + * $carddav->set_auth('username', 'password'); + * $carddav->get(); + * var_dump($carddav->get_debug()); + * + * + * CardDAV server list + * ------------------- + * DAViCal: https://example.com/{resource|principal|username}/{collection}/ + * Apple Addressbook Server: https://example.com/addressbooks/users/{resource|principal|username}/{collection}/ + * memotoo: https://sync.memotoo.com/cardDAV/ + * SabreDAV: https://example.com/addressbooks/{resource|principal|username}/{collection}/ + * ownCloud: https://example.com/apps/contacts/carddav.php/addressbooks/{resource|principal|username}/{collection}/ + * SOGo: https://example.com/SOGo/dav/{resource|principal|username}/Contacts/{collection}/ + * + * + * @author Christian Putzke + * @copyright Christian Putzke + * @link http://www.graviox.de/ + * @link https://twitter.com/cputzke/ + * @since 20.07.2011 + * @version 0.6 + * @license http://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + * + */ + +class carddav_backend +{ + /** + * CardDAV PHP Version + * + * @constant string + */ + const VERSION = '0.6.c'; + + /** + * User agent displayed in http requests + * + * @constant string + */ + const USERAGENT = 'Z-Push CardDAV/'; + + /** + * CardDAV server url + * + * @var string + */ + private $url = null; + + /** + * CardDAV server url_parts + * + * @var array + */ + private $url_parts = null; + + /** + * Authentication string + * + * @var string + */ + private $auth = null; + + /** + * Authentication: username + * + * @var string + */ + private $username = null; + + /** + * Authentication: password + * + * @var string + */ + private $password = null; + + /** + * Characters used for vCard id generation + * + * @var array + */ + private $vcard_id_chars = array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'A', 'B', 'C', 'D', 'E', 'F'); + + /** + * CardDAV server connection (curl handle) + * + * @var resource + */ + private $curl; + + /** + * Debug on or off + * + * @var boolean + */ + private $debug = false; + + /** + * All available debug information + * + * @var array + */ + private $debug_information = array(); + + + /** + * Sync-token for sync-collection operations. + * + * @var array[string] + */ + private $synctoken = array(); + + + /* VCard File URL Extension + * + * @var string + */ + private $url_vcard_extension; + + /** + * Exception codes + */ + const EXCEPTION_WRONG_HTTP_STATUS_CODE_GET = 1000; + const EXCEPTION_WRONG_HTTP_STATUS_CODE_GET_VCARD = 1001; + const EXCEPTION_WRONG_HTTP_STATUS_CODE_GET_XML_VCARD = 1002; + const EXCEPTION_WRONG_HTTP_STATUS_CODE_DELETE = 1003; + const EXCEPTION_WRONG_HTTP_STATUS_CODE_ADD = 1004; + const EXCEPTION_WRONG_HTTP_STATUS_CODE_UPDATE = 1005; + const EXCEPTION_MALFORMED_XML_RESPONSE = 1006; + const EXCEPTION_COULD_NOT_GENERATE_NEW_VCARD_ID = 1007; + const EXCEPTION_COULD_NOT_FIND_VCARD_HREF = 1008; + + + /** + * Constructor + * Sets the CardDAV server url + * + * @param string $url CardDAV server url + * @param string $url_vcard_extension extension needed to recover the vcard, it could be empty + */ + public function __construct($url = null, $url_vcard_extension = '.vcf') { + if ($url !== null) { + $this->set_url($url); + } + $this->url_vcard_extension = $url_vcard_extension; + } + + /** + * Sets debug information + * + * @param array $debug_information Debug information + * @return void + */ + public function set_debug(array $debug_information) { + $this->debug_information[] = $debug_information; + } + + /** + * Sets the CardDAV server url + * + * @param string $url CardDAV server url + * @return void + */ + public function set_url($url) { + $this->url = $url; + + // Url always end with trailing / + if (substr($this->url, -1, 1) !== '/') { + $this->url .= '/'; + } + + $this->url_parts = parse_url($this->url); + } + + /** + * Sets authentication information + * + * @param string $username CardDAV server username + * @param string $password CardDAV server password + * @return void + */ + public function set_auth($username, $password) { + $this->username = $username; + $this->password = $password; + $this->auth = $username . ':' . $password; + } + + /** + * Gets all available debug information + * + * @return array $this->debug_information All available debug information + */ + public function get_debug() { + return $this->debug_information; + } + + /** + * Sets the CardDAV vcard url extension + * + * Most providers do requests handling Vcards with .vcf, however + * this isn't always the case and some providers (such as Google) + * returned a 404 if the .vcf extension is used - or the other + * way around, returning 404 unless .vcf is used. + * + * Both approaches are technically correct, see rfc635 + * http://tools.ietf.org/html/rfc6352 + * + * + * @param string $extension File extension + * @return void + */ + public function set_vcard_extension($extension) { + $this->url_vcard_extension = $extension; + } + + /** + * Gets all vCards including additional information from the CardDAV server. + * This operation could be slow if you have a lot of vcards. + * + * @param boolean $include_vcards Include vCards within the response (simplified only) + * @param boolean $raw Get response raw or simplified + * @params boolean $discover Only discover addressbooks + * @return string Raw or simplified XML response + */ + public function get($include_vcards = true, $raw = false, $discover = false) { +// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->get")); + if ($discover) { + $result = $this->query($this->url, 'PROPFIND', null, null, '1'); + } + else { + $result = $this->query($this->url, 'PROPFIND'); + } + + switch ($result['http_code']) { + case 200: + case 207: + if ($raw === true) { + return $result['response']; + } + else { + return $this->simplify($result['response'], $include_vcards); + } + break; + + default: + throw new Exception('Woops, something\'s gone wrong! The CardDAV server returned the http status code ' . $result['http_code'] . '.', self::EXCEPTION_WRONG_HTTP_STATUS_CODE_GET); + break; + } + } + + + /** + * Get all vcards matching a full name or mail. + * + * @param string $pattern Pattern to search + * @param integer $limit Return only N vcards + * @param boolean $include_vcards Include vCards within the response (simplified only) + * @param boolean $raw Get response raw or simplified + * @param boolean $support_fn_search If the server supports searchs by fn + * @return string Raw or simplified XML response + */ + public function search_vcards($pattern, $limit, $include_vcards = true, $raw = false, $support_fn_search = false) { +// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->search_vcards")); + if ($support_fn_search) { + $xml = << + + + + + + + + + + $pattern + + + + $limit + + +EOFCONTENTSEARCH; + } + else { + $xml = << + + + + + + + + + + $pattern + + + $pattern + + + $pattern + + + + $limit + + +EOFCONTENTSEARCH; + } + + return $this->do_query_report($xml, $include_vcards, $raw, true); + } + + /** + * Get all vcards or changes since the last sync. + * + * @param boolean $initial If the sync should be full + * @param boolean $include_vcards If the vCards should be included within the response + * @param boolean $support_carddav_sync If the cardDAV server supports sync-collection operations (DAViCal supports it) + * @return string Simplified XML response + */ + public function do_sync($initial = true, $include_vcards = false, $support_carddav_sync = false) { +// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->do_sync")); + + if ($support_carddav_sync) { + if ($initial) { + $token = ""; + } + else { + $token = $this->synctoken[$this->url]; + } + + $xml = << + + $token + 1 + + + + + +EOFXMLINITIALSYNC; + + return $this->do_query_report($xml, $include_vcards, false); + } + else { + return $this->get($include_vcards, false); + } + } + + + /** + * Do a REPORT query against the server + * + * @param string $xml XML body request + * @param boolean $include_vcards If the vCards should be included within the response + * @param boolean $raw If the response should be raw or XML simplified + * @param boolean $remove_duplicates If we will apply uniqness to the response vcards + * @return string + */ + private function do_query_report($xml, $include_vcards = true, $raw = false, $remove_duplicates = false) { +// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->do_query_report")); + $result = $this->query($this->url, 'REPORT', $xml, 'text/xml'); + + try { + switch ($result['http_code']) { + case 200: + case 207: + if ($raw === true) { + return $result['response']; + } + else { + return $this->simplify($result['response'], $include_vcards, $remove_duplicates); + } + break; + + default: + throw new Exception('Woops, something\'s gone wrong! The CardDAV server returned the http status code ' . $result['http_code'] . '.', self::EXCEPTION_WRONG_HTTP_STATUS_CODE_GET); + break; + } + } + catch(Exception $ex) { + // vcard not found + if ($ex->getCode() == self::EXCEPTION_COULD_NOT_FIND_VCARD_HREF) { + if (strlen($this->url_vcard_extension) == 0 || stripos($xml, $this->url_vcard_extension) === FALSE) { + throw $ex; + } + else { + // try to do the same without the $this->url_vcard_extension + return $this->do_query_report(str_ireplace($this->url_vcard_extension, "", $xml), $include_vcards, $raw, $remove_duplicates); + } + } + else { + throw $ex; + } + } + } + + /** + * Gets a clean vCard from the CardDAV server + * + * @param string $vcard_href vCard href on the CardDAV server + * @return string vCard (text/vcard) + */ + private function get_vcard($vcard_href) { +// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->get_vcard")); + $url = $this->url_parts['scheme'] . '://' . $this->url_parts['host'] . ':' . $this->url_parts['port'] . $vcard_href; + $result = $this->query($url, 'GET'); + + switch ($result['http_code']) { + case 200: + case 207: + return $result['response']; + break; + + default: + throw new Exception('Woops, something\'s gone wrong! The CardDAV server returned the http status code ' . $result['http_code'] . '.', self::EXCEPTION_WRONG_HTTP_STATUS_CODE_GET_VCARD); + break; + } + } + + /** + * Gets a vCard + XML from the CardDAV Server + * + * @param string $vcard_id vCard id on the CardDAV Server + * @return string Raw or simplified vCard (text/xml) + */ + public function get_xml_vcard($vcard_id) { +// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->get_xml_vcard")); + $href = $this->url_parts['path'] . str_replace($this->url_vcard_extension, null, $vcard_id) . $this->url_vcard_extension; + + // If we don't ask for allprop, SOGo doesn't return the content_type + $xml = << + + + + + + + + + $href + +EOFXMLGETXMLVCARD; + + return $this->do_query_report($xml); + } + + /** + * Enables the debug mode + * + * @return void + */ + public function enable_debug() { + $this->debug = true; + } + + /** + * Checks if the CardDAV server is reachable + * + * @return boolean + */ + public function check_connection() { +// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->check_connection")); + $result = $this->query($this->url, 'OPTIONS'); + + $status = false; + switch($result['http_code']) { + case 200: + case 207: + case 401: + $status = true; + break; + } + + return $status; + } + + /** + * Deletes an entry from the CardDAV server + * + * @param string $vcard_id vCard id on the CardDAV server + * @return boolean + */ + public function delete($vcard_id) { +// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->delete")); + $result = $this->query($this->url . $vcard_id . $this->url_vcard_extension, 'DELETE'); + + + switch ($result['http_code']) { + case 204: + return true; + break; + + default: + throw new Exception('Woops, something\'s gone wrong! The CardDAV server returned the http status code ' . $result['http_code'] . '.', self::EXCEPTION_WRONG_HTTP_STATUS_CODE_DELETE); + break; + } + } + + /** + * Adds an entry to the CardDAV server + * + * @param string $vcard vCard + * @param string $vcard_id vCard id on the CardDAV server + * @return string The new vCard id + */ + public function add($vcard, $vcard_id = null) { +// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->add")); + if ($vcard_id === null) { + $vcard_id = $this->generate_vcard_id(); + } + + $vcard = str_replace("\nEND:VCARD","\nUID:" . $vcard_id . "\r\nEND:VCARD", $vcard); + $result = $this->query($this->url . $vcard_id . $this->url_vcard_extension, 'PUT', $vcard, 'text/vcard'); + + + switch($result['http_code']) { + case 201: + case 204: + return $vcard_id; + break; + + default: + throw new Exception('Woops, something\'s gone wrong! The CardDAV server returned the http status code ' . $result['http_code'] . '.', self::EXCEPTION_WRONG_HTTP_STATUS_CODE_ADD); + break; + } + } + + /** + * Updates an entry to the CardDAV server + * + * @param string $vcard vCard + * @param string $vcard_id vCard id on the CardDAV server + * @return boolean + */ + public function update($vcard, $vcard_id) { + try { + return $this->add($vcard, $vcard_id); + } + catch (Exception $e) { + throw new Exception($e->getMessage(), self::EXCEPTION_WRONG_HTTP_STATUS_CODE_UPDATE); + } + } + + /** + * Simplify CardDAV XML response + * + * @param string $response CardDAV XML response + * @param boolean $include_vcards Include vCards or not + * @param boolean $remove_duplicates If we will apply uniqness to the response vcards + * @return string Simplified CardDAV XML response + */ + private function simplify($response, $include_vcards = true, $remove_duplicates = false) { + $response = $this->remove_namespaces($response); + + try { + $xml = new SimpleXMLElement($response); + } + catch(Exception $e) { + throw new Exception('The XML response seems to be malformed and can\'t be simplified!', self::EXCEPTION_MALFORMED_XML_RESPONSE, $e); + } + + if (!empty($xml->{'sync-token'})) { + $this->synctoken[$this->url] = $xml->{'sync-token'}; + } + + $simplified_xml = new XMLWriter(); + $simplified_xml->openMemory(); + $simplified_xml->setIndent(4); + + $simplified_xml->startDocument('1.0', 'utf-8'); + $simplified_xml->startElement('response'); + + if (!empty($xml->response)) { + $unique_etags = array(); + + foreach ($xml->response as $response) { + if (isset($response->propstat)) { + if ((strlen($this->url_vcard_extension) > 0 && preg_match('/'.$this->url_vcard_extension.'/', $response->href) && + !(isset($response->propstat->prop->resourcetype) && isset($response->propstat->prop->resourcetype->addressbook))) + || preg_match('/vcard/', $response->propstat->prop->getcontenttype) || isset($response->propstat->prop->{'address-data'}) || isset($response->propstat->prop->{'addressbook-data'})) { + // It's a vcard + $id = basename($response->href); + $id = str_replace($this->url_vcard_extension, null, $id); + + if (!empty($id)) { + $simplified_xml->startElement('element'); + $simplified_xml->writeElement('id', $id); + $simplified_xml->writeElement('etag', str_replace('"', null, $response->propstat->prop->getetag)); + $simplified_xml->writeElement('last_modified', $response->propstat->prop->getlastmodified); + + if ($include_vcards === true) { + if (isset($response->propstat->prop->{'address-data'})) { + // We already have the full vcard + $simplified_xml->writeElement('vcard', $response->propstat->prop->{'address-data'}); + } + else if (isset($response->propstat->prop->{'addressbook-data'})) { + // We already have the full vcard, also + $simplified_xml->writeElement('vcard', $response->propstat->prop->{'addressbook-data'}); + } + else { + // We don't have the vcard, we need to get it. We never should hit here, it would mean a buggy server + $simplified_xml->writeElement('vcard', $this->get_vcard($response->href)); + } + } + $simplified_xml->endElement(); + } + } + else if (isset($response->propstat->prop->resourcetype->addressbook)) { + // It's an addressbook + if (isset($response->propstat->prop->href)) { + $href = $response->propstat->prop->href; + } + else if (isset($response->href)) { + $href = $response->href; + } + else { + $href = null; + } + + $url = str_replace($this->url_parts['path'], null, $this->url) . $href; + $simplified_xml->startElement('addressbook_element'); + $simplified_xml->writeElement('display_name', $response->propstat->prop->displayname); + $simplified_xml->writeElement('url', $url); + $simplified_xml->writeElement('last_modified', $response->propstat->prop->getlastmodified); + $simplified_xml->endElement(); + } + } + else { + // We don't have a propstat node, so it will be an error answer + if (isset($response->status) && preg_match('/404 Not Found/', $response->status)) { + throw new Exception('Not found!', self::EXCEPTION_COULD_NOT_FIND_VCARD_HREF); + } + else { + throw new Exception('The XML response is an error message and can\'t be simplified!', self::EXCEPTION_MALFORMED_XML_RESPONSE); + } + } + } + + unset($unique_etags); + } + + $simplified_xml->endElement(); + $simplified_xml->endDocument(); + + return $simplified_xml->outputMemory(); + } + + /** + * Cleans CardDAV XML response + * + * @param string $response CardDAV XML response + * @return string $response Cleaned CardDAV XML response + */ + private function remove_namespaces($response) { +// $response = preg_replace('/<[a-z0-9]+:(.*)/i', '<$1', $response); +// $response = preg_replace('/<\/[a-z0-9]+:(.*)/i', ' + + + + + + +EOFXSL; + + $dom = new DOMDocument(); + $dom->loadXML($response); + $stylesheet = new DOMDocument(); + $stylesheet->loadXML($xsl); + $xsltprocessor = new XSLTProcessor(); + $xsltprocessor->importStylesheet($stylesheet); + + $response = $xsltprocessor->transformToXML($dom); + $dom = null; + $stylesheet = null; + $xsltprocessor = null; + + return $response; + } + + /** + * Curl initialization + * + * @return void + */ + public function curl_init() { + if (empty($this->curl)) { + $this->curl = curl_init(); + curl_setopt($this->curl, CURLOPT_HEADER, true); + curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($this->curl, CURLOPT_USERAGENT, self::USERAGENT.self::VERSION); + + if ($this->auth !== null) { + curl_setopt($this->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + curl_setopt($this->curl, CURLOPT_USERPWD, $this->auth); + } + } + } + + /** + * Query the CardDAV server via curl and returns the response + * + * @param string $url CardDAV server URL + * @param string $method HTTP method like (OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, COPY, MOVE) + * @param string $content Content for CardDAV queries + * @param string $content_type Set content type + * @param string $depth Set Depth + * @return array Raw CardDAV Response and http status code + */ + private function query($url, $method, $content = null, $content_type = null, $depth = "infinity") { +// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->query - '%s' '%s' '%s' '%s'", $url, $method, $content, $content_type)); + + $this->curl_init(); + + curl_setopt($this->curl, CURLOPT_URL, $url); + curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $method); + + if ($content !== null) { + curl_setopt($this->curl, CURLOPT_POST, true); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, $content); + } + else { + curl_setopt($this->curl, CURLOPT_POST, false); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, null); + } + + if ($content_type !== null) { + curl_setopt($this->curl, CURLOPT_HTTPHEADER, array('Content-type: '.$content_type. '; charset=utf-8', 'Depth: '.$depth)); + } + else { + curl_setopt($this->curl, CURLOPT_HTTPHEADER, array('Depth: '.$depth)); + } + + $complete_response = curl_exec($this->curl); + $header_size = curl_getinfo($this->curl, CURLINFO_HEADER_SIZE); + $http_code = curl_getinfo($this->curl, CURLINFO_HTTP_CODE); + $header = trim(substr($complete_response, 0, $header_size)); + $response = substr($complete_response, $header_size); + + $return = array( + 'response' => $response, + 'http_code' => $http_code + ); + + if ($this->debug === true) { + $debug = $return; + $debug['url'] = $url; + $debug['method'] = $method; + $debug['content'] = $content; + $debug['content_type'] = $content_type; + $debug['header'] = $header; + $this->set_debug($debug); + } + + return $return; + } + + /** + * Returns a valid and unused vCard id + * + * @return string $vcard_id Valid vCard id + */ + private function generate_vcard_id() { + $vcard_id = null; + + for ($number = 0; $number <= 25; $number ++) { + if ($number == 8 || $number == 17) { + $vcard_id .= '-'; + } + else { + $vcard_id .= $this->vcard_id_chars[mt_rand(0, (count($this->vcard_id_chars) - 1))]; + } + } + + try { + $carddav = new carddav_backend($this->url); + $carddav->set_auth($this->username, $this->password); + + $result = $carddav->query($this->url . $vcard_id . $this->url_vcard_extension, 'GET'); + + if ($result['http_code'] !== 404) { + $vcard_id = $this->generate_vcard_id(); + } + + return $vcard_id; + } + catch (Exception $e) { + throw new Exception($e->getMessage(), self::EXCEPTION_COULD_NOT_GENERATE_NEW_VCARD_ID); + } + } + + /** + * Destructor + * Close curl connection if it's open + * + * @return void + */ + public function __destruct() { + if (!empty($this->curl)) { + curl_close($this->curl); + } + } +} diff --git a/sources/index.php b/sources/index.php index 182b8a4..345c808 100644 --- a/sources/index.php +++ b/sources/index.php @@ -133,15 +133,24 @@ include_once('version.php'); Request::Initialize(); ZLog::Initialize(); + $autenticationInfo = Request::AuthenticationInfo(); + $GETUser = Request::GetGETUser(); + ZLog::Write(LOGLEVEL_DEBUG,"-------- Start"); ZLog::Write(LOGLEVEL_INFO, sprintf("Version='%s' method='%s' from='%s' cmd='%s' getUser='%s' devId='%s' devType='%s'", @constant('ZPUSH_VERSION'), Request::GetMethod(), Request::GetRemoteAddr(), - Request::GetCommand(), Request::GetGETUser(), Request::GetDeviceID(), Request::GetDeviceType())); + Request::GetCommand(), $GETUser, Request::GetDeviceID(), Request::GetDeviceType())); // Stop here if this is an OPTIONS request - if (Request::IsMethodOPTIONS()) - throw new NoPostRequestException("Options request", NoPostRequestException::OPTIONS_REQUEST); + if (Request::IsMethodOPTIONS()) { + if (!$autenticationInfo || !$GETUser) { + throw new AuthenticationRequiredException("Access denied. Please send authorisation information"); + } + else { + throw new NoPostRequestException("Options request", NoPostRequestException::OPTIONS_REQUEST); + } + } ZPush::CheckAdvancedConfig(); @@ -152,11 +161,22 @@ include_once('version.php'); if(Request::IsMethodPOST() && (Request::GetCommandCode() === false || !Request::GetDeviceID() || !Request::GetDeviceType())) throw new FatalException("Requested the Z-Push URL without the required GET parameters"); + + // This won't be useful with Zarafa, but it will be with standalone Z-Push + if (defined('PRE_AUTHORIZE_USERS') && PRE_AUTHORIZE_USERS === true) { + if (!Request::IsMethodGET()) { + // Check if User/Device are authorized + if (ZPush::GetDeviceManager()->GetUserDevicePermission($GETUser, Request::GetDeviceID()) != SYNC_COMMONSTATUS_SUCCESS) { + throw new AuthenticationRequiredException("Access denied. Username and Device not authorized"); + } + } + } + // Load the backend $backend = ZPush::GetBackend(); // always request the authorization header - if (! Request::AuthenticationInfo() || !Request::GetGETUser()) + if (!$autenticationInfo || !$GETUser) throw new AuthenticationRequiredException("Access denied. Please send authorisation information"); // check the provisioning information diff --git a/sources/lib/core/asdevice.php b/sources/lib/core/asdevice.php index 37a0ab3..c7e7377 100644 --- a/sources/lib/core/asdevice.php +++ b/sources/lib/core/asdevice.php @@ -67,6 +67,7 @@ class ASDevice extends StateObject { 'asversion' => false, 'ignoredmessages' => array(), 'announcedASversion' => false, + 'foldersynccomplete' => true, ); static private $loadedData; diff --git a/sources/lib/core/changesmemorywrapper.php b/sources/lib/core/changesmemorywrapper.php index 057f029..97b711f 100644 --- a/sources/lib/core/changesmemorywrapper.php +++ b/sources/lib/core/changesmemorywrapper.php @@ -115,6 +115,7 @@ class ChangesMemoryWrapper extends HierarchyCache implements IImportChanges, IEx public function LoadConflicts($contentparameters, $state) { return true; } public function ConfigContentParameters($contentparameters) { return true; } public function ImportMessageReadFlag($id, $flags) { return true; } + public function ImportMessageStarFlag($id, $flags) { return true; } public function ImportMessageMove($id, $newfolder) { return true; } /**---------------------------------------------------------------------------------------------------------- diff --git a/sources/lib/core/devicemanager.php b/sources/lib/core/devicemanager.php index 9654963..d0fdfe9 100644 --- a/sources/lib/core/devicemanager.php +++ b/sources/lib/core/devicemanager.php @@ -595,6 +595,11 @@ class DeviceManager { * @return boolean */ public function IsHierarchyFullResyncRequired() { + // do not check for loop detection, if the foldersync is not yet complete + if ($this->GetFolderSyncComplete() === false) { + ZLog::Write(LOGLEVEL_INFO, "DeviceManager->IsHierarchyFullResyncRequired(): aborted, as exporting of folders has not yet completed"); + return false; + } // check for potential process loops like described in ZP-5 return $this->loopdetection->ProcessLoopDetectionIsHierarchyResyncRequired(); } @@ -695,6 +700,45 @@ class DeviceManager { return true; } + /** + * Returns the indicator if the FolderSync was completed successfully (all folders synchronized) + * + * @access public + * @return boolean + */ + public function GetFolderSyncComplete() { + return $this->device->GetFolderSyncComplete(); + } + + /** + * Sets if the FolderSync was completed successfully (all folders synchronized) + * + * @param boolean $complete indicating if all folders were sent + * + * @access public + * @return boolean + */ + public function SetFolderSyncComplete($complete, $user = false, $devid = false) { + $this->device->SetFolderSyncComplete($complete); + } + + /** + * Removes the Loop detection data for a user & device + * + * @param string $user + * @param string $devid + * + * @access public + * @return boolean + */ + public function ClearLoopDetectionData($user, $devid) { + if ($user == false || $devid == false) { + return false; + } + ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->ClearLoopDetectionData(): clearing data for user '%s' and device '%s'", $user, $devid)); + return $this->loopdetection->ClearData($user, $devid); + } + /** * Indicates if the device needs an AS version update * @@ -709,6 +753,24 @@ class DeviceManager { return ($announced != $latest); } + + /**---------------------------------------------------------------------------------------------------------- + * DeviceManager User-Device pre-authorization + */ + + /** + * Return if the User-Device has permission to sync against this Z-Push. + * + * @param string $user Username + * @param string $devid DeviceId + * + * @access public + * @return integer + */ + public function GetUserDevicePermission($user, $devid) { + return $this->statemachine->GetUserDevicePermission($user, $devid); + } + /**---------------------------------------------------------------------------------------------------------- * private DeviceManager methods */ diff --git a/sources/lib/core/interprocessdata.php b/sources/lib/core/interprocessdata.php index 5a5cefc..66ba699 100644 --- a/sources/lib/core/interprocessdata.php +++ b/sources/lib/core/interprocessdata.php @@ -294,4 +294,4 @@ abstract class InterProcessData { } -?> \ No newline at end of file +?> diff --git a/sources/lib/core/streamimporter.php b/sources/lib/core/streamimporter.php index 83ba64c..4d9aa86 100644 --- a/sources/lib/core/streamimporter.php +++ b/sources/lib/core/streamimporter.php @@ -189,6 +189,41 @@ class ImportChangesStream implements IImportChanges { return true; } + /** + * Imports a change in 'star' flag + * Can only be applied to SyncMail (Email) requests + * + * @param string $id + * @param int $flags - flagged/unflagged + * + * @access public + * @return boolean + */ + public function ImportMessageStarFlag($id, $flags) { + if(!($this->objclass instanceof SyncMail)) + return false; + + $this->importedMsgs++; + + $this->encoder->startTag(SYNC_MODIFY); + $this->encoder->startTag(SYNC_SERVERENTRYID); + $this->encoder->content($id); + $this->encoder->endTag(); + $this->encoder->startTag(SYNC_DATA); + $this->encoder->startTag(SYNC_POOMMAIL_FLAG); + $this->encoder->startTag(SYNC_POOMMAIL_FLAGSTATUS); + $this->encoder->content($flags == 1? "2" : "0"); + $this->encoder->endTag(); + $this->encoder->startTag(SYNC_POOMMAIL_FLAGTYPE); + $this->encoder->content("FollowUp"); + $this->encoder->endTag(); + $this->encoder->endTag(); + $this->encoder->endTag(); + $this->encoder->endTag(); + + return true; + } + /** * ImportMessageMove is not implemented, as this operation can not be streamed to a WBXMLEncoder * diff --git a/sources/lib/core/synccollections.php b/sources/lib/core/synccollections.php index 54f7a99..17a3b31 100644 --- a/sources/lib/core/synccollections.php +++ b/sources/lib/core/synccollections.php @@ -449,6 +449,13 @@ class SyncCollections implements Iterator { // wait for changes $started = time(); $endat = time() + $lifetime; + + // always use policy key from the request if it was sent + $policyKey = $this->GetReferencePolicyKey(); + if (Request::WasPolicyKeySent() && Request::GetPolicyKey() != 0) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("refpolkey:'%s', sent polkey:'%s'", $policyKey, Request::GetPolicyKey())); + $policyKey = Request::GetPolicyKey(); + } while(($now = time()) < $endat) { // how long are we waiting for changes $this->waitingTime = $now-$started; @@ -460,7 +467,7 @@ class SyncCollections implements Iterator { // Check if provisioning is necessary // if a PolicyKey was sent use it. If not, compare with the ReferencePolicyKey - if (PROVISIONING === true && $this->GetReferencePolicyKey() !== false && ZPush::GetDeviceManager()->ProvisioningRequired($this->GetReferencePolicyKey(), true)) + if (PROVISIONING === true && $policyKey !== false && ZPush::GetDeviceManager()->ProvisioningRequired($policyKey, true)) // the hierarchysync forces provisioning throw new StatusException("SyncCollections->CheckForChanges(): PolicyKey changed. Provisioning required.", self::ERROR_WRONG_HIERARCHY); diff --git a/sources/lib/core/zpush.php b/sources/lib/core/zpush.php index 7efac2c..b4316aa 100644 --- a/sources/lib/core/zpush.php +++ b/sources/lib/core/zpush.php @@ -147,7 +147,7 @@ class ZPush { self::COMMAND_SETTINGS => array(self::ASV_12, self::REQUESTHANDLER => "Settings"), self::COMMAND_WEBSERVICE_DEVICE => array(self::REQUESTHANDLER => "Webservice", self::PLAININPUT, self::NOACTIVESYNCCOMMAND, self::WEBSERVICECOMMAND), - self::COMMAND_WEBSERVICE_USERS => array(self::REQUESTHANDLER => "Webservice", self::PLAININPUT, self::NOACTIVESYNCCOMMAND, self::WEBSERVICECOMMAND), + self::COMMAND_WEBSERVICE_USERS => array(self::REQUESTHANDLER => "Webservice", self::PLAININPUT, self::NOACTIVESYNCCOMMAND, self::WEBSERVICECOMMAND), ); @@ -276,12 +276,16 @@ class ZPush { else if (SINK_FORCERECHECK !== false && (!is_int(SINK_FORCERECHECK) || SINK_FORCERECHECK < 1)) throw new FatalMisconfigurationException("The SINK_FORCERECHECK value must be 'false' or a number higher than 0."); - if (!defined('SYNC_CONTACTS_MAXPICTURESIZE')) { - define('SYNC_CONTACTS_MAXPICTURESIZE', 49152); - } - else if ((!is_int(SYNC_CONTACTS_MAXPICTURESIZE) || SYNC_CONTACTS_MAXPICTURESIZE < 1)) + if (!defined('SYNC_CONTACTS_MAXPICTURESIZE')) { + define('SYNC_CONTACTS_MAXPICTURESIZE', 49152); + } + else if ((!is_int(SYNC_CONTACTS_MAXPICTURESIZE) || SYNC_CONTACTS_MAXPICTURESIZE < 1)) throw new FatalMisconfigurationException("The SYNC_CONTACTS_MAXPICTURESIZE value must be a number higher than 0."); + if (!defined('USE_PARTIAL_FOLDERSYNC')) { + define('USE_PARTIAL_FOLDERSYNC', false); + } + // the check on additional folders will not throw hard errors, as this is probably changed on live systems if (isset($additionalFolders) && !is_array($additionalFolders)) ZLog::Write(LOGLEVEL_ERROR, "ZPush::CheckConfig() : The additional folders synchronization not available as array."); @@ -349,8 +353,14 @@ class ZPush { } else { // Initialize the default StateMachine - include_once('lib/default/filestatemachine.php'); - ZPush::$stateMachine = new FileStateMachine(); + if (defined('STATE_MACHINE') && STATE_MACHINE == 'SQL') { + include_once('lib/default/sqlstatemachine.php'); + ZPush::$stateMachine = new SqlStateMachine(); + } + else { + include_once('lib/default/filestatemachine.php'); + ZPush::$stateMachine = new FileStateMachine(); + } } if (ZPush::$stateMachine->GetStateVersion() !== ZPush::GetLatestStateVersion()) { @@ -617,9 +627,9 @@ class ZPush { $message $additionalMessage

More information about Z-Push can be found at:
- Z-Push homepage
- Z-Push download page at BerliOS
- Z-Push Bugtracker and Roadmap
+ Z-Push homepage
+ Z-Push download page
+ Z-Push Bugtracker and Roadmap

All modifications to this sourcecode must be published and returned to the community.
Please see AGPLv3 License for details.
@@ -676,7 +686,9 @@ END; * @return string */ static public function GetSupportedProtocolVersions($valueOnly = false) { - $versions = implode(',', array_slice(self::$supportedASVersions, 0, (array_search(self::GetSupportedASVersion(), self::$supportedASVersions)+1))); + //$versions = implode(',', array_slice(self::$supportedASVersions, 0, (array_search(self::GetSupportedASVersion(), self::$supportedASVersions)+1))); + // Removing support for AS 1.0, 2.0, 2.1 - That will make Outlook 2013 works + $versions = implode(',', array_slice(self::$supportedASVersions, 3, (array_search(self::GetSupportedASVersion(), self::$supportedASVersions)+1))); ZLog::Write(LOGLEVEL_DEBUG, "ZPush::GetSupportedProtocolVersions(): " . $versions); if ($valueOnly === true) diff --git a/sources/lib/core/zpushdefs.php b/sources/lib/core/zpushdefs.php index ac42066..04ec8f5 100644 --- a/sources/lib/core/zpushdefs.php +++ b/sources/lib/core/zpushdefs.php @@ -1025,6 +1025,7 @@ define("SYNC_FLAGSTATUS_COMPLETE", 1); define("SYNC_FLAGSTATUS_ACTIVE", 2); define("DEFAULT_EMAIL_CONTENTCLASS", "urn:content-classes:message"); +define("DEFAULT_CALENDAR_CONTENTCLASS", "urn:content-classes:calendarmessage"); define("SYNC_MAIL_LASTVERB_UNKNOWN", 0); define("SYNC_MAIL_LASTVERB_REPLYSENDER", 1); diff --git a/sources/lib/default/backend.php b/sources/lib/default/backend.php index d7c7c09..99c5b39 100644 --- a/sources/lib/default/backend.php +++ b/sources/lib/default/backend.php @@ -203,6 +203,17 @@ abstract class Backend implements IBackend { return $r; } + /** + * Returns the email address and the display name of the user. Used by autodiscover. + * + * @param string $username The username + * + * @access public + * @return Array + */ + public function GetUserDetails($username) { + return array('emailaddress' => $username, 'fullname' => $username); + } /**---------------------------------------------------------------------------------------------------------- * Protected methods for BackendStorage diff --git a/sources/lib/default/diffbackend/diffbackend.php b/sources/lib/default/diffbackend/diffbackend.php index 0abb9b5..c41f46b 100644 --- a/sources/lib/default/diffbackend/diffbackend.php +++ b/sources/lib/default/diffbackend/diffbackend.php @@ -340,6 +340,25 @@ abstract class BackendDiff extends Backend { */ public abstract function SetReadFlag($folderid, $id, $flags, $contentParameters); + /** + * Changes the 'star' flag of a message on disk. The $flags + * parameter can only be '2' (starred) or '0' (unstarred). After a call to + * SetStarFlag(), GetMessageList() should return the message with the + * new 'flags' but should not modify the 'mod' parameter. If you do + * change 'mod', simply setting the message to 'starred' on the mobile will trigger + * a full resync of the item from the server. + * + * @param string $folderid id of the folder + * @param string $id id of the message + * @param int $flags star flag of the message + * @param ContentParameters $contentParameters + * + * @access public + * @return boolean status of the operation + * @throws StatusException could throw specific SYNC_STATUS_* exceptions + */ + public abstract function SetStarFlag($folderid, $id, $flags, $contentParameters); + /** * Called when the user has requested to delete (really delete) a message. Usually * this means just unlinking the file its in or somesuch. After this call has succeeded, a call to diff --git a/sources/lib/default/diffbackend/diffstate.php b/sources/lib/default/diffbackend/diffstate.php index 8f317fa..fabe98c 100644 --- a/sources/lib/default/diffbackend/diffstate.php +++ b/sources/lib/default/diffbackend/diffstate.php @@ -88,7 +88,19 @@ class DiffState implements IChanges { */ public function ConfigContentParameters($contentparameters) { $this->contentparameters = $contentparameters; - $this->cutoffdate = Utils::GetCutOffDate($contentparameters->GetFilterType()); + + $filtertype = $contentparameters->GetFilterType(); + switch($contentparameters->GetContentClass()) { + case "Email": + case "Calendar": + $this->cutoffdate = ($filtertype === false) ? 0 : Utils::GetCutOffDate($filtertype); + break; + case "Contacts": + case "Tasks": + default: + $this->cutoffdate = 0; + break; + } } /** @@ -120,8 +132,12 @@ class DiffState implements IChanges { * @return boolean */ static public function RowCmp($a, $b) { - // TODO implement different comparing functions - return $a["id"] < $b["id"] ? 1 : -1; + if (is_numeric($a["id"]) && is_numeric($b["id"])) { + return $a["id"] < $b["id"] ? 1 : -1; + } + else { + return strcmp($a["id"], $b["id"]) < 0 ? 1 : -1; + } } /** @@ -152,7 +168,7 @@ class DiffState implements IChanges { break; if($this->syncstate[$iold]["id"] == $new[$inew]["id"]) { - // Both messages are still available, compare flags and mod + // Both messages are still available, compare flags, star and mod if(isset($this->syncstate[$iold]["flags"]) && isset($new[$inew]["flags"]) && $this->syncstate[$iold]["flags"] != $new[$inew]["flags"]) { // Flags changed $change["type"] = "flags"; @@ -161,6 +177,14 @@ class DiffState implements IChanges { $changes[] = $change; } + if(isset($this->syncstate[$iold]["star"]) && isset($new[$inew]["star"]) && $this->syncstate[$iold]["star"] != $new[$inew]["star"]) { + // Star changed + $change["type"] = "star"; + $change["id"] = $new[$inew]["id"]; + $change["star"] = $new[$inew]["star"]; + $changes[] = $change; + } + if($this->syncstate[$iold]["mod"] != $new[$inew]["mod"]) { $change["type"] = "change"; $change["id"] = $new[$inew]["id"]; @@ -180,6 +204,7 @@ class DiffState implements IChanges { // Message in new seems to be new (add) $change["type"] = "change"; $change["flags"] = SYNC_NEWMESSAGE; + $change["star"] = SYNC_NEWMESSAGE; $change["id"] = $new[$inew]["id"]; $changes[] = $change; $inew++; @@ -199,6 +224,7 @@ class DiffState implements IChanges { // All data left in new have been added $change["type"] = "change"; $change["flags"] = SYNC_NEWMESSAGE; + $change["star"] = SYNC_NEWMESSAGE; $change["id"] = $new[$inew]["id"]; $changes[] = $change; $inew++; @@ -235,6 +261,9 @@ class DiffState implements IChanges { if($type == "flags") { // Update flags $this->syncstate[$i]["flags"] = $change["flags"]; + } else if($type == "star") { + // Update star + $this->syncstate[$i]["star"] = $change["star"]; } else if($type == "delete") { // Delete item array_splice($this->syncstate, $i, 1); diff --git a/sources/lib/default/diffbackend/exportchangesdiff.php b/sources/lib/default/diffbackend/exportchangesdiff.php index 982d7bd..c9ad12d 100644 --- a/sources/lib/default/diffbackend/exportchangesdiff.php +++ b/sources/lib/default/diffbackend/exportchangesdiff.php @@ -195,6 +195,10 @@ class ExportChangesDiff extends DiffState implements IExportChanges{ if($this->flags & BACKEND_DISCARD_DATA || $this->importer->ImportMessageReadFlag($change["id"], $change["flags"]) == true) $this->updateState("flags", $change); break; + case "star": + if($this->flags & BACKEND_DISCARD_DATA || $this->importer->ImportMessageStarFlag($change["id"], $change["star"]) == true) + $this->updateState("star", $change); + break; case "move": if($this->flags & BACKEND_DISCARD_DATA || $this->importer->ImportMessageMove($change["id"], $change["parent"]) == true) $this->updateState("move", $change); diff --git a/sources/lib/default/diffbackend/importchangesdiff.php b/sources/lib/default/diffbackend/importchangesdiff.php index 90a8eb3..3145593 100644 --- a/sources/lib/default/diffbackend/importchangesdiff.php +++ b/sources/lib/default/diffbackend/importchangesdiff.php @@ -100,6 +100,7 @@ class ImportChangesDiff extends DiffState implements IImportChanges { $change["mod"] = 0; // dummy, will be updated later if the change succeeds $change["parent"] = $this->folderid; $change["flags"] = (isset($message->read)) ? $message->read : 0; + $change["star"] = (isset($message->flag) && isset($message->flag->flagstatus)) ? $message->flag->flagstatus : 0; $this->updateState("change", $change); if($conflict && $this->flags == SYNC_CONFLICT_OVERWRITE_PIM) @@ -184,6 +185,35 @@ class ImportChangesDiff extends DiffState implements IImportChanges { return true; } + /** + * Imports a change in 'star' flag + * This can never conflict + * + * @param string $id + * @param int $flags - flagged/unflagged + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ImportMessageStarFlag($id, $flags) { + //do nothing if it is a dummy folder + if ($this->folderid == SYNC_FOLDER_TYPE_DUMMY) + throw new StatusException(sprintf("ImportChangesDiff->ImportMessageStarFlag('%s','%s'): can not be done on a dummy folder", $id, $flags), SYNC_STATUS_SYNCCANNOTBECOMPLETED); + + // Update client state + $change = array(); + $change["id"] = $id; + $change["star"] = $flags; + $this->updateState("star", $change); + + $stat = $this->backend->SetStarFlag($this->folderid, $id, $flags, $this->contentparameters); + if (!$stat) + throw new StatusException(sprintf("ImportChangesDiff->ImportMessageStarFlag('%s','%s'): Error, unable retrieve message from backend", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND); + + return true; + } + /** * Imports a move of a message. This occurs when a user moves an item to another folder * diff --git a/sources/lib/default/filestatemachine.php b/sources/lib/default/filestatemachine.php index 363685c..f8dbec0 100644 --- a/sources/lib/default/filestatemachine.php +++ b/sources/lib/default/filestatemachine.php @@ -423,6 +423,117 @@ class FileStateMachine implements IStateMachine { } + /** + * Return if the User-Device has permission to sync against this Z-Push. + * + * @param string $user Username + * @param string $devid DeviceId + * + * @access public + * @return integer + */ + public function GetUserDevicePermission($user, $devid) { + include_once("simplemutex.php"); + $mutex = new SimpleMutex(); + + $status = SYNC_COMMONSTATUS_SUCCESS; + + $userFile = STATE_DIR . 'PreAuthUserDevices'; + + if ($mutex->Block()) { + if (@file_exists($userFile)) { + $userList = json_decode(@file_get_contents($userFile), true); + } + else { + $userList = Array(); + } + + // Android PROVISIONING initial step + if ($devid != "validate") { + $changed = false; + + if (array_key_exists($user, $userList)) { + // User already pre-authorized + + // User could be blocked if a "authorized" device exist and it's false + if (!$userList[$user]["authorized"]) { + $status = SYNC_COMMONSTATUS_USERDISABLEDFORSYNC; + ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Blocked user '%s', tried '%s'", $user, $devid)); + } + else { + if (array_key_exists($devid, $userList[$user])) { + // Device pre-authorized found + + if ($userList[$user][$devid] === false) { + $status = SYNC_COMMONSTATUS_DEVICEBLOCKEDFORUSER; + ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Blocked device '%s' for user '%s'", $devid, $user)); + } + else { + ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Pre-authorized device '%s' for user '%s'", $devid, $user)); + } + } + else { + // Device not pre-authorized + + if (defined('PRE_AUTHORIZE_NEW_DEVICES') && PRE_AUTHORIZE_NEW_DEVICES === true) { + if (defined('PRE_AUTHORIZE_MAX_DEVICES') && PRE_AUTHORIZE_MAX_DEVICES >= count($userList[$user])) { + $userList[$user][$devid] = true; + $changed = true; + ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Pre-authorized new device '%s' for user '%s'", $devid, $user)); + } + else { + $status = SYNC_COMMONSTATUS_MAXDEVICESREACHED; + ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Max number of devices reached for user '%s', tried '%s'", $user, $devid)); + } + } + else { + $status = SYNC_COMMONSTATUS_DEVICEBLOCKEDFORUSER; + $userList[$user][$devid] = false; + $changed = true; + ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Blocked new device '%s' for user '%s'", $devid, $user)); + } + } + } + } + else { + // User not pre-authorized + + if (defined('PRE_AUTHORIZE_NEW_USERS') && PRE_AUTHORIZE_NEW_USERS === true) { + $userList[$user] = array("authorized" => true); + if (defined('PRE_AUTHORIZE_NEW_DEVICES') && PRE_AUTHORIZE_NEW_DEVICES === true) { + if (defined('PRE_AUTHORIZE_MAX_DEVICES') && PRE_AUTHORIZE_MAX_DEVICES >= count($userList[$user])) { + $userList[$user][$devid] = true; + ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Pre-authorized new device '%s' for new user '%s'", $devid, $user)); + } + } + else { + $status = SYNC_COMMONSTATUS_DEVICEBLOCKEDFORUSER; + $userList[$user][$devid] = false; + ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Blocked new device '%s' for new user '%s'", $devid, $user)); + } + + $changed = true; + } + else { + $status = SYNC_COMMONSTATUS_USERDISABLEDFORSYNC; + $userList[$user] = array("authorized" => false, $devid => false); + $changed = true; + ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Blocked new user '%s' and device '%s'", $user, $devid)); + } + } + + if ($changed) { + file_put_contents($userFile, json_encode($userList)); + } + } + + $mutex->Release(); + } + + return $status; + } + + /**---------------------------------------------------------------------------------------------------------- * Private FileStateMachine stuff */ diff --git a/sources/lib/default/sqlstatemachine.php b/sources/lib/default/sqlstatemachine.php new file mode 100644 index 0000000..c98c22d --- /dev/null +++ b/sources/lib/default/sqlstatemachine.php @@ -0,0 +1,781 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SqlStateMachine implements IStateMachine { + const SUPPORTED_STATE_VERSION = IStateMachine::STATEVERSION_02; + const VERSION = "version"; + + + + private $dbh; + private $options; + + /** + * Constructor + * + * Performs some basic checks and initilizes the state directory + * + * @access public + * @throws FatalMisconfigurationException + */ + public function SqlStateMachine() { + ZLog::Write(LOGLEVEL_DEBUG, "SqlStateMachine(): init"); + + if (!defined('STATE_SQL_DSN') || !defined('STATE_SQL_USER') || !defined('STATE_SQL_PASSWORD')) { + throw new FatalMisconfigurationException("No configuration for the state sql database available."); + } + + $this->options = array(); + if (defined('STATE_SQL_OPTIONS')) { + $this->options = unserialize(STATE_SQL_OPTIONS); + } + + try { + $this->dbh = new PDO(STATE_SQL_DSN, STATE_SQL_USER, STATE_SQL_PASSWORD, $this->options); + } + catch(PDOException $ex) { + throw new FatalMisconfigurationException(sprintf("Not possible to connect to the state database: %s", $ex->getMessage())); + } + + $this->clearConnection($this->dbh); + } + + /** + * Gets a hash value indicating the latest dataset of the named + * state with a specified key and counter. + * If the state is changed between two calls of this method + * the returned hash should be different + * + * @param string $devid the device id + * @param string $type the state type + * @param string $key (opt) + * @param string $counter (opt) + * + * @access public + * @return string + * @throws StateNotFoundException, StateInvalidException + */ + public function GetStateHash($devid, $type, $key = false, $counter = false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SqlStateMachine->GetStateHash(): '%s', '%s', '%s', '%s'", $devid, $type, $key, $counter)); + + $sql = "select updated_at from zpush_states where device_id = :devid and state_type = :type and uuid = :key and counter = :counter"; + $params = $this->getParams($devid, $type, $key, $counter); + + $hash = null; + $sth = null; + $record = null; + try { + $this->dbh = new PDO(STATE_SQL_DSN, STATE_SQL_USER, STATE_SQL_PASSWORD, $this->options); + + $sth = $this->dbh->prepare($sql); + $sth->execute($params); + + $record = $sth->fetch(PDO::FETCH_ASSOC); + if (!$record) { + $this->clearConnection($this->dbh, $sth, $record); + throw new StateNotFoundException(sprintf("SqlStateMachine->GetStateHash(): Could not locate state")); + } + else { + // datetime->format("U") returns EPOCH + $datetime = new DateTime($record["updated_at"]); + $hash = $datetime->format("U"); + } + } + catch(PDOException $ex) { + $this->clearConnection($this->dbh, $sth, $record); + throw new StateNotFoundException(sprintf("SqlStateMachine->GetStateHash(): Could not locate state: %s", $ex->getMessage())); + } + + $this->clearConnection($this->dbh, $sth, $record); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SqlStateMachine->GetStateHash(): return '%s'", $hash)); + + return $hash; + } + + /** + * Gets a state for a specified key and counter. + * This method sould call IStateMachine->CleanStates() + * to remove older states (same key, previous counters) + * + * @param string $devid the device id + * @param string $type the state type + * @param string $key (opt) + * @param string $counter (opt) + * @param string $cleanstates (opt) + * + * @access public + * @return mixed + * @throws StateNotFoundException, StateInvalidException + */ + public function GetState($devid, $type, $key = false, $counter = false, $cleanstates = true) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SqlStateMachine->GetState(): '%s', '%s', '%s', '%s', '%s'", $devid, $type, $key, $counter, $cleanstates)); + if ($counter && $cleanstates) + $this->CleanStates($devid, $type, $key, $counter); + + $sql = "select state_data from zpush_states where device_id = :devid and state_type = :type and uuid = :key and counter = :counter"; + $params = $this->getParams($devid, $type, $key, $counter); + + $data = null; + $sth = null; + $record = null; + try { + $this->dbh = new PDO(STATE_SQL_DSN, STATE_SQL_USER, STATE_SQL_PASSWORD, $this->options); + + $sth = $this->dbh->prepare($sql); + $sth->execute($params); + + $record = $sth->fetch(PDO::FETCH_ASSOC); + if (!$record) { + $this->clearConnection($this->dbh, $sth, $record); + // throw an exception on all other states, but not FAILSAVE as it's most of the times not there by default + if ($type !== IStateMachine::FAILSAVE) { + throw new StateNotFoundException(sprintf("SqlStateMachine->GetState(): Could not locate state")); + } + } + else { + $data = unserialize($record["state_data"]); + } + } + catch(PDOException $ex) { + $this->clearConnection($this->dbh, $sth, $record); + throw new StateNotFoundException(sprintf("SqlStateMachine->GetState(): Could not locate state: %s", $ex->getMessage())); + } + + $this->clearConnection($this->dbh, $sth, $record); + + return $data; + } + + /** + * Writes ta state to for a key and counter + * + * @param mixed $state + * @param string $devid the device id + * @param string $type the state type + * @param string $key (opt) + * @param int $counter (opt) + * + * @access public + * @return boolean + * @throws StateInvalidException + */ + public function SetState($state, $devid, $type, $key = false, $counter = false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SqlStateMachine->SetState(): '%s', '%s', '%s', '%s'", $devid, $type, $key, $counter)); + + $sql = "select device_id from zpush_states where device_id = :devid and state_type = :type and uuid = :key and counter = :counter"; + $params = $this->getParams($devid, $type, $key, $counter); + + $sth = null; + $record = null; + $bytes = 0; + + try { + $this->dbh = new PDO(STATE_SQL_DSN, STATE_SQL_USER, STATE_SQL_PASSWORD, $this->options); + + $sth = $this->dbh->prepare($sql); + $sth->execute($params); + + $params[":data"] = serialize($state); + $params[":updated_at"] = $this->getNow(); + + $record = $sth->fetch(PDO::FETCH_ASSOC); + if (!$record) { + // New record + $sql = "insert into zpush_states (device_id, state_type, uuid, counter, state_data, created_at, updated_at) values (:devid, :type, :key, :counter, :data, :created_at, :updated_at)"; + $params[":created_at"] = $params[":updated_at"]; + + $sth = $this->dbh->prepare($sql); + } + else { + // Existing record, we update it + $sql = "update zpush_states set state_data = :data, updated_at = :updated_at where device_id = :devid and state_type = :type and uuid = :key and counter = :counter"; + + $sth = $this->dbh->prepare($sql); + } + + if (!$sth->execute($params) ) { + $this->clearConnection($this->dbh, $sth); + throw new FatalMisconfigurationException(sprintf("SqlStateMachine->SetState(): Could not write state")); + } + else { + $bytes = strlen($params[":data"]); + } + } + catch(PDOException $ex) { + $this->clearConnection($this->dbh, $sth); + throw new FatalMisconfigurationException(sprintf("SqlStateMachine->SetState(): Could not write state: %s", $ex->getMessage())); + } + + $this->clearConnection($this->dbh, $sth, $record); + + return $bytes; + } + + /** + * Cleans up all older states + * If called with a $counter, all states previous state counter can be removed + * If called without $counter, all keys (independently from the counter) can be removed + * + * @param string $devid the device id + * @param string $type the state type + * @param string $key + * @param string $counter (opt) + * + * @access public + * @return + * @throws StateInvalidException + */ + public function CleanStates($devid, $type, $key, $counter = false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SqlStateMachine->CleanStates(): '%s', '%s', '%s', '%s'", $devid, $type, $key, $counter)); + + + if ($counter === false) { + // Remove all the states. Counter are -1 or > 0, then deleting >= -1 deletes all + $sql = "delete from zpush_states where device_id = :devid and state_type = :type and uuid = :key and counter >= :counter"; + } + else { + $sql = "delete from zpush_states where device_id = :devid and state_type = :type and uuid = :key and counter < :counter"; + } + $params = $this->getParams($devid, $type, $key, $counter); + + $sth = null; + try { + $this->dbh = new PDO(STATE_SQL_DSN, STATE_SQL_USER, STATE_SQL_PASSWORD, $this->options); + + $sth = $this->dbh->prepare($sql); + $sth->execute($params); + } + catch(PDOException $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("SqlStateMachine->CleanStates(): Error deleting states: %s", $ex->getMessage())); + } + + $this->clearConnection($this->dbh, $sth, $record); + } + + /** + * Links a user to a device + * + * @param string $username + * @param string $devid + * + * @access public + * @return boolean indicating if the user was added or not (existed already) + */ + public function LinkUserDevice($username, $devid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SqlStateMachine->LinkUserDevice(): '%s', '%s'", $username, $devid)); + + $sth = null; + $record = null; + $changed = false; + try { + $this->dbh = new PDO(STATE_SQL_DSN, STATE_SQL_USER, STATE_SQL_PASSWORD, $this->options); + + $sql = "select username from zpush_users where username = :username and device_id = :devid"; + $params = array(":username" => $username, ":devid" => $devid); + + $sth = $this->dbh->prepare($sql); + $sth->execute($params); + + $record = $sth->fetch(PDO::FETCH_ASSOC); + if ($record) { + ZLog::Write(LOGLEVEL_DEBUG, "SqlStateMachine->LinkUserDevice(): nothing changed"); + } + else { + $sth = null; + $sql = "insert into zpush_users (username, device_id, created_at, updated_at) values (:username, :devid, :created_at, :updated_at)"; + $params[":created_at"] = $params[":updated_at"] = $this->getNow(); + $sth = $this->dbh->prepare($sql); + if ($sth->execute($params)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SqlStateMachine->LinkUserDevice(): Linked user-device: '%s' '%s'", $username, $devid)); + $changed = true; + } + else { + ZLog::Write(LOGLEVEL_ERROR, sprintf("SqlStateMachine->LinkUserDevice(): Unable to link user-device")); + } + } + } + catch(PDOException $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("SqlStateMachine->LinkUserDevice(): Error linking user-device: %s", $ex->getMessage())); + } + + $this->clearConnection($this->dbh, $sth, $record); + + return $changed; + } + + /** + * Unlinks a device from a user + * + * @param string $username + * @param string $devid + * + * @access public + * @return boolean + */ + public function UnLinkUserDevice($username, $devid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SqlStateMachine->UnLinkUserDevice(): '%s', '%s'", $username, $devid)); + + $sth = null; + $changed = false; + try { + $this->dbh = new PDO(STATE_SQL_DSN, STATE_SQL_USER, STATE_SQL_PASSWORD, $this->options); + + $sql = "delete from zpush_users where username = :username and device_id = :devid"; + $params = array(":username" => $username, ":devid" => $devid); + + $sth = $this->dbh->prepare($sql); + if ($sth->execute($params)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SqlStateMachine->UnLinkUserDevice(): Unlinked user-device: '%s' '%s'", $username, $devid)); + $changed = true; + } + else { + ZLog::Write(LOGLEVEL_DEBUG, "SqlStateMachine->UnLinkUserDevice(): nothing changed"); + } + } + catch(PDOException $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("SqlStateMachine->UnLinkUserDevice(): Error unlinking user-device: %s", $ex->getMessage())); + } + + $this->clearConnection($this->dbh, $sth); + + return $changed; + } + + /** + * Returns an array with all device ids for a user. + * If no user is set, all device ids should be returned + * + * @param string $username (opt) + * + * @access public + * @return array + */ + public function GetAllDevices($username = false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SqlStateMachine->GetAllDevices(): '%s'", $username)); + + $sth = null; + $record = null; + $out = array(); + try { + $this->dbh = new PDO(STATE_SQL_DSN, STATE_SQL_USER, STATE_SQL_PASSWORD, $this->options); + + if ($username === false) { + $sql = "select distinct(device_id) from zpush_users order by device_id"; + $params = array(); + } + else { + $sql = "select device_id from zpush_users where username = :username order by device_id"; + $params = array(":username" => $username); + } + $sth = $this->dbh->prepare($sql); + $sth->execute($params); + + while ($record = $sth->fetch(PDO::FETCH_ASSOC)) { + $out[] = $record["device_id"]; + } + } + catch(PDOException $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("SqlStateMachine->GetAllDevices(): Error listing devices: %s", $ex->getMessage())); + } + + $this->clearConnection($this->dbh, $sth, $record); + + return $out; + } + + /** + * Returns the current version of the state files + * + * @access public + * @return int + */ + public function GetStateVersion() { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SqlStateMachine->GetStateVersion()")); + + $sth = null; + $record = null; + $version = IStateMachine::STATEVERSION_01; + try { + $this->dbh = new PDO(STATE_SQL_DSN, STATE_SQL_USER, STATE_SQL_PASSWORD, $this->options); + + $sql = "select key_value from zpush_settings where key_name = :key_name"; + $params = array(":key_name" => self::VERSION); + + $sth = $this->dbh->prepare($sql); + $sth->execute($params); + + $record = $sth->fetch(PDO::FETCH_ASSOC); + if ($record) { + $version = $record["key_value"]; + } + else { + $this->SetStateVersion(self::SUPPORTED_STATE_VERSION); + $version = self::SUPPORTED_STATE_VERSION; + } + } + catch(PDOException $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("SqlStateMachine->GetStateVersion(): Error getting state version: %s", $ex->getMessage())); + } + + $this->clearConnection($this->dbh, $sth, $record); + + return $version; + } + + /** + * Sets the current version of the state files + * + * @param int $version the new supported version + * + * @access public + * @return boolean + */ + public function SetStateVersion($version) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SqlStateMachine->SetStateVersion(): '%s'", $version)); + + $sth = null; + $record = null; + $status = false; + try { + $this->dbh = new PDO(STATE_SQL_DSN, STATE_SQL_USER, STATE_SQL_PASSWORD, $this->options); + + $sql = "select key_value from zpush_settings where key_name = :key_name"; + $params = array(":key_name" => self::VERSION); + + $sth = $this->dbh->prepare($sql); + $sth->execute($params); + + $record = $sth->fetch(PDO::FETCH_ASSOC); + if ($record) { + $sth = null; + $sql = "update zpush_settings set key_value = :value, updated_at = :updated_at where key_name = :key_name"; + $params[":value"] = $version; + $params[":updated_at"] = $this->getNow(); + + $sth = $this->dbh->prepare($sql); + if ($sth->execute($params)) { + $status = true; + } + } + else { + $sth = null; + $sql = "insert into zpush_settings (key_name, key_value, created_at, updated_at) values (:key_name, :value, :created_at, :updated_at)"; + $params[":value"] = $version; + $params[":updated_at"] = $params[":created_at"] = $this->getNow(); + + $sth = $this->dbh->prepare($sql); + if ($sth->execute($params)) { + $status = true; + } + } + } + catch(PDOException $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("SqlStateMachine->SetStateVersion(): Error saving state version: %s", $ex->getMessage())); + } + + $this->clearConnection($this->dbh, $sth, $record); + + return $status; + } + + /** + * Returns all available states for a device id + * + * @param string $devid the device id + * + * @access public + * @return array(mixed) + */ + public function GetAllStatesForDevice($devid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SqlStateMachine->GetAllStatesForDevice(): '%s'", $devid)); + + $sth = null; + $record = null; + $out = array(); + try { + $this->dbh = new PDO(STATE_SQL_DSN, STATE_SQL_USER, STATE_SQL_PASSWORD, $this->options); + + $sql = "select state_type, uuid, counter from zpush_states where device_id = :devid order by id_state"; + $params = array(":devid" => $devid); + + $sth = $this->dbh->prepare($sql); + $sth->execute($params); + + while ($record = $sth->fetch(PDO::FETCH_ASSOC)) { + $state = array('type' => false, 'counter' => false, 'uuid' => false); + if ($record["state_type"] !== null && strlen($record["state_type"]) > 0) { + $state["type"] = $record["state_type"]; + } + else { + if ($record["counter"] !== null && is_numeric($record["counter"])) { + $state["type"] = ""; + } + } + if ($record["counter"] !== null && strlen($record["counter"]) > 0) { + $state["counter"] = $record["counter"]; + } + if ($record["uuid"] !== null && strlen($record["uuid"]) > 0) { + $state["uuid"] = $record["uuid"]; + } + $out[] = $state; + } + } + catch(PDOException $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("SqlStateMachine->GetAllStatesForDevice(): Error listing states: %s", $ex->getMessage())); + } + + $this->clearConnection($this->dbh, $sth, $record); + + return $out; + } + + /** + * Return if the User-Device has permission to sync against this Z-Push. + * + * @param string $user Username + * @param string $devid DeviceId + * + * @access public + * @return integer + */ + public function GetUserDevicePermission($user, $devid) { + $status = SYNC_COMMONSTATUS_SUCCESS; + + $userExist = false; + $userBlocked = false; + $deviceExist = false; + $deviceBlocked = false; + + // Android PROVISIONING initial step + if ($devid != "validate") { + + $sth = null; + $record = null; + try { + $this->dbh = new PDO(STATE_SQL_DSN, STATE_SQL_USER, STATE_SQL_PASSWORD, $this->options); + + $sql = "select authorized from zpush_preauth_users where username = :user and device_id = :devid"; + $params = array(":user" => $user, ":devid" => "authorized"); + $paramsNewDevid = array(); + $paramsNewUser = array(); + + $sth = $this->dbh->prepare($sql); + $sth->execute($params); + if ($record = $sth->fetch(PDO::FETCH_ASSOC)) { + $userExist = true; + $userBlocked = !$record["authorized"]; + } + $record = null; + $sth = null; + + if ($userExist) { + // User already pre-authorized + + // User could be blocked if a "authorized" device exist and it's false + if ($userBlocked) { + $status = SYNC_COMMONSTATUS_USERDISABLEDFORSYNC; + ZLog::Write(LOGLEVEL_INFO, sprintf("SqlStateMachine->GetUserDevicePermission(): Blocked user '%s', tried '%s'", $user, $devid)); + } + else { + $params[":devid"] = $devid; + + $sth = $this->dbh->prepare($sql); + $sth->execute($params); + if ($record = $sth->fetch(PDO::FETCH_ASSOC)) { + $deviceExist = true; + $deviceBlocked = !$record["authorized"]; + } + $record = null; + $sth = null; + + if ($deviceExist) { + // Device pre-authorized found + + if ($deviceBlocked) { + $status = SYNC_COMMONSTATUS_DEVICEBLOCKEDFORUSER; + ZLog::Write(LOGLEVEL_INFO, sprintf("SqlStateMachine->GetUserDevicePermission(): Blocked device '%s' for user '%s'", $devid, $user)); + } + else { + ZLog::Write(LOGLEVEL_INFO, sprintf("SqlStateMachine->GetUserDevicePermission(): Pre-authorized device '%s' for user '%s'", $devid, $user)); + } + } + else { + // Device not pre-authorized + + if (defined('PRE_AUTHORIZE_NEW_DEVICES') && PRE_AUTHORIZE_NEW_DEVICES === true) { + if (defined('PRE_AUTHORIZE_MAX_DEVICES') && PRE_AUTHORIZE_MAX_DEVICES >= count($userList[$user])) { + $paramsNewDevid[":auth"] = true; + ZLog::Write(LOGLEVEL_INFO, sprintf("SqlStateMachine->GetUserDevicePermission(): Pre-authorized new device '%s' for user '%s'", $devid, $user)); + } + else { + $status = SYNC_COMMONSTATUS_MAXDEVICESREACHED; + ZLog::Write(LOGLEVEL_INFO, sprintf("SqlStateMachine->GetUserDevicePermission(): Max number of devices reached for user '%s', tried '%s'", $user, $devid)); + } + } + else { + $status = SYNC_COMMONSTATUS_DEVICEBLOCKEDFORUSER; + $paramsNewDevid[":auth"] = false; + ZLog::Write(LOGLEVEL_INFO, sprintf("SqlStateMachine->GetUserDevicePermission(): Blocked new device '%s' for user '%s'", $devid, $user)); + } + } + } + } + else { + // User not pre-authorized + + if (defined('PRE_AUTHORIZE_NEW_USERS') && PRE_AUTHORIZE_NEW_USERS === true) { + $paramsNewUser[":auth"] = true; + if (defined('PRE_AUTHORIZE_NEW_DEVICES') && PRE_AUTHORIZE_NEW_DEVICES === true) { + if (defined('PRE_AUTHORIZE_MAX_DEVICES') && PRE_AUTHORIZE_MAX_DEVICES >= count($userList[$user])) { + $paramsNewDevid[":auth"] = true; + ZLog::Write(LOGLEVEL_INFO, sprintf("SqlStateMachine->GetUserDevicePermission(): Pre-authorized new device '%s' for new user '%s'", $devid, $user)); + } + } + else { + $status = SYNC_COMMONSTATUS_DEVICEBLOCKEDFORUSER; + $paramsNewDevid[":auth"] = false; + ZLog::Write(LOGLEVEL_INFO, sprintf("SqlStateMachine->GetUserDevicePermission(): Blocked new device '%s' for new user '%s'", $devid, $user)); + } + } + else { + $status = SYNC_COMMONSTATUS_USERDISABLEDFORSYNC; + $paramsNewUser[":auth"] = false; + $paramsNewDevid[":auth"] = false; + ZLog::Write(LOGLEVEL_INFO, sprintf("SqlStateMachine->GetUserDevicePermission(): Blocked new user '%s' and device '%s'", $user, $devid)); + } + } + + if (count($paramsNewUser) > 0) { + $sql = "insert into zpush_preauth_users (username, device_id, authorized, created_at, updated_at) values (:user, :devid, :auth, :created_at, :updated_at)"; + $paramsNewUser[":user"] = $user; + $paramsNewUser[":devid"] = "authorized"; + $paramsNewUser[":created_at"] = $paramsNewUser[":updated_at"] = $this->getNow(); + + $sth = $this->dbh->prepare($sql); + if (!$sth->execute($paramsNewUser)) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("SqlStateMachine->GetUserDevicePermission(): Error creating new user")); + $status = SYNC_COMMONSTATUS_USERDISABLEDFORSYNC; + } + } + + if (count($paramsNewDevid) > 0) { + $sql = "insert into zpush_preauth_users (username, device_id, authorized, created_at, updated_at) values (:user, :devid, :auth, :created_at, :updated_at)"; + $paramsNewDevid[":user"] = $user; + $paramsNewDevid[":devid"] = $devid; + $paramsNewDevid[":created_at"] = $paramsNewDevid[":updated_at"] = $this->getNow(); + + $sth = $this->dbh->prepare($sql); + if (!$sth->execute($paramsNewDevid)) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("SqlStateMachine->GetUserDevicePermission(): Error creating user new device")); + $status = SYNC_COMMONSTATUS_USERDISABLEDFORSYNC; + } + } + } + catch(PDOException $ex) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("SqlStateMachine->GetUserDevicePermission(): Error checking permission for username '%s' device '%s': %s", $user, $devid, $ex->getMessage())); + $status = SYNC_COMMONSTATUS_USERDISABLEDFORSYNC; + } + + $this->clearConnection($this->dbh, $sth, $record); + } + + return $status; + } + + + /**---------------------------------------------------------------------------------------------------------- + * Private SqlStateMachine stuff + */ + + /** + * Return a string with the datetime NOW + * + * @return string + * @access private + */ + private function getNow() { + $now = new DateTime("NOW"); + return $now->format("Y-m-d H:i:s"); + } + + /** + * Return an array with the params for the PDO query + * + * @params string $devid + * @params string $type + * @params string $key + * @params string $counter + * @return array + * @access private + */ + private function getParams($devid, $type, $key, $counter) { + return array(":devid" => $devid, ":type" => $type, ":key" => $key, ":counter" => ($counter === false ? -1 : $counter) ); + } + + /** + * Free PDO resources. + * + * @params PDOConnection $dbh + * @params PDOStatement $sth + * @params PDORecord $record + * @access private + */ + private function clearConnection(&$dbh, &$sth = null, &$record = null) { + if ($record != null) { + $record = null; + } + if ($sth != null) { + $sth = null; + } + if ($dbh != null) { + $dbh = null; + } + } + +} +?> \ No newline at end of file diff --git a/sources/lib/interface/ibackend.php b/sources/lib/interface/ibackend.php index 2ce018b..d4b36f3 100644 --- a/sources/lib/interface/ibackend.php +++ b/sources/lib/interface/ibackend.php @@ -289,6 +289,16 @@ interface IBackend { * @return SyncObject $resolveRecipients */ public function ResolveRecipients($resolveRecipients); + + /** + * Returns the email address and the display name of the user. Used by autodiscover. + * + * @param string $username The username + * + * @access public + * @return Array + */ + public function GetUserDetails($username); } ?> \ No newline at end of file diff --git a/sources/lib/interface/iimportchanges.php b/sources/lib/interface/iimportchanges.php index 31da666..3ce5629 100644 --- a/sources/lib/interface/iimportchanges.php +++ b/sources/lib/interface/iimportchanges.php @@ -98,6 +98,19 @@ interface IImportChanges extends IChanges { */ public function ImportMessageReadFlag($id, $flags); + /** + * Imports a change in 'star' flag + * This can never conflict + * + * @param string $id + * @param int $flags + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ImportMessageStarFlag($id, $flags); + /** * Imports a move of a message. This occurs when a user moves an item to another folder * diff --git a/sources/lib/request/foldersync.php b/sources/lib/request/foldersync.php index 92ee92a..653e6dc 100644 --- a/sources/lib/request/foldersync.php +++ b/sources/lib/request/foldersync.php @@ -188,7 +188,36 @@ class FolderSync extends RequestProcessor { $exporter->InitializeExporter($changesMem); // Stream all changes to the ImportExportChangesMem - while(is_array($exporter->Synchronize())); + $maxExporttime = Request::GetExpectedConnectionTimeout(); + $totalChanges = $exporter->GetChangeCount(); + $started = time(); + $exported = 0; + $partial = false; + while(is_array($exporter->Synchronize())) { + $exported++; + + if (time() % 4 ) { + self::$topCollector->AnnounceInformation(sprintf("Exported %d from %d folders", $exported, $totalChanges)); + } + + // if partial sync is allowed, stop if this takes too long + if (USE_PARTIAL_FOLDERSYNC && (time() - $started) > $maxExporttime) { + ZLog::Write(LOGLEVEL_WARN, sprintf("Request->HandleFolderSync(): Exporting folders is too slow. In %d seconds only %d from %d changes were processed.",(time() - $started), $exported, $totalChanges)); + self::$topCollector->AnnounceInformation(sprintf("Partial export of %d out of %d folders", $exported, $totalChanges), true); + self::$deviceManager->SetFolderSyncComplete(false); + $partial = true; + break; + } + } + + // update the foldersync complete flag + if (USE_PARTIAL_FOLDERSYNC && $partial == false && self::$deviceManager->GetFolderSyncComplete() === false) { + // say that we are done with partial synching + self::$deviceManager->SetFolderSyncComplete(true); + // reset the loop data to prevent any loop detection to kick in now + self::$deviceManager->ClearLoopDetectionData(Request::GetAuthUser(), Request::GetDeviceId()); + ZLog::Write(LOGLEVEL_INFO, "Request->HandleFolderSync(): Chunked exporting of folders completed successfully"); + } // get the new state from the backend $newsyncstate = (isset($exporter))?$exporter->GetState():""; diff --git a/sources/lib/request/itemoperations.php b/sources/lib/request/itemoperations.php index d6dcd01..c68c838 100644 --- a/sources/lib/request/itemoperations.php +++ b/sources/lib/request/itemoperations.php @@ -90,144 +90,145 @@ class ItemOperations extends RequestProcessor { return false; } - if ($fetch) { - if(!self::$decoder->getElementStartTag(SYNC_ITEMOPERATIONS_STORE)) - return false; - $operation['store'] = self::$decoder->getElementContent(); - if(!self::$decoder->getElementEndTag()) - return false;//SYNC_ITEMOPERATIONS_STORE + // process operation + while(1) { + if ($fetch) { + if(self::$decoder->getElementStartTag(SYNC_ITEMOPERATIONS_STORE)) { + $operation['store'] = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false;//SYNC_ITEMOPERATIONS_STORE + } - if(self::$decoder->getElementStartTag(SYNC_SEARCH_LONGID)) { - $operation['longid'] = self::$decoder->getElementContent(); - if(!self::$decoder->getElementEndTag()) - return false;//SYNC_SEARCH_LONGID - } + if(self::$decoder->getElementStartTag(SYNC_SEARCH_LONGID)) { + $operation['longid'] = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false;//SYNC_SEARCH_LONGID + } - if(self::$decoder->getElementStartTag(SYNC_FOLDERID)) { - $operation['folderid'] = self::$decoder->getElementContent(); - if(!self::$decoder->getElementEndTag()) - return false;//SYNC_FOLDERID - } + if(self::$decoder->getElementStartTag(SYNC_FOLDERID)) { + $operation['folderid'] = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false;//SYNC_FOLDERID + } - if(self::$decoder->getElementStartTag(SYNC_SERVERENTRYID)) { - $operation['serverid'] = self::$decoder->getElementContent(); - if(!self::$decoder->getElementEndTag()) - return false;//SYNC_SERVERENTRYID - } + if(self::$decoder->getElementStartTag(SYNC_SERVERENTRYID)) { + $operation['serverid'] = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false;//SYNC_SERVERENTRYID + } - if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_FILEREFERENCE)) { - $operation['filereference'] = self::$decoder->getElementContent(); - if(!self::$decoder->getElementEndTag()) - return false;//SYNC_AIRSYNCBASE_FILEREFERENCE - } + if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_FILEREFERENCE)) { + $operation['filereference'] = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false;//SYNC_AIRSYNCBASE_FILEREFERENCE + } - if(self::$decoder->getElementStartTag(SYNC_ITEMOPERATIONS_OPTIONS)) { - //TODO other options - //schema - //range - //username - //password - //bodypartpreference - //rm:RightsManagementSupport + if(($el = self::$decoder->getElementStartTag(SYNC_ITEMOPERATIONS_OPTIONS)) && ($el[EN_FLAGS] & EN_FLAGS_CONTENT)) { + //TODO other options + //schema + //range + //username + //password + //bodypartpreference + //rm:RightsManagementSupport - // Save all OPTIONS into a ContentParameters object - $operation["cpo"] = new ContentParameters(); - while(1) { - // Android 4.3 sends empty options tag, so we don't have to look further - $e = self::$decoder->peek(); - if($e[EN_TYPE] == EN_TYPE_ENDTAG) { - break; - } + // Save all OPTIONS into a ContentParameters object + $operation["cpo"] = new ContentParameters(); + while(1) { + while (self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_BODYPREFERENCE)) { + if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_TYPE)) { + $bptype = self::$decoder->getElementContent(); + $operation["cpo"]->BodyPreference($bptype); + if(!self::$decoder->getElementEndTag()) { + return false; + } + } - while (self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_BODYPREFERENCE)) { - if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_TYPE)) { - $bptype = self::$decoder->getElementContent(); - $operation["cpo"]->BodyPreference($bptype); - if(!self::$decoder->getElementEndTag()) { + if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_TRUNCATIONSIZE)) { + $operation["cpo"]->BodyPreference($bptype)->SetTruncationSize(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_ALLORNONE)) { + $operation["cpo"]->BodyPreference($bptype)->SetAllOrNone(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_PREVIEW)) { + $operation["cpo"]->BodyPreference($bptype)->SetPreview(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(!self::$decoder->getElementEndTag()) + return false;//SYNC_AIRSYNCBASE_BODYPREFERENCE + } + + if(self::$decoder->getElementStartTag(SYNC_MIMESUPPORT)) { + $operation["cpo"]->SetMimeSupport(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) return false; + } + + if(self::$decoder->getElementStartTag(SYNC_ITEMOPERATIONS_RANGE)) { + $operation["range"] = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_ITEMOPERATIONS_SCHEMA)) { + // read schema tags + while (1) { + // TODO save elements + $el = self::$decoder->getElement(); + $e = self::$decoder->peek(); + if($e[EN_TYPE] == EN_TYPE_ENDTAG) { + self::$decoder->getElementEndTag(); + break; + } } } - if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_TRUNCATIONSIZE)) { - $operation["cpo"]->BodyPreference($bptype)->SetTruncationSize(self::$decoder->getElementContent()); - if(!self::$decoder->getElementEndTag()) - return false; + //break if it reached the endtag + $e = self::$decoder->peek(); + if($e[EN_TYPE] == EN_TYPE_ENDTAG) { + self::$decoder->getElementEndTag(); + break; } - - if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_ALLORNONE)) { - $operation["cpo"]->BodyPreference($bptype)->SetAllOrNone(self::$decoder->getElementContent()); - if(!self::$decoder->getElementEndTag()) - return false; - } - - if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_PREVIEW)) { - $operation["cpo"]->BodyPreference($bptype)->SetPreview(self::$decoder->getElementContent()); - if(!self::$decoder->getElementEndTag()) - return false; - } - - if(!self::$decoder->getElementEndTag()) - return false;//SYNC_AIRSYNCBASE_BODYPREFERENCE - } - - if(self::$decoder->getElementStartTag(SYNC_MIMESUPPORT)) { - $operation["cpo"]->SetMimeSupport(self::$decoder->getElementContent()); - if(!self::$decoder->getElementEndTag()) - return false; - } - - if(self::$decoder->getElementStartTag(SYNC_ITEMOPERATIONS_RANGE)) { - $operation["range"] = self::$decoder->getElementContent(); - if(!self::$decoder->getElementEndTag()) - return false; - } - - if(self::$decoder->getElementStartTag(SYNC_ITEMOPERATIONS_SCHEMA)) { - // read schema tags - while (1) { - // TODO save elements - $el = self::$decoder->getElement(); - $e = self::$decoder->peek(); - if($e[EN_TYPE] == EN_TYPE_ENDTAG) { - self::$decoder->getElementEndTag(); - break; - } - } - } - - //break if it reached the endtag - $e = self::$decoder->peek(); - if($e[EN_TYPE] == EN_TYPE_ENDTAG) { - self::$decoder->getElementEndTag(); - break; } } - } - } + } // end if fetch - if ($efc) { - if(self::$decoder->getElementStartTag(SYNC_FOLDERID)) { - $operation['folderid'] = self::$decoder->getElementContent(); - if(!self::$decoder->getElementEndTag()) - return false;//SYNC_FOLDERID - } - if(self::$decoder->getElementStartTag(SYNC_ITEMOPERATIONS_OPTIONS)) { - if(self::$decoder->getElementStartTag(SYNC_ITEMOPERATIONS_DELETESUBFOLDERS)) { - $operation['deletesubfolders'] = true; - if (($dsf = self::$decoder->getElementContent()) !== false) { - $operation['deletesubfolders'] = (boolean)$dsf; - if(!self::$decoder->getElementEndTag()) - return false; - } + if ($efc) { + if(self::$decoder->getElementStartTag(SYNC_FOLDERID)) { + $operation['folderid'] = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false;//SYNC_FOLDERID } + if(self::$decoder->getElementStartTag(SYNC_ITEMOPERATIONS_OPTIONS)) { + if(self::$decoder->getElementStartTag(SYNC_ITEMOPERATIONS_DELETESUBFOLDERS)) { + $operation['deletesubfolders'] = true; + if (($dsf = self::$decoder->getElementContent()) !== false) { + $operation['deletesubfolders'] = (boolean)$dsf; + if(!self::$decoder->getElementEndTag()) + return false; + } + } + self::$decoder->getElementEndTag(); + } + } + + //TODO move + + //break if it reached the endtag SYNC_ITEMOPERATIONS_FETCH or SYNC_ITEMOPERATIONS_EMPTYFOLDERCONTENTS or SYNC_ITEMOPERATIONS_MOVE + $e = self::$decoder->peek(); + if($e[EN_TYPE] == EN_TYPE_ENDTAG) { self::$decoder->getElementEndTag(); + break; } - } - - //TODO move - - if(!self::$decoder->getElementEndTag()) - return false; //SYNC_ITEMOPERATIONS_FETCH or SYNC_ITEMOPERATIONS_EMPTYFOLDERCONTENTS or SYNC_ITEMOPERATIONS_MOVE + } // end while operation $itemoperations[] = $operation; //break if it reached the endtag @@ -236,11 +237,7 @@ class ItemOperations extends RequestProcessor { self::$decoder->getElementEndTag(); //SYNC_ITEMOPERATIONS_ITEMOPERATIONS break; } - - } - -// if(!self::$decoder->getElementEndTag()) -// return false;//SYNC_ITEMOPERATIONS_ITEMOPERATIONS + } // end operations loop $status = SYNC_ITEMOPERATIONSSTATUS_SUCCESS; diff --git a/sources/lib/request/request.php b/sources/lib/request/request.php index d37b4b4..6511aa5 100644 --- a/sources/lib/request/request.php +++ b/sources/lib/request/request.php @@ -113,8 +113,12 @@ class Request { self::$command = self::filterEvilInput($_GET["Cmd"], self::LETTERS_ONLY); // getUser is unfiltered, as everything is allowed.. even "/", "\" or ".." - if(isset($_GET["User"])) + if(isset($_GET["User"])) { self::$getUser = strtolower($_GET["User"]); + if(defined('USE_FULLEMAIL_FOR_LOGIN') && ! USE_FULLEMAIL_FOR_LOGIN) { + self::$getUser = Utils::GetLocalPartFromEmail(self::$getUser); + } + } if(isset($_GET["DeviceId"])) self::$devid = strtolower(self::filterEvilInput($_GET["DeviceId"], self::WORDCHAR_ONLY)); if(isset($_GET["DeviceType"])) @@ -140,8 +144,12 @@ class Request { if (!isset(self::$command) && isset($query['Command'])) self::$command = Utils::GetCommandFromCode($query['Command']); - if (!isset(self::$getUser) && isset($query[self::COMMANDPARAM_USER])) + if (!isset(self::$getUser) && isset($query[self::COMMANDPARAM_USER])) { self::$getUser = strtolower($query[self::COMMANDPARAM_USER]); + if(defined('USE_FULLEMAIL_FOR_LOGIN') && ! USE_FULLEMAIL_FOR_LOGIN) { + self::$getUser = Utils::GetLocalPartFromEmail(self::$getUser); + } + } if (!isset(self::$devid) && isset($query['DevID'])) self::$devid = strtolower(self::filterEvilInput($query['DevID'], self::WORDCHAR_ONLY)); @@ -172,8 +180,12 @@ class Request { } // in base64 encoded query string user is not necessarily set - if (!isset(self::$getUser) && isset($_SERVER['PHP_AUTH_USER'])) + if (!isset(self::$getUser) && isset($_SERVER['PHP_AUTH_USER'])) { list(self::$getUser,) = Utils::SplitDomainUser(strtolower($_SERVER['PHP_AUTH_USER'])); + if(defined('USE_FULLEMAIL_FOR_LOGIN') && ! USE_FULLEMAIL_FOR_LOGIN) { + self::$getUser = Utils::GetLocalPartFromEmail(self::$getUser); + } + } } /** @@ -233,6 +245,9 @@ class Request { list(self::$authUser, self::$authDomain) = Utils::SplitDomainUser($_SERVER['PHP_AUTH_USER']); self::$authPassword = (isset($_SERVER['PHP_AUTH_PW']))?$_SERVER['PHP_AUTH_PW'] : ""; } + if(defined('USE_FULLEMAIL_FOR_LOGIN') && ! USE_FULLEMAIL_FOR_LOGIN) { + self::$authUser = Utils::GetLocalPartFromEmail(self::$authUser); + } // authUser & authPassword are unfiltered! return (self::$authUser != "" && self::$authPassword != ""); } @@ -566,6 +581,32 @@ class Request { return (isset(self::$headers["content-length"]))? (int) self::$headers["content-length"] : 0; } + /** + * Returns the amount of seconds this request is able to be kept open without the client + * closing it. This depends on the vendor. + * + * @access public + * @return boolean + */ + static public function GetExpectedConnectionTimeout() { + // Different vendors implement different connection timeouts. + // In order to optimize processing, we return a specific time for the major + // classes currently known (feedback welcome). + // The amount of time returned is somehow lower than the max timeout so we have + // time for processing. + + // Apple and Windows Phone have higher timeouts (4min = 240sec) + if (in_array(self::GetDeviceType(), array("iPod", "iPad", "iPhone", "WP"))) { + return 200; + } + // Samsung devices have a intermediate timeout (90sec) + if (in_array(self::GetDeviceType(), array("SAMSUNGGTI"))) { + return 50; + } + + // for all other devices, a timeout of 30 seconds is expected + return 20; + } /**---------------------------------------------------------------------------------------------------------- * Private stuff diff --git a/sources/lib/request/search.php b/sources/lib/request/search.php index 643a7b6..d365fc8 100644 --- a/sources/lib/request/search.php +++ b/sources/lib/request/search.php @@ -401,25 +401,27 @@ class Search extends RequestProcessor { } } elseif ($searchname == ISearchProvider::SEARCH_MAILBOX) { - foreach ($rows as $u) { - self::$encoder->startTag(SYNC_SEARCH_RESULT); - self::$encoder->startTag(SYNC_FOLDERTYPE); - self::$encoder->content($u['class']); - self::$encoder->endTag(); - self::$encoder->startTag(SYNC_SEARCH_LONGID); - self::$encoder->content($u['longid']); - self::$encoder->endTag(); - self::$encoder->startTag(SYNC_FOLDERID); - self::$encoder->content($u['folderid']); - self::$encoder->endTag(); + if (is_array($rows) && !empty($rows)) { + foreach ($rows as $u) { + self::$encoder->startTag(SYNC_SEARCH_RESULT); + self::$encoder->startTag(SYNC_FOLDERTYPE); + self::$encoder->content($u['class']); + self::$encoder->endTag(); + self::$encoder->startTag(SYNC_SEARCH_LONGID); + self::$encoder->content($u['longid']); + self::$encoder->endTag(); + self::$encoder->startTag(SYNC_FOLDERID); + self::$encoder->content($u['folderid']); + self::$encoder->endTag(); - self::$encoder->startTag(SYNC_SEARCH_PROPERTIES); - $tmp = explode(":", $u['longid']); - $message = self::$backend->Fetch($u['folderid'], $tmp[1], $cpo); - $message->Encode(self::$encoder); + self::$encoder->startTag(SYNC_SEARCH_PROPERTIES); + $tmp = explode(":", $u['longid']); + $message = self::$backend->Fetch($u['folderid'], $tmp[1], $cpo); + $message->Encode(self::$encoder); - self::$encoder->endTag();//result - self::$encoder->endTag();//properties + self::$encoder->endTag();//result + self::$encoder->endTag();//properties + } } } // it seems that android 4 requires range and searchtotal diff --git a/sources/lib/request/sync.php b/sources/lib/request/sync.php index 6317f1e..b71e5cf 100644 --- a/sources/lib/request/sync.php +++ b/sources/lib/request/sync.php @@ -62,6 +62,18 @@ class Sync extends RequestProcessor { $wbxmlproblem = false; $emptysync = false; + + // check if the hierarchySync was fully completed + if (USE_PARTIAL_FOLDERSYNC) { + if (self::$deviceManager->GetFolderSyncComplete() === false) { + ZLog::Write(LOGLEVEL_INFO, "Request->HandleSync(): Sync request aborted, as exporting of folders has not yet completed"); + self::$topCollector->AnnounceInformation("Aborted due incomplete folder sync", true); + $status = SYNC_STATUS_FOLDERHIERARCHYCHANGED; + } + else + ZLog::Write(LOGLEVEL_INFO, "Request->HandleSync(): FolderSync marked as complete"); + } + // Start Synchronize if(self::$decoder->getElementStartTag(SYNC_SYNCHRONIZE)) { @@ -189,7 +201,7 @@ class Sync extends RequestProcessor { else $supfields[] = $el[EN_TAG]; } - self::$deviceManager->SetSupportedFields($spa->GetFolderId(), $supfields); + self::$deviceManager->SetSupportedFields($spa->GetFolderId(), $supfields); } } diff --git a/sources/lib/syncobjects/syncappointment.php b/sources/lib/syncobjects/syncappointment.php index 879469c..6f929e5 100644 --- a/sources/lib/syncobjects/syncappointment.php +++ b/sources/lib/syncobjects/syncappointment.php @@ -125,7 +125,7 @@ class SyncAppointment extends SyncObject { // 3 = Out of office SYNC_POOMCAL_BUSYSTATUS => array ( self::STREAMER_VAR => "busystatus", self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETTWO, - self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2,3) )), + self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2,3,4) )), SYNC_POOMCAL_ALLDAYEVENT => array ( self::STREAMER_VAR => "alldayevent", self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ZEROORONE => self::STREAMER_CHECK_SETZERO)), diff --git a/sources/lib/syncobjects/syncmail.php b/sources/lib/syncobjects/syncmail.php index 7918a93..d694396 100644 --- a/sources/lib/syncobjects/syncmail.php +++ b/sources/lib/syncobjects/syncmail.php @@ -164,7 +164,7 @@ class SyncMail extends SyncObject { self::STREAMER_ARRAY => SYNC_AIRSYNCBASE_ATTACHMENT); $mapping[SYNC_POOMMAIL_CONTENTCLASS] = array ( self::STREAMER_VAR => "contentclass", - self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(DEFAULT_EMAIL_CONTENTCLASS) )); + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(DEFAULT_EMAIL_CONTENTCLASS, DEFAULT_CALENDAR_CONTENTCLASS) )); $mapping[SYNC_POOMMAIL_FLAG] = array ( self::STREAMER_VAR => "flag", self::STREAMER_TYPE => "SyncMailFlags", diff --git a/sources/lib/syncobjects/syncmeetingrequest.php b/sources/lib/syncobjects/syncmeetingrequest.php index a293cbd..66167a2 100644 --- a/sources/lib/syncobjects/syncmeetingrequest.php +++ b/sources/lib/syncobjects/syncmeetingrequest.php @@ -62,6 +62,7 @@ class SyncMeetingRequest extends SyncObject { public $busystatus; public $timezone; public $globalobjid; + public $disallownewtimeproposal; function SyncMeetingRequest() { $mapping = array ( @@ -126,6 +127,10 @@ class SyncMeetingRequest extends SyncObject { self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => base64_encode(pack("la64vvvvvvvv"."la64vvvvvvvv"."l",0,"",0,0,0,0,0,0,0,0,0,"",0,0,0,0,0,0,0,0,0)) )), SYNC_POOMMAIL_GLOBALOBJID => array ( self::STREAMER_VAR => "globalobjid"), + + SYNC_POOMMAIL_DISALLOWNEWTIMEPROPOSAL => array ( self::STREAMER_VAR => "disallownewtimeproposal", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO, + self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), ); parent::SyncObject($mapping); diff --git a/sources/lib/utils/compat.php b/sources/lib/utils/compat.php index 1c3bbef..528dd97 100644 --- a/sources/lib/utils/compat.php +++ b/sources/lib/utils/compat.php @@ -49,10 +49,54 @@ if (!function_exists("quoted_printable_encode")) { * * @param string $string string to be encoded * - * @see http://www.php.net/manual/en/function.quoted-printable-decode.php#89417 + * @see http://www.php.net/manual/en/function.quoted-printable-encode.php#106078 */ function quoted_printable_encode($string) { - return preg_replace('/[^\r\n]{73}[^=\r\n]{2}/', "$0=\n", str_replace(array('%20', '%0D%0A', '%'), array(' ', "\r\n", '='), rawurlencode($string))); + $lp = 0; + $ret = ''; + $hex = "0123456789ABCDEF"; + $length = strlen($str); + $str_index = 0; + + while ($length--) { + if ((($c = $str[$str_index++]) == "\015") && ($str[$str_index] == "\012") && $length > 0) { + $ret .= "\015"; + $ret .= $str[$str_index++]; + $length--; + $lp = 0; + } else { + if (ctype_cntrl($c) + || (ord($c) == 0x7f) + || (ord($c) & 0x80) + || ($c == '=') + || (($c == ' ') && ($str[$str_index] == "\015"))) + { + if (($lp += 3) > PHP_QPRINT_MAXL) + { + $ret .= '='; + $ret .= "\015"; + $ret .= "\012"; + $lp = 3; + } + $ret .= '='; + $ret .= $hex[ord($c) >> 4]; + $ret .= $hex[ord($c) & 0xf]; + } + else + { + if ((++$lp) > PHP_QPRINT_MAXL) + { + $ret .= '='; + $ret .= "\015"; + $ret .= "\012"; + $lp = 1; + } + $ret .= $c; + } + } + } + + return $ret; } } diff --git a/sources/lib/utils/timezoneutil.php b/sources/lib/utils/timezoneutil.php index 910b3fc..d994587 100644 --- a/sources/lib/utils/timezoneutil.php +++ b/sources/lib/utils/timezoneutil.php @@ -1100,11 +1100,23 @@ class TimezoneUtil { ZLog::Write(LOGLEVEL_DEBUG, "TimezoneUtil::GetFullTZ() for ". $phptimezone); $servertzname = self::guessTZNameFromPHPName($phptimezone); - $offset = self::$tzonesoffsets[$servertzname]; + return self::GetFullTZFromTZName($servertzname); + } + + /** + * Returns a full timezone array + * + * @param string $tzname a TZID value + * + * @access public + * @return array + */ + static public function GetFullTZFromTZName($tzname) { + $offset = self::$tzonesoffsets[$tzname]; $tz = array( "bias" => $offset[0], - "tzname" => self::encodeTZName(self::getMSTZnameFromTZName($servertzname)), + "tzname" => self::encodeTZName(self::getMSTZnameFromTZName($tzname)), "dstendyear" => $offset[3], "dstendmonth" => $offset[4], "dstendday" => $offset[6], @@ -1114,7 +1126,7 @@ class TimezoneUtil { "dstendsecond" => $offset[9], "dstendmillis" => $offset[10], "stdbias" => $offset[1], - "tznamedst" => self::encodeTZName(self::getMSTZnameFromTZName($servertzname)), + "tznamedst" => self::encodeTZName(self::getMSTZnameFromTZName($tzname)), "dststartyear" => $offset[11], "dststartmonth" => $offset[12], "dststartday" => $offset[14], @@ -1258,6 +1270,26 @@ class TimezoneUtil { } } + /** + * Pack timezone info for Sync + * + * @param array $tz + * + * @access private + * @return string + */ + static public function GetSyncBlobFromTZ($tz) { + // set the correct TZ name (done using the Bias) + if (!isset($tz["tzname"]) || !$tz["tzname"] || !isset($tz["tznamedst"]) || !$tz["tznamedst"]) + $tz = TimezoneUtil::FillTZNames($tz); + + $packed = pack("la64vvvvvvvv" . "la64vvvvvvvv" . "l", + $tz["bias"], $tz["tzname"], 0, $tz["dstendmonth"], $tz["dstendday"], $tz["dstendweek"], $tz["dstendhour"], $tz["dstendminute"], $tz["dstendsecond"], $tz["dstendmillis"], + $tz["stdbias"], $tz["tznamedst"], 0, $tz["dststartmonth"], $tz["dststartday"], $tz["dststartweek"], $tz["dststarthour"], $tz["dststartminute"], $tz["dststartsecond"], $tz["dststartmillis"], + $tz["dstbias"]); + + return $packed; + } } ?> \ No newline at end of file diff --git a/sources/lib/utils/utils.php b/sources/lib/utils/utils.php index aadcec6..296b54c 100644 --- a/sources/lib/utils/utils.php +++ b/sources/lib/utils/utils.php @@ -917,6 +917,78 @@ class Utils { return 0; } + + /** + * Returns the local part from email address. + * + * @param string $email + * + * @access public + * @return string + */ + public static function GetLocalPartFromEmail($email) { + $pos = strpos($email, '@'); + if ($pos === false) { + return $email; + } + return substr($email, 0, $pos); + } + + /** + * Generate date object from string and timezone. + * + * @param string $value + * @param string $timezone + * + * @access public + * @return int epoch + */ + public static function MakeUTCDate($value, $timezone = null) { + $tz = null; + if ($timezone) { + $tz = timezone_open($timezone); + } + if (!$tz) { + //If there is no timezone set, we use the default timezone + $tz = timezone_open(date_default_timezone_get()); + } + //20110930T090000Z + $date = date_create_from_format('Ymd\THis\Z', $value, timezone_open("UTC")); + if (!$date) { + //20110930T090000 + $date = date_create_from_format('Ymd\THis', $value, $tz); + } + if (!$date) { + //20110930 (Append T000000Z to the date, so it starts at midnight) + $date = date_create_from_format('Ymd\THis\Z', $value . "T000000Z", $tz); + } + return date_timestamp_get($date); + } + + + /** + * Generate a tzid from various formats + * + * @param str $timezone + * + * @access public + * @return timezone id + */ + public static function ParseTimezone($timezone) { + //(GMT+01.00) Amsterdam / Berlin / Bern / Rome / Stockholm / Vienna + if (preg_match('/GMT(\\+|\\-)0(\d)/', $timezone, $matches)) { + return "Etc/GMT" . $matches[1] . $matches[2]; + } + //(GMT+10.00) XXX / XXX / XXX / XXX + if (preg_match('/GMT(\\+|\\-)1(\d)/', $timezone, $matches)) { + return "Etc/GMT" . $matches[1] . "1" . $matches[2]; + } + ///inverse.ca/20101018_1/Europe/Amsterdam or /inverse.ca/20101018_1/America/Argentina/Buenos_Aires + if (preg_match('/\/[.[:word:]]+\/\w+\/(\w+)\/([\w\/]+)/', $timezone, $matches)) { + return $matches[1] . "/" . $matches[2]; + } + return trim($timezone, '"'); + } } diff --git a/sources/sql/mysql.sql b/sources/sql/mysql.sql new file mode 100644 index 0000000..84904d4 --- /dev/null +++ b/sources/sql/mysql.sql @@ -0,0 +1,19 @@ +create table zpush_settings (key_name varchar(50) not null, key_value varchar(50) not null, created_at datetime not null, updated_at datetime not null, primary key (key_name)); + +create table zpush_users (username varchar(50) not null, device_id varchar(50) not null, created_at datetime not null, updated_at datetime not null, primary key (username, device_id)); + +create table zpush_states (id_state integer auto_increment, device_id varchar(50) not null, uuid varchar(50), state_type varchar(50), counter integer, state_data mediumtext not null, + created_at datetime not null, updated_at datetime not null, primary key (id_state)); + +create unique index idx_zpush_states_unique on zpush_states (device_id, uuid, state_type, counter); + +-- This is optional, and will require extra configuration in your mysql +-- http://www.mysqlperformanceblog.com/2012/05/30/data-compression-in-innodb-for-text-and-blob-fields/ +alter table zpush_states engine=InnoDB row_format=compressed key_block_size=16; + + +-- This table has a primary key id integer, because I will be linking a Rails model against it (admin wui) +create table zpush_preauth_users (id integer auto_increment, username varchar(50) not null, device_id varchar(50) not null, authorized boolean not null, + created_at datetime not null, updated_at datetime not null, primary key (id)); + +create unique index index_zpush_preauth_users_on_username_and_device_id on zpush_preauth_users (username, device_id); \ No newline at end of file diff --git a/sources/testing/samples/meeting_request.txt b/sources/testing/samples/meeting_request.txt new file mode 100644 index 0000000..d8df4ba --- /dev/null +++ b/sources/testing/samples/meeting_request.txt @@ -0,0 +1,51 @@ +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:Microsoft Exchange Server 2010 +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Romance Standard Time +BEGIN:STANDARD +DTSTART:16010101T030000 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +ORGANIZER;CN=Pablo Marmol:MAILTO:pablo.marmol@zpush.org +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=pedro.pica + piedra@zpush.org:MAILTO:pedro.picapiedra@zpush.org +DESCRIPTION;LANGUAGE=es-ES:Texto de la segunda cita\n\n +SUMMARY;LANGUAGE=es-ES:Segunda cita +DTSTART;TZID=Romance Standard Time:20140519T090000 +DTEND;TZID=Romance Standard Time:20140519T093000 +UID:040000008200E00074C5B7101A82E0080000000070BC3EB80871CF01000000000000000 + 010000000B3492E4691795F4E810CCD60A178B53C +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20140516T111408Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:0 +LOCATION;LANGUAGE=es-ES:Oficina +X-MICROSOFT-CDO-APPT-SEQUENCE:0 +X-MICROSOFT-CDO-OWNERAPPTID:860903390 +X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:0 +X-MICROSOFT-DISALLOW-COUNTER:FALSE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:REMINDER +TRIGGER;RELATED=START:-PT15M +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/sources/testing/samples/messages/emoticon.txt b/sources/testing/samples/messages/emoticon.txt new file mode 100644 index 0000000..93f64b8 --- /dev/null +++ b/sources/testing/samples/messages/emoticon.txt @@ -0,0 +1,28 @@ +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on xxxxx +X-Spam-Level: +X-Spam-Status: No, score=-1.0 required=5.0 tests=ALL_TRUSTED autolearn=ham + autolearn_force=no version=3.4.0 +Delivered-To: xxxx@xxxxxxxxxxxx +Received: from localhost (xxxx.xxxxxxxxx.xx [127.0.0.1]) + by xxxx.xxxxxxxxxxx.xx (Postfix) with ESMTP id D8AB65001D6 + for ; Sun, 20 Jul 2014 15:30:58 +0200 (CEST) +X-Virus-Scanned: Debian amavisd-new at xxxx.xxxxxxxxx.xx +Received: from xxxx.xxxxxxxxx.xxx ([127.0.0.1]) + by localhost (xxxx.xxxxxxxxxx.xx [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id vI3uJWoUh-Pz for ; + Sun, 20 Jul 2014 15:30:58 +0200 (CEST) +Received-SPF: none (xxxx.xxx: No applicable sender policy available) receiver=xxxx.xxxxxxxxxx.xx; identity=mailfrom; envelope-from="xxxx.xxxxx@xxxx.xxxx"; helo=$ +From: xxxxxxxxxx +Content-Type: text/plain; + charset=utf-8 +Content-Transfer-Encoding: quoted-printable +Mime-Version: 1.0 (1.0) +Subject: Testing +Message-Id: +Date: Sun, 20 Jul 2014 15:31:09 +0200 +To: xxxx xxxxx + +Testing emojis =F0=9F=98=84 + +Sendt fra min iPad= diff --git a/sources/testing/samples/messages/emoticon_base64.txt b/sources/testing/samples/messages/emoticon_base64.txt new file mode 100644 index 0000000..ca768cb --- /dev/null +++ b/sources/testing/samples/messages/emoticon_base64.txt @@ -0,0 +1,39 @@ +Return-Path: xxxxxx@xxxxxxxx.xxx +X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on mail.xxxxxxxxx.xx +X-Spam-Level: +X-Spam-Status: No, score=-1.0 required=5.0 tests=ALL_TRUSTED, HEADER_FROM_DIFFERENT_DOMAINS autolearn=unavailable autolearn_force=no version=3.4.0 +Delivered-To: xxxxxxx@xxxxxxxxxxx.xx +Received: from localhost (xxxxx.xxxxxx.xx [127.0.0.1]) by xxxxx.xxxxxxxxxx.xx (Postfix) with ESMTP id 1982C500AD4; Fri, 11 Jul 2014 21:20:53 +0200 (CEST) +X-Virus-Scanned: Debian amavisd-new at xxxxx.xxxxxxx.xx +Received: from xxxxx.xxxxxxxxxx.xx ([127.0.0.1]) by localhost (xxxx.xxxxxxxx.xx [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id 2nP27OZ1CUzN; Fri, 11 Jul 2014 21:20:52 +0200 (CEST) +Resent-From: xxxxx@xxxxxxx.xx +DMARC-Filter: OpenDMARC Filter v1.2.0 xxxx.xxxxxx.xxx 9Dxxx7500A9D +X-Virus-Scanned: Debian amavisd-new at xxxxx.xxxxxxxxxx.xx +MIME-Version: 1.0 +DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=xxxxxxxx.xx; s=mail; t=1405106446; bh=ufc8aM3vMmQISb8aKXpA3Rk8t/+ce6kFhZUr1x6DwtU=; h=From:Subject:Date:To:From; b=uQb7eWfRZdkUF+didgfmgJTWW78GdyHJlEqhz+WFl2Ouqkt7iXZPwljR4OIoFvumK1IJqsXWRW74Py+Mq2CWkxeuML/9kTrWBXJDMichOqll666EgG8/DyyYH6RPIhje9ygILX85E4VO0zO1N4uh0Tc/D0Ow93hUWS51D1cX7gA= +From: xxxxx@xxxxxxxx.xx +Subject: Kontroll +Message-ID: 28EF0B82-61D9-4BA3-B534-CE4FB6101D1A@xxxxxx.xxx +Date: Fri, 11 Jul 2014 21:21:03 +0200 +To: stxxx xxxxx@xxxxxxxxx.xx +Content-Type: multipart/mixed; boundary="=_83c3cde24966264fd2b3537156aaaba6" +Received-SPF: Pass (xxxx.xxxxxxxx.loc: domain of xxxxxx@xxxxxx.xx designates xxx.xxx.x.xx as permitted sender) receiver=xxxx.xxxxxxxx.xxx; client-ip=xxx.xxx.x.xx; helo=xxxxx.xxxxxxx.xx; +X-Auto-Response-Suppress: DR, OOF, AutoReply +Resent-Message-Id: 20140711192053.1982C500AD4@xxxxxx.xxxxxx.xx +Resent-Date: Fri, 11 Jul 2014 21:20:53 +0200 (CEST) + +--=_83c3cde24966264fd2b3537156aaaba6 +Content-Type: multipart/alternative; boundary="=_3eaa2d9769aa280737bc3e5268efb2ad" + +--=_3eaa2d9769aa280737bc3e5268efb2ad +Content-Transfer-Encoding: base64 +Content-Type: text/plain; charset="utf-8" + +RGV0IGVyIHDDpSB0aWRlIGF0dCB2aSBwbGFubGVnZ2VyIHVuaWZvcm1zIGtvbnRyb2xsIA0KS3Zh +ciBoZWxnIG5vZW4gc29tIGlra2plIGhhcg0KQWxsZSBtw6Uga3VubmUgZ2rDuHIgZW4gaW5uc2F0 +cyBldHRlciBmZXJpZW4gMiAgcGVycyB4IDQgaGVsZ2VyIHPDpSB0cm9yIGVnIG15ZSBhdiBwcm9i +bGVtbWV0IGVyIHZla2tlDQpHb2Qgc29tbWVyIG9nIGZlcmllIPCfmI4NClZpYmVrZQ0KDQpTZW5k +dCBmcmEgbWluIGlQaG9uZQ== +--=_3eaa2d9769aa280737bc3e5268efb2ad-- + +--=_83c3cde24966264fd2b3537156aaaba6-- diff --git a/sources/testing/samples/messages/emoticon_subject.txt b/sources/testing/samples/messages/emoticon_subject.txt new file mode 100644 index 0000000..cc17c76 --- /dev/null +++ b/sources/testing/samples/messages/emoticon_subject.txt @@ -0,0 +1,20 @@ +Mime-Version: 1.0 +From: xxxx@xxxxxxx.xxx +Subject: =?utf-8?B?8J+YhA==?= +Message-id: EC08EA2B-1439-423D-AA69-843776D5C39C@xxxxxxxx.xx +Date: Wed, 23 Jul 2014 19:50:36 +0200 +To: test2014@xxxxxxxxxxx.xx +Content-Type: multipart/mixed; boundary="=_72ced2dca575896ff9586958e1bb6592" + +This is a multi-part message in MIME format. +--=_72ced2dca575896ff9586958e1bb6592 +Content-Type: multipart/alternative; boundary="=_9818f474f7be6252ec285590ed281857" + +--=_9818f474f7be6252ec285590ed281857 +Content-Transfer-Encoding: base64 +Content-Type: text/plain; charset=utf-8 + +8J+Y4oCeDQoNClNlbmR0IGZyYSBtaW4gaVBob25l +--=_9818f474f7be6252ec285590ed281857-- + +--=_72ced2dca575896ff9586958e1bb6592-- diff --git a/sources/testing/samples/messages/french.txt b/sources/testing/samples/messages/french.txt new file mode 100644 index 0000000..15c3490 --- /dev/null +++ b/sources/testing/samples/messages/french.txt @@ -0,0 +1,48 @@ +Return-Path: +Delivered-To: informatique@domain.com +Received: from localhost (localhost.localdomain [127.0.0.1]) + by domain.com (Postfix) with ESMTP id AB32E1000EA + for ; Sun, 7 Sep 2014 09:50:21 +0200 (CEST) +Authentication-Results: domain.com (amavisd-new); + dkim=pass (1024-bit key) reason="pass (just generated, assumed good)" + header.d=domain.com +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d= + domain.com; h=content-transfer-encoding:content-type + :content-type:mime-version:from:from:subject:subject:date:date + :received; s=mail; t=1410076220; x=1411890621; bh=paEf4SYhXmrsFW + vtqTtrw7K9W9eprm8TMRs5JiO2p6o=; b=Dszu0yEi/ktiqQoFdLPKxkvdDB+sU+ + 3b+B2O0Jf2PzVahHvMj7bGJSoW0eVb1u6tot6q5NOjFH85Ab1MZdhoga+jMW3h8E + 5GrVvA4NumMV/HMJ8M/1LpAc5nTaGxN+8KfGHxCM0DsjZEFwuLwL3xU6PgSrggcC + 6DT6GcpnPTRxM= +X-Virus-Scanned: Debian amavisd-new at domain.com +X-Spam-Flag: NO +X-Spam-Score: 0.138 +X-Spam-Level: +X-Spam-Status: No, score=0.138 tagged_above=-999 required=4 + tests=[MISSING_MID=0.14, NO_RECEIVED=-0.001, NO_RELAYS=-0.001] + autolearn=no +Received: from domain.com ([127.0.0.1]) + by localhost (domain.com [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id YDofG_pOTs47 for ; + Sun, 7 Sep 2014 09:50:20 +0200 (CEST) +Date: Sun, 07 Sep 2014 09:50:03 +0200 +Subject: =?ISO-8859-1?Q?Re:_[TC5_BAT]_Construction_g=E9n=E9rale_/_08:30?= +From: =?ISO-8859-1?Q?Edt_Secr=E9tariat_ESITC_Cachan?= + +To: Informatique ESITC +MIME-Version: 1.0 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: base64 +Message-Id: <20140907075021.AB32E1000EA@domain.com> + +Qydlc3QgcmVjdGlmacOpLgpCb25uZSBqb3VybsOpZS4KTGUgc2VjcsOpdGFyaWF0IGVkdMKgIGRl +IGwnRVNJVEMKCkxlIDYgc2VwdC4gMjAxNCAyMzo0NCwgSW5mb3JtYXRpcXVlIEVTSVRDIDxpbmZv +cm1hdGlxdWVAYWRtLmVzaXRjLWNhY2hhbi5mcj4gYSDDqWNyaXQgOgo+Cj4gQm9uam91ciwgCj4K +PiBJbCBzZW1ibGVyYWl0IHF1J2lsIHkgYWl0IHVuZSBwZXRpdGUgZXJyZXVyIHN1ciBsJ2VtcGxv +aSBkdSB0ZW1wcyBkZXMgCj4gVEM1IEJBVC4gCj4KPiBMZSBMdW5kaSAwNSBKYW52aWVyIDIwMTUg +w6AgMDhIMzAuIExlIGNvdXJzIGRlIENHIGR1cmUgMWgzMCBhbG9ycyBxdSdpbCB5IAo+IGEgMiBz +w6lhbmNlcy4gCj4gRG9uYyBzb2l0IGMnZXN0IAo+IFNvaXQgdW5lIHPDqWFuY2UgZGUgMUgzMCAK +PiBTb2l0IDIgc8OpYW5jZXMgZGUgM0ggKGF1IHRvdGFsKSAKPiBNYWlzIHBhcyAyIHPDqWFuY2Vz +IGRlIDFIMzAgKGF1IHRvdGFsKSAKPgo+IENvcmRpYWxlbWVudCwgCj4gSW5mb3JtYXRpcXVlIEVT +SVRDIAo= + diff --git a/sources/testing/samples/messages/m0001.txt b/sources/testing/samples/messages/m0001.txt new file mode 100644 index 0000000..5615811 --- /dev/null +++ b/sources/testing/samples/messages/m0001.txt @@ -0,0 +1,31 @@ +From: "Doug Sauder" +To: "Jürgen Schmürgen" +Subject: Die Hasen und die Frösche (Microsoft Outlook 00) +Date: Wed, 17 May 2000 19:08:29 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: 8bit +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +Die Hasen und die Frösche + +Die Hasen klagten einst über ihre mißliche Lage; "wir leben", sprach ein +Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, der +Adler, ja fast aller Raubtiere! Unsere stete Angst ist ärger als der Tod +selbst. Auf, laßt uns ein für allemal sterben." + +In einem nahen Teich wollten sie sich nun ersäufen; sie eilten ihm zu; +allein das außerordentliche Getöse und ihre wunderbare Gestalt erschreckte +eine Menge Frösche, die am Ufer saßen, so sehr, daß sie aufs schnellste +untertauchten. + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ersäufen noch ein +wenig aufschieben, denn auch uns fürchten, wie ihr seht, einige Tiere, +welche also wohl noch unglücklicher sein müssen als wir." + diff --git a/sources/testing/samples/messages/m0002.txt b/sources/testing/samples/messages/m0002.txt new file mode 100644 index 0000000..b98b41d --- /dev/null +++ b/sources/testing/samples/messages/m0002.txt @@ -0,0 +1,31 @@ +From: "Doug Sauder" +To: "Jürgen Schmürgen" +Subject: Die Hasen und die Frösche (Microsoft Outlook 00) +Date: Wed, 17 May 2000 19:10:31 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach = +ein Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der = +Hunde, der Adler, ja fast aller Raubtiere! Unsere stete Angst ist = +=E4rger als der Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; = +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt = +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF = +sie aufs schnellste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch = +ein wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige = +Tiere, welche also wohl noch ungl=FCcklicher sein m=FCssen als wir."=20 + diff --git a/sources/testing/samples/messages/m0003.txt b/sources/testing/samples/messages/m0003.txt new file mode 100644 index 0000000..c5109ef --- /dev/null +++ b/sources/testing/samples/messages/m0003.txt @@ -0,0 +1,30 @@ +From: "Doug Sauder" +To: "Jürgen Schmürgen" +Subject: Die Hasen und die Frösche (Microsoft Outlook 00) +Date: Wed, 17 May 2000 19:11:50 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: base64 +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +RGllIEhhc2VuIHVuZCBkaWUgRnL2c2NoZQ0KDQpEaWUgSGFzZW4ga2xhZ3RlbiBlaW5zdCD8YmVy +IGlocmUgbWnfbGljaGUgTGFnZTsgIndpciBsZWJlbiIsIHNwcmFjaCBlaW4gUmVkbmVyLCAiaW4g +c3RldGVyIEZ1cmNodCB2b3IgTWVuc2NoZW4gdW5kIFRpZXJlbiwgZWluZSBCZXV0ZSBkZXIgSHVu +ZGUsIGRlciBBZGxlciwgamEgZmFzdCBhbGxlciBSYXVidGllcmUhIFVuc2VyZSBzdGV0ZSBBbmdz +dCBpc3Qg5HJnZXIgYWxzIGRlciBUb2Qgc2VsYnN0LiBBdWYsIGxh33QgdW5zIGVpbiBm/HIgYWxs +ZW1hbCBzdGVyYmVuLiIgDQoNCkluIGVpbmVtIG5haGVuIFRlaWNoIHdvbGx0ZW4gc2llIHNpY2gg +bnVuIGVyc+R1ZmVuOyBzaWUgZWlsdGVuIGlobSB6dTsgYWxsZWluIGRhcyBhdd9lcm9yZGVudGxp +Y2hlIEdldPZzZSB1bmQgaWhyZSB3dW5kZXJiYXJlIEdlc3RhbHQgZXJzY2hyZWNrdGUgZWluZSBN +ZW5nZSBGcvZzY2hlLCBkaWUgYW0gVWZlciBzYd9lbiwgc28gc2VociwgZGHfIHNpZSBhdWZzIHNj +aG5lbGxzdGUgdW50ZXJ0YXVjaHRlbi4gDQoNCiJIYWx0IiwgcmllZiBudW4gZWJlbiBkaWVzZXIg +U3ByZWNoZXIsICJ3aXIgd29sbGVuIGRhcyBFcnPkdWZlbiBub2NoIGVpbiB3ZW5pZyBhdWZzY2hp +ZWJlbiwgZGVubiBhdWNoIHVucyBm/HJjaHRlbiwgd2llIGlociBzZWh0LCBlaW5pZ2UgVGllcmUs +IHdlbGNoZSBhbHNvIHdvaGwgbm9jaCB1bmds/GNrbGljaGVyIHNlaW4gbfxzc2VuIGFscyB3aXIu +IiANCg== + diff --git a/sources/testing/samples/messages/m0004.txt b/sources/testing/samples/messages/m0004.txt new file mode 100644 index 0000000..fe16b2a --- /dev/null +++ b/sources/testing/samples/messages/m0004.txt @@ -0,0 +1,31 @@ +From: "Doug Sauder" +To: =?iso-8859-1?B?SvxyZ2VuIFNjaG38cmdlbg==?= +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Fr=F6sche_=28Microsoft_Outlook_00=29?= +Date: Wed, 17 May 2000 19:13:51 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: 8bit +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +Die Hasen und die Frösche + +Die Hasen klagten einst über ihre mißliche Lage; "wir leben", sprach ein +Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, der +Adler, ja fast aller Raubtiere! Unsere stete Angst ist ärger als der Tod +selbst. Auf, laßt uns ein für allemal sterben." + +In einem nahen Teich wollten sie sich nun ersäufen; sie eilten ihm zu; +allein das außerordentliche Getöse und ihre wunderbare Gestalt erschreckte +eine Menge Frösche, die am Ufer saßen, so sehr, daß sie aufs schnellste +untertauchten. + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ersäufen noch ein +wenig aufschieben, denn auch uns fürchten, wie ihr seht, einige Tiere, +welche also wohl noch unglücklicher sein müssen als wir." + diff --git a/sources/testing/samples/messages/m0005.txt b/sources/testing/samples/messages/m0005.txt new file mode 100644 index 0000000..56f1089 --- /dev/null +++ b/sources/testing/samples/messages/m0005.txt @@ -0,0 +1,31 @@ +From: "Doug Sauder" +To: =?iso-8859-1?B?SvxyZ2VuIFNjaG38cmdlbg==?= +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Fr=F6sche_=28Microsoft_Outlook_00=29?= +Date: Wed, 17 May 2000 19:15:35 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach = +ein Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der = +Hunde, der Adler, ja fast aller Raubtiere! Unsere stete Angst ist = +=E4rger als der Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; = +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt = +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF = +sie aufs schnellste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch = +ein wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige = +Tiere, welche also wohl noch ungl=FCcklicher sein m=FCssen als wir."=20 + diff --git a/sources/testing/samples/messages/m0006.txt b/sources/testing/samples/messages/m0006.txt new file mode 100644 index 0000000..a51ea70 --- /dev/null +++ b/sources/testing/samples/messages/m0006.txt @@ -0,0 +1,34 @@ +From: "Doug Sauder" +To: "Joe Blow" , + =?utf-7?B?SitBUHctcmdlbiBTY2htK0FQdy1yZ2Vu?= +Subject: =?utf-7?Q?Die_Hasen_und_die_Fr+APY-sche_=28Microsoft_Outlook_00=29?= +Date: Wed, 17 May 2000 19:18:39 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: text/plain; + charset="utf-7" +Content-Transfer-Encoding: quoted-printable +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +Die Hasen und die Fr+APY-sche + +Die Hasen klagten einst +APw-ber ihre mi+AN8-liche Lage+ADs- +ACI-wir = +leben+ACI-, sprach ein Redner, +ACI-in steter Furcht vor Menschen und = +Tieren, eine Beute der Hunde, der Adler, ja fast aller Raubtiere+ACE- = +Unsere stete Angst ist +AOQ-rger als der Tod selbst. Auf, la+AN8-t uns = +ein f+APw-r allemal sterben.+ACI-=20 + +In einem nahen Teich wollten sie sich nun ers+AOQ-ufen+ADs- sie eilten = +ihm zu+ADs- allein das au+AN8-erordentliche Get+APY-se und ihre = +wunderbare Gestalt erschreckte eine Menge Fr+APY-sche, die am Ufer = +sa+AN8-en, so sehr, da+AN8- sie aufs schnellste untertauchten.=20 + ++ACI-Halt+ACI-, rief nun eben dieser Sprecher, +ACI-wir wollen das = +Ers+AOQ-ufen noch ein wenig aufschieben, denn auch uns f+APw-rchten, wie = +ihr seht, einige Tiere, welche also wohl noch ungl+APw-cklicher sein = +m+APw-ssen als wir.+ACI-=20 + diff --git a/sources/testing/samples/messages/m0007.txt b/sources/testing/samples/messages/m0007.txt new file mode 100644 index 0000000..cd5c589 --- /dev/null +++ b/sources/testing/samples/messages/m0007.txt @@ -0,0 +1,32 @@ +From: "Doug Sauder" +To: =?utf-7?Q?Heinz_M+APw-ller?= +Subject: =?utf-7?Q?Die_Hasen_und_die_Fr+APY-sche_=28Microsoft_Outlook_00=29?= +Date: Wed, 17 May 2000 19:20:24 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: text/plain; + charset="utf-7" +Content-Transfer-Encoding: quoted-printable +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +Die Hasen und die Fr+APY-sche + +Die Hasen klagten einst +APw-ber ihre mi+AN8-liche Lage+ADs- +ACI-wir = +leben+ACI-, sprach ein Redner, +ACI-in steter Furcht vor Menschen und = +Tieren, eine Beute der Hunde, der Adler, ja fast aller Raubtiere+ACE- = +Unsere stete Angst ist +AOQ-rger als der Tod selbst. Auf, la+AN8-t uns = +ein f+APw-r allemal sterben.+ACI-=20 + +In einem nahen Teich wollten sie sich nun ers+AOQ-ufen+ADs- sie eilten = +ihm zu+ADs- allein das au+AN8-erordentliche Get+APY-se und ihre = +wunderbare Gestalt erschreckte eine Menge Fr+APY-sche, die am Ufer = +sa+AN8-en, so sehr, da+AN8- sie aufs schnellste untertauchten.=20 + ++ACI-Halt+ACI-, rief nun eben dieser Sprecher, +ACI-wir wollen das = +Ers+AOQ-ufen noch ein wenig aufschieben, denn auch uns f+APw-rchten, wie = +ihr seht, einige Tiere, welche also wohl noch ungl+APw-cklicher sein = +m+APw-ssen als wir.+ACI-=20 diff --git a/sources/testing/samples/messages/m0008.txt b/sources/testing/samples/messages/m0008.txt new file mode 100644 index 0000000..80c620c --- /dev/null +++ b/sources/testing/samples/messages/m0008.txt @@ -0,0 +1,32 @@ +From: "Doug Sauder" +To: =?utf-8?Q?Heinz_M=C3=BCller?= +Subject: =?utf-8?Q?Die_Hasen_und_die_Fr=C3=B6sche_=28Microsoft_Outlook_00=29?= +Date: Wed, 17 May 2000 19:27:04 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: text/plain; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +Die Hasen und die Fr=C3=B6sche + +Die Hasen klagten einst =C3=BCber ihre mi=C3=9Fliche Lage; "wir leben", = +sprach ein Redner, "in steter Furcht vor Menschen und Tieren, eine Beute = +der Hunde, der Adler, ja fast aller Raubtiere! Unsere stete Angst ist = +=C3=A4rger als der Tod selbst. Auf, la=C3=9Ft uns ein f=C3=BCr allemal = +sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=C3=A4ufen; sie eilten ihm = +zu; allein das au=C3=9Ferordentliche Get=C3=B6se und ihre wunderbare = +Gestalt erschreckte eine Menge Fr=C3=B6sche, die am Ufer sa=C3=9Fen, so = +sehr, da=C3=9F sie aufs schnellste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=C3=A4ufen = +noch ein wenig aufschieben, denn auch uns f=C3=BCrchten, wie ihr seht, = +einige Tiere, welche also wohl noch ungl=C3=BCcklicher sein m=C3=BCssen = +als wir."=20 diff --git a/sources/testing/samples/messages/m0009.txt b/sources/testing/samples/messages/m0009.txt new file mode 100644 index 0000000..e25390c --- /dev/null +++ b/sources/testing/samples/messages/m0009.txt @@ -0,0 +1,27 @@ +From: "Doug Sauder" +To: "Heinz Müller" +Subject: Die Hasen und die Frösche (Microsoft Outlook 00) +Date: Wed, 17 May 2000 19:28:40 -0400 +Message-ID: +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +Die Hasen und die Frösche + +Die Hasen klagten einst über ihre mißliche Lage; "wir leben", sprach ein +Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, der +Adler, ja fast aller Raubtiere! Unsere stete Angst ist ärger als der Tod +selbst. Auf, laßt uns ein für allemal sterben." + +In einem nahen Teich wollten sie sich nun ersäufen; sie eilten ihm zu; +allein das außerordentliche Getöse und ihre wunderbare Gestalt erschreckte +eine Menge Frösche, die am Ufer saßen, so sehr, daß sie aufs schnellste +untertauchten. + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ersäufen noch ein +wenig aufschieben, denn auch uns fürchten, wie ihr seht, einige Tiere, +welche also wohl noch unglücklicher sein müssen als wir." + diff --git a/sources/testing/samples/messages/m0010.txt b/sources/testing/samples/messages/m0010.txt new file mode 100644 index 0000000..e353439 --- /dev/null +++ b/sources/testing/samples/messages/m0010.txt @@ -0,0 +1,30 @@ +From: "Doug Sauder" +To: =?Windows-1252?Q?Heinz_M=FCller?= +Subject: =?Windows-1252?Q?Die_Hasen_und_die_Fr=F6sche_=28Microsoft_Outlook_00=29?= +Date: Wed, 17 May 2000 19:30:20 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: text/plain; + charset="Windows-1252" +Content-Transfer-Encoding: quoted-printable +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach = +ein Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der = +Hunde, der Adler, ja fast aller Raubtiere! Unsere stete Angst ist = +=E4rger als der Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; = +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt = +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF = +sie aufs schnellste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch = +ein wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige = +Tiere, welche also wohl noch ungl=FCcklicher sein m=FCssen als wir."=20 diff --git a/sources/testing/samples/messages/m0011.txt b/sources/testing/samples/messages/m0011.txt new file mode 100644 index 0000000..95eb05a --- /dev/null +++ b/sources/testing/samples/messages/m0011.txt @@ -0,0 +1,135 @@ +From: "Doug Sauder" +To: =?iso-8859-1?Q?Heinz_M=FCller?= +Subject: Test message from Microsoft Outlook 00 +Date: Wed, 17 May 2000 19:32:47 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_0002_01BFC036.AE309650" +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +This is a multi-part message in MIME format. + +------=_NextPart_000_0002_01BFC036.AE309650 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + +The Hare and the Tortoise=20 +=20 +A HARE one day ridiculed the short feet and slow pace of the Tortoise, = +who replied, laughing: "Though you be swift as the wind, I will beat = +you in a race." The Hare, believing her assertion to be simply = +impossible, assented to the proposal; and they agreed that the Fox = +should choose the course and fix the goal. On the day appointed for the = +race the two started together. The Tortoise never for a moment stopped, = +but went on with a slow but steady pace straight to the end of the = +course. The Hare, lying down by the wayside, fell fast asleep. At last = +waking up, and moving as fast as he could, he saw the Tortoise had = +reached the goal, and was comfortably dozing after her fatigue. =20 +=20 +Slow but steady wins the race. =20 + +------=_NextPart_000_0002_01BFC036.AE309650 +Content-Type: image/png; + name="blueball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= + +------=_NextPart_000_0002_01BFC036.AE309650 +Content-Type: image/png; + name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="greenball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAAIQAA +CAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAhrQBCvRhj +xjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBCzhDO55Te563G +55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY1gAx5wBS7yFr7zlK +7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAp1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAFtSURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy ++N9ery1bVe9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh +0fHJaTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJVG9Ea +EjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyexI0ZxBP3a +fE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWrYUO23hMANUKR +Rl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0RVh0Q29tbWVudABj +bGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= + +------=_NextPart_000_0002_01BFC036.AE309650 +Content-Type: image/png; + name="redball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="redball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa +AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0 +AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM +AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm +f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB +AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2 +AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH +AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC +AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe +AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs +AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV +AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM +AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK +iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ +29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+ +d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q +m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV +tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw +HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5 +QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd +tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5 +IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== + +------=_NextPart_000_0002_01BFC036.AE309650-- diff --git a/sources/testing/samples/messages/m0012.txt b/sources/testing/samples/messages/m0012.txt new file mode 100644 index 0000000..44bbe3b --- /dev/null +++ b/sources/testing/samples/messages/m0012.txt @@ -0,0 +1,43 @@ +From: "Doug Sauder" +To: "Joe Blow" +Subject: Test message from Microsoft Outlook 00 +Date: Wed, 17 May 2000 19:35:05 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: image/png; + name="redball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="redball.png" +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa +AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0 +AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM +AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm +f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB +AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2 +AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH +AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC +AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe +AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs +AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV +AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM +AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK +iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ +29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+ +d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q +m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV +tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw +HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5 +QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd +tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5 +IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== diff --git a/sources/testing/samples/messages/m0013.txt b/sources/testing/samples/messages/m0013.txt new file mode 100644 index 0000000..d9a38fa --- /dev/null +++ b/sources/testing/samples/messages/m0013.txt @@ -0,0 +1,83 @@ +From: "Doug Sauder" +To: "Joe Blow" +Subject: Test message from Microsoft Outlook 00 +Date: Wed, 17 May 2000 19:36:13 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_0004_01BFC037.28F2FA90" +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +This is a multi-part message in MIME format. + +------=_NextPart_000_0004_01BFC037.28F2FA90 +Content-Type: image/png; + name="blueball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= + +------=_NextPart_000_0004_01BFC037.28F2FA90 +Content-Type: image/png; + name="redball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="redball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa +AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0 +AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM +AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm +f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB +AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2 +AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH +AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC +AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe +AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs +AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV +AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM +AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK +iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ +29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+ +d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q +m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV +tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw +HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5 +QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd +tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5 +IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== + +------=_NextPart_000_0004_01BFC037.28F2FA90-- diff --git a/sources/testing/samples/messages/m0014.txt b/sources/testing/samples/messages/m0014.txt new file mode 100644 index 0000000..213493c --- /dev/null +++ b/sources/testing/samples/messages/m0014.txt @@ -0,0 +1,72 @@ +From: "Doug Sauder" +To: "Joe Blow" +Subject: Test message from Microsoft Outlook 00 +Date: Wed, 17 May 2000 19:38:28 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0005_01BFC037.799BEF60" +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +This is a multi-part message in MIME format. + +------=_NextPart_000_0005_01BFC037.799BEF60 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + +The Hare and the Tortoise=20 +=20 +A HARE one day ridiculed the short feet and slow pace of the Tortoise, = +who replied, laughing: "Though you be swift as the wind, I will beat = +you in a race." The Hare, believing her assertion to be simply = +impossible, assented to the proposal; and they agreed that the Fox = +should choose the course and fix the goal. On the day appointed for the = +race the two started together. The Tortoise never for a moment stopped, = +but went on with a slow but steady pace straight to the end of the = +course. The Hare, lying down by the wayside, fell fast asleep. At last = +waking up, and moving as fast as he could, he saw the Tortoise had = +reached the goal, and was comfortably dozing after her fatigue. =20 +=20 +Slow but steady wins the race. =20 + + +------=_NextPart_000_0005_01BFC037.799BEF60 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + +

The Hare and the Tortoise = +
 
A HARE=20 +one day ridiculed the short feet and slow pace of the Tortoise, who = +replied,=20 +laughing:  "Though you be swift as the wind, I will beat you in a=20 +race."  The Hare, believing her assertion to be simply impossible, = +assented=20 +to the proposal; and they agreed that the Fox should choose the course = +and fix=20 +the goal.  On the day appointed for the race the two started=20 +together.  The Tortoise never for a moment stopped, but went on = +with a slow=20 +but steady pace straight to the end of the course.  The Hare, lying = +down by=20 +the wayside, fell fast asleep.  At last waking up, and moving as = +fast as he=20 +could, he saw the Tortoise had reached the goal, and was comfortably = +dozing=20 +after her fatigue. 
 
Slow but steady wins the = +race. =20 +
+ +------=_NextPart_000_0005_01BFC037.799BEF60-- diff --git a/sources/testing/samples/messages/m0015.txt b/sources/testing/samples/messages/m0015.txt new file mode 100644 index 0000000..353f096 --- /dev/null +++ b/sources/testing/samples/messages/m0015.txt @@ -0,0 +1,144 @@ +From: "Doug Sauder" +To: "Joe Blow" +Subject: Test message from Microsoft Outlook 00 +Date: Wed, 17 May 2000 19:42:10 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_0008_01BFC037.FDD8EE90" +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +This is a multi-part message in MIME format. + +------=_NextPart_000_0008_01BFC037.FDD8EE90 +Content-Type: multipart/alternative; + boundary="----=_NextPart_001_0009_01BFC037.FDD8EE90" + + +------=_NextPart_001_0009_01BFC037.FDD8EE90 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + +The Hare and the Tortoise=20 +=20 +A HARE one day ridiculed the short feet and slow pace of the Tortoise, = +who replied, laughing: "Though you be swift as the wind, I will beat = +you in a race." The Hare, believing her assertion to be simply = +impossible, assented to the proposal; and they agreed that the Fox = +should choose the course and fix the goal. On the day appointed for the = +race the two started together. The Tortoise never for a moment stopped, = +but went on with a slow but steady pace straight to the end of the = +course. The Hare, lying down by the wayside, fell fast asleep. At last = +waking up, and moving as fast as he could, he saw the Tortoise had = +reached the goal, and was comfortably dozing after her fatigue. =20 +=20 +Slow but steady wins the race. =20 + + +------=_NextPart_001_0009_01BFC037.FDD8EE90 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + +

The Hare and the Tortoise = +
 
A HARE=20 +one day ridiculed the short feet and slow pace of the Tortoise, who = +replied,=20 +laughing:  "Though you be swift as the wind, I will beat you in a=20 +race."  The Hare, believing her assertion to be simply impossible, = +assented=20 +to the proposal; and they agreed that the Fox should choose the course = +and fix=20 +the goal.  On the day appointed for the race the two started=20 +together.  The Tortoise never for a moment stopped, but went on = +with a slow=20 +but steady pace straight to the end of the course.  The Hare, lying = +down by=20 +the wayside, fell fast asleep.  At last waking up, and moving as = +fast as he=20 +could, he saw the Tortoise had reached the goal, and was comfortably = +dozing=20 +after her fatigue. 
 
Slow but steady wins the = +race. =20 +
+ +------=_NextPart_001_0009_01BFC037.FDD8EE90-- + +------=_NextPart_000_0008_01BFC037.FDD8EE90 +Content-Type: image/png; + name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="greenball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAAIQAA +CAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAhrQBCvRhj +xjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBCzhDO55Te563G +55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY1gAx5wBS7yFr7zlK +7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAp1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAFtSURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy ++N9ery1bVe9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh +0fHJaTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJVG9Ea +EjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyexI0ZxBP3a +fE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWrYUO23hMANUKR +Rl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0RVh0Q29tbWVudABj +bGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= + +------=_NextPart_000_0008_01BFC037.FDD8EE90 +Content-Type: image/png; + name="redball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="redball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa +AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0 +AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM +AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm +f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB +AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2 +AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH +AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC +AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe +AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs +AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV +AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM +AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK +iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ +29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+ +d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q +m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV +tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw +HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5 +QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd +tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5 +IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== + +------=_NextPart_000_0008_01BFC037.FDD8EE90-- diff --git a/sources/testing/samples/messages/m0016.txt b/sources/testing/samples/messages/m0016.txt new file mode 100644 index 0000000..c121613 --- /dev/null +++ b/sources/testing/samples/messages/m0016.txt @@ -0,0 +1,156 @@ +From: "Doug Sauder" +To: "Joe Blow" +Subject: Test message from Microsoft Outlook 00 +Date: Wed, 17 May 2000 19:44:45 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: multipart/related; + boundary="----=_NextPart_000_000C_01BFC038.5A5C8E60" +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +This is a multi-part message in MIME format. + +------=_NextPart_000_000C_01BFC038.5A5C8E60 +Content-Type: multipart/alternative; + boundary="----=_NextPart_001_000D_01BFC038.5A5C8E60" + + +------=_NextPart_001_000D_01BFC038.5A5C8E60 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + +The Hare and the Tortoise=20 +=20 +A HARE one day ridiculed the short feet and slow pace of the Tortoise, = +who replied, laughing: "Though you be swift as the wind, I will beat = +you in a race." The Hare, believing her assertion to be simply = +impossible, assented to the proposal; and they agreed that the Fox = +should choose the course and fix the goal. On the day appointed for the = +race the two started together. The Tortoise never for a moment stopped, = +but went on with a slow but steady pace straight to the end of the = +course. The Hare, lying down by the wayside, fell fast asleep. At last = +waking up, and moving as fast as he could, he saw the Tortoise had = +reached the goal, and was comfortably dozing after her fatigue. =20 + + +=20 +Slow but steady wins the race. =20 + + + + +------=_NextPart_001_000D_01BFC038.5A5C8E60 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + +

The Hare and the Tortoise = +
 
A HARE=20 +one day ridiculed the short feet and slow pace of the Tortoise, who = +replied,=20 +laughing:  "Though you be swift as the wind, I will beat you in a=20 +race."  The Hare, believing her assertion to be simply impossible, = +assented=20 +to the proposal; and they agreed that the Fox should choose the course = +and fix=20 +the goal.  On the day appointed for the race the two started=20 +together.  The Tortoise never for a moment stopped, but went on = +with a slow=20 +but steady pace straight to the end of the course.  The Hare, lying = +down by=20 +the wayside, fell fast asleep.  At last waking up, and moving as = +fast as he=20 +could, he saw the Tortoise had reached the goal, and was comfortably = +dozing=20 +after her fatigue.  
+
 
+
3D"blue
 
Slow but = +steady wins=20 +the race. 
+
 
+
3D"red
+ +------=_NextPart_001_000D_01BFC038.5A5C8E60-- + +------=_NextPart_000_000C_01BFC038.5A5C8E60 +Content-Type: image/png; + name="blueball.png" +Content-Transfer-Encoding: base64 +Content-ID: <823504223@17052000-0f8d> + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= + +------=_NextPart_000_000C_01BFC038.5A5C8E60 +Content-Type: image/png; + name="redball.png" +Content-Transfer-Encoding: base64 +Content-ID: <823504223@17052000-0f94> + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa +AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0 +AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM +AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm +f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB +AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2 +AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH +AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC +AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe +AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs +AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV +AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM +AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK +iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ +29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+ +d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q +m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV +tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw +HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5 +QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd +tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5 +IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== + +------=_NextPart_000_000C_01BFC038.5A5C8E60-- diff --git a/sources/testing/samples/messages/m0017.txt b/sources/testing/samples/messages/m0017.txt new file mode 100644 index 0000000..a22ff6d --- /dev/null +++ b/sources/testing/samples/messages/m0017.txt @@ -0,0 +1,188 @@ +From: "Doug Sauder" +To: "Joe Blow" +Subject: Test message from Microsoft Outlook 00 +Date: Wed, 17 May 2000 19:47:24 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_0010_01BFC038.B91BC650" +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +This is a multi-part message in MIME format. + +------=_NextPart_000_0010_01BFC038.B91BC650 +Content-Type: multipart/related; + boundary="----=_NextPart_001_0011_01BFC038.B91BC650" + + +------=_NextPart_001_0011_01BFC038.B91BC650 +Content-Type: multipart/alternative; + boundary="----=_NextPart_002_0012_01BFC038.B91BC650" + + +------=_NextPart_002_0012_01BFC038.B91BC650 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + +The Hare and the Tortoise=20 +=20 +A HARE one day ridiculed the short feet and slow pace of the Tortoise, = +who replied, laughing: "Though you be swift as the wind, I will beat = +you in a race." The Hare, believing her assertion to be simply = +impossible, assented to the proposal; and they agreed that the Fox = +should choose the course and fix the goal. On the day appointed for the = +race the two started together. The Tortoise never for a moment stopped, = +but went on with a slow but steady pace straight to the end of the = +course. The Hare, lying down by the wayside, fell fast asleep. At last = +waking up, and moving as fast as he could, he saw the Tortoise had = +reached the goal, and was comfortably dozing after her fatigue. =20 +=20 +Slow but steady wins the race. =20 + + + +------=_NextPart_002_0012_01BFC038.B91BC650 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + +
3D"blue
+

The Hare and the Tortoise = +
 
A HARE=20 +one day ridiculed the short feet and slow pace of the Tortoise, who = +replied,=20 +laughing:  "Though you be swift as the wind, I will beat you in a=20 +race."  The Hare, believing her assertion to be simply impossible, = +assented=20 +to the proposal; and they agreed that the Fox should choose the course = +and fix=20 +the goal.  On the day appointed for the race the two started=20 +together.  The Tortoise never for a moment stopped, but went on = +with a slow=20 +but steady pace straight to the end of the course.  The Hare, lying = +down by=20 +the wayside, fell fast asleep.  At last waking up, and moving as = +fast as he=20 +could, he saw the Tortoise had reached the goal, and was comfortably = +dozing=20 +after her fatigue. 
 
Slow but steady wins the = +race. =20 +
+

 
+ +------=_NextPart_002_0012_01BFC038.B91BC650-- + +------=_NextPart_001_0011_01BFC038.B91BC650 +Content-Type: image/png; + name="blueball.png" +Content-Transfer-Encoding: base64 +Content-ID: <938014623@17052000-0f9b> + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= + +------=_NextPart_001_0011_01BFC038.B91BC650-- + +------=_NextPart_000_0010_01BFC038.B91BC650 +Content-Type: image/png; + name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="greenball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAAIQAA +CAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAhrQBCvRhj +xjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBCzhDO55Te563G +55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY1gAx5wBS7yFr7zlK +7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAp1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAFtSURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy ++N9ery1bVe9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh +0fHJaTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJVG9Ea +EjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyexI0ZxBP3a +fE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWrYUO23hMANUKR +Rl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0RVh0Q29tbWVudABj +bGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= + +------=_NextPart_000_0010_01BFC038.B91BC650 +Content-Type: image/png; + name="redball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="redball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa +AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0 +AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM +AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm +f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB +AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2 +AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH +AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC +AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe +AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs +AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV +AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM +AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK +iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ +29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+ +d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q +m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV +tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw +HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5 +QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd +tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5 +IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== + +------=_NextPart_000_0010_01BFC038.B91BC650-- diff --git a/sources/testing/samples/messages/m0018.txt b/sources/testing/samples/messages/m0018.txt new file mode 100644 index 0000000..d56b749 --- /dev/null +++ b/sources/testing/samples/messages/m0018.txt @@ -0,0 +1,131 @@ +From: "Doug Sauder" +To: "Joe Blow" +Subject: Test message from Microsoft Outlook 00 +Date: Wed, 17 May 2000 19:58:13 -0400 +Message-ID: +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + + +The Hare and the Tortoise + +A HARE one day ridiculed the short feet and slow pace of the Tortoise, who +replied, laughing: "Though you be swift as the wind, I will beat you in a +race." The Hare, believing her assertion to be simply impossible, assented +to the proposal; and they agreed that the Fox should choose the course and +fix the goal. On the day appointed for the race the two started together. +The Tortoise never for a moment stopped, but went on with a slow but steady +pace straight to the end of the course. The Hare, lying down by the +wayside, fell fast asleep. At last waking up, and moving as fast as he +could, he saw the Tortoise had reached the goal, and was comfortably dozing +after her fatigue. + +Slow but steady wins the race. + + +begin 666 blueball.png +MB5!.1PT*&@H````-24A$4@```!L````;" ,```"Z"@1G```#`%!,5$7___\` +M``@``! ``!@`````""D`$$(`$$H`"#$`$%(((7,(*7L(*80((6L`&%H`"#D( +M(6,0.9P80J480JT80K40.:40,90`&%(0,8PI4K4Y8\8Y8\XI6LX82LX80LX8 +M0KU":\9[G-:\ZMSMZESN]:C.\0,9Q:A-:MUN?.[^^,O>]*>^\8.:482L:, +MO>>UY^^4O>\``"E:C.=SI>]KG.\(*8PQ8^\I6N\((7L````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M``````````````````````````````````````````````````````````#! +M,@U5`````7123E,`0.;89@```!9T15AT4V]F='=AOF@```&(241!5'B<==)M5YLP& ;@$DZ )A!2DX;,"4RG!2+:JHO;K&_; +M?/G_ORA[TE;LL<>;+YQSG3LAY!F-A@0H1"APHYT$.(HA48+#CXS'A*80DK$H +M0=OHT(*,54I*9+0#>J23(LO^U\/RJJ64PV(!L1,[WT[//I^?#)K6MGI +M+#)O>P8QG>P?GO:G9^?SIFP7BC"\*;J$:W%QV??]U8]Y4]7"4I:#>0PB,I'7 +M/_O^U^^;65/60H&%:PMCNKR]OKN_?_"U5B[>S3T66I0'?_[^N_&[U:+3A*_, +MC1QF3Z*=S9]?&D]R:M,L3@9+EW75@%1 "ZLIC_UW>GLLJ)5M65:W-=!2:<+\ +M^59F8J*F==U"28H.B,.2&PMSEJJE%%(N.JOT:U9$9K4D6("++%6VZZRU*O6$ +MUS5_B# 'U$HIG5+"BR@9"(HF'W-"*7TEG(^!UBNNS"&3QP7CG#.X=[-I;7XV +M8))'D!PF`FV3QP`9@XT)W^7M;OUK@. 9P&T-Q6ZVI^E3^8B[X[L#_P$45,S8 +MF6D.OP```"5T15AT0V]M;65N= !C;&EP,F=I9B!V+C N-B!B>2!9=F5S(%!I +49W5E=#9S.[P`````245.1*Y"8((` +` +end + +begin 666 greenball.png +MB5!.1PT*&@H````-24A$4@```!L````;" ,```"Z"@1G```#`%!,5$7___\` +M````$ ``& ``(0``" ``,0``0@``4@``6@``2@`(8P`(P`0C `(:P`` +M.0``8P`0E `0G `AI0`0I0`AK0!"O1ACQC%CQCE2QB$IS@`8O0`0K0`8K0`A +MO0"4UF.MUGN$UE)*WA@AU@`8Q@`8M0``*0!"SA#.YY3>YZW&YY24YV-2YR$A +MW@`8S@!2WB&KRU;5>]/60`]SX,0^'\8HC[&`29]Y('?:!&F+.2< +MAR(@R.NVVF@02C4<#N-8#:LMN[^]G#XU-FDB)VC_PW8#K614(E9K+_6\Z7)DR)5 +M&]$:$C(QV7R9F9)&BH>4:+^V`8\38\HF1ZGD+*AN[TSC4*5)7LIWJN2V#52) +M6-AWSS)*"R>Q(T9Q!/W:?$T$ERJ.E9)RP\KW!$V2%B)"RQ XWX2"D@ZY1A=> +M0!EC0M"@W]+6K ]T1##&U$6K84.VWA,`-4*11EZU$YV]<1^^#V&Y+*TTV=K_ +MJK>-T@@L+ ```"5T15AT0V]M;65N= !C;&EP,F=I +F9B!V+C N-B!B>2!9=F5S(%!I9W5E=#9S.[P`````245.1*Y"8((` +` +end + +begin 666 redball.png +MB5!.1PT*&@H````-24A$4@```!L````;" ,```"Z"@1G```#`%!,5$7___\` +M```!```+```5```:```7```1```*```#```<```R``!$``!-``!(```]```C +M```6```F``!A``![``"&``"'``!]``!T``!@```Y```4```&```G``!+``!O +M``"0``"E``"[``"_``"K``"A``",``!S``!;```N```(``!,``!W``"9``"T +M&1G*.#C5/3W;*2G=!06X``"H``!Y```Q```8```$``!%``":``"[)"3186'? +MA(3F?W_F5E;J'Q_2``"Y``"C``!=``!"```H```)``!G``"Z#P_05%3>DY/E +MK*SIF9GM967O)277``#!``"Q``"<``!M``!3```V```;```%``!*``"!``#+ +M("#=9V?HG)SKIJ;MBHKO45'O%!31``##``"V``!X``!>``! ```B``!7``"2 +M``#"``#:&QOH557L>'CO>'CO5U?O)B;A``#.``"W``"G``"5``!'```K```/ +M``"=``#%``#A!07K*"CO/#SO-37O&QOC``#0``#)``"U``"7``"$``!L``!/ +M```2```"``!B``#I``#O`@+G``#8``#+``"Z``"P``!P```3```D``!8``#( +M``#3``#-``"S``"#``!N```>``!^``# ``"D``"-``!_``!I``!0```P``"1 +M``"I``"\``"J``";``!E``!)```J```.```T``"L``"O``"M``"F``")``!Z +M``!K``!:```^```I``!J``""``"?``">``"6``"/``!\```9```'``!5``". +M``"*```X```0```_``!R``" ``!<```W```L``!F``!#``!6```@```S```\ +M```Z```?```,```=```-```````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M``````````````````````````````````````````````````````````#\ +M+ME%`````7123E,`0.;89@```!9T15AT4V]F='=AOF@```((241!5'B<8V" @_\0P( !F)A96%G9V#DX,:4YN+AY>'GY^+D% +M!#E0)?\S"_$(BXB*B4M(\DH)2?D%A47%R26E9E$=Y1258[C^3D&15=4UM77U#8U.SJWW[10_(IZTQ.W@($E7_+;BD#F\3WZ*P;-F1H[.WSI'D/29P'!J+__\? +MESUQV..DR*)3B[:J;SNM?49(V@(N%\%VEK_#Y=PV^?9=PIE\85.E(^")X_]_ +MCH.R8>=Y0S,OA&JGFV5T6B"EF___#TE?7'M)ZO+E*]P[KQ[O0DY20);%M2FL +MUR]>O7B=[09:<@.QF2QDM-S=M2PP4^)_5(":"/%(84W7`)A(.MC?YL?R```` +M)71%6'1#;VUM96YT`&-L:7 R9VEF('8N,"XV(&)Y(%EV97,@4&EG=65T-G,[ +-O ````!)14Y$KD)@@@`` +` +end diff --git a/sources/testing/samples/messages/m1001.txt b/sources/testing/samples/messages/m1001.txt new file mode 100644 index 0000000..a5b8214 --- /dev/null +++ b/sources/testing/samples/messages/m1001.txt @@ -0,0 +1,32 @@ +Message-ID: <3923561C.B7078DEF@example.com> +Date: Wed, 17 May 2000 22:31:57 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: =?iso-8859-1?Q?J=FCrgen=20Schm=FCrgen?= +Subject: Die Hasen und die =?iso-8859-1?Q?Fr=F6sche?= (Netscape Communicator 4.7) +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach = +ein +Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, +der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als de= +r +Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben." + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs +schnellste untertauchten. + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ei= +n +wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere, +welche also wohl noch ungl=FCcklicher sein m=FCssen als wir." + + diff --git a/sources/testing/samples/messages/m1002.txt b/sources/testing/samples/messages/m1002.txt new file mode 100644 index 0000000..15a8d34 --- /dev/null +++ b/sources/testing/samples/messages/m1002.txt @@ -0,0 +1,61 @@ +Message-ID: <392359CF.DFF4527@example.com> +Date: Wed, 17 May 2000 22:47:43 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Heinz =?iso-8859-1?Q?M=FCller?= +Subject: Die Hasen und die =?iso-8859-1?Q?Fr=F6sche?= (Netscape Communicator 4.7) +Content-Type: multipart/alternative; + boundary="------------9D454F23DA86BCD63FA3805F" + + +--------------9D454F23DA86BCD63FA3805F +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach = +ein +Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, +der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als de= +r +Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben." + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs +schnellste untertauchten. + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ei= +n +wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere, +welche also wohl noch ungl=FCcklicher sein m=FCssen als wir." + + + +--------------9D454F23DA86BCD63FA3805F +Content-Type: text/html; charset=us-ascii +Content-Transfer-Encoding: 7bit + + + +Die Hasen und die Frösche +

Die Hasen klagten einst über ihre mißliche Lage; "wir leben", +sprach ein Redner, "in steter Furcht vor Menschen und Tieren, eine Beute +der Hunde, der Adler, ja fast aller Raubtiere! Unsere stete Angst ist ärger +als der Tod selbst. Auf, laßt uns ein für allemal sterben." +

In einem nahen Teich wollten sie sich nun ersäufen; sie eilten +ihm zu; allein das außerordentliche Getöse und ihre wunderbare +Gestalt erschreckte eine Menge Frösche, die am Ufer saßen, so +sehr, daß sie aufs schnellste untertauchten. +

"Halt", rief nun eben dieser Sprecher, "wir wollen das Ersäufen +noch ein wenig aufschieben, denn auch uns fürchten, wie ihr seht, +einige Tiere, welche also wohl noch unglücklicher sein müssen +als wir." +
  +
  + +--------------9D454F23DA86BCD63FA3805F-- diff --git a/sources/testing/samples/messages/m1003.txt b/sources/testing/samples/messages/m1003.txt new file mode 100644 index 0000000..d64bf91 --- /dev/null +++ b/sources/testing/samples/messages/m1003.txt @@ -0,0 +1,137 @@ +Message-ID: <39235E1C.1DC7EA90@example.com> +Date: Wed, 17 May 2000 23:06:04 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Joe Blow +Subject: Test message from Netscape Communicator 4.7 +Content-Type: multipart/mixed; + boundary="------------A1FCDEE154E03D875E5D6779" + +This is a multi-part message in MIME format. +--------------A1FCDEE154E03D875E5D6779 +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach = +ein +Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, +der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als de= +r +Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben." + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs +schnellste untertauchten. + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ei= +n +wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere, +welche also wohl noch ungl=FCcklicher sein m=FCssen als wir." + + + +--------------A1FCDEE154E03D875E5D6779 +Content-Type: image/png; + name="redball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="redball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAV +AAAaAAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACH +AAB9AAB0AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABb +AAAuAAAIAABMAAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACa +AAC7JCTRYWHfhITmf3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5Pl +rKzpmZntZWXvJSXXAADBAACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzr +pqbtiorvUVHvFBTRAADDAAC2AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjv +V1fvJibhAADOAAC3AACnAACVAABHAAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQ +AADJAAC1AACXAACEAABsAABPAAASAAACAABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAAT +AAAkAABYAADIAADTAADNAACzAACDAABuAAAeAAB+AADAAACkAACNAAB/AABpAABQAAAwAACR +AACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACsAACvAACtAACmAACJAAB6AABrAABaAAA+ +AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABVAACOAACKAAA4AAAQAAA/AAByAACA +AABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAMAAAdAAANAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8 +LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAII +SURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iKiUtI8koJ +Scsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ29ja +2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2Yn +OAj+d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/ +uXLzVJ2qm6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW +0g0bN63crGqVtWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36Kw +bNmRo7O3zpHkPSZwHBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8 +YVOlI+CJ4/9/joOyYed5QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms +1y9evXid7QZacgOxmSxktNzdtSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21t +ZW50AGNsaXAyZ2lmIHYuMC42IGJ5IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== +--------------A1FCDEE154E03D875E5D6779 +Content-Type: image/png; + name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="greenball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAA +IQAACAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAh +rQBCvRhjxjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBC +zhDO55Te563G55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY +1gAx5wBS7yFr7zlK7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp +1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAFt +SURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy+N9ery1b +Ve9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh0fHJ +aTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJV +G9EaEjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyex +I0ZxBP3afE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWr +YUO23hMANUKRRl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0 +RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5C +YII= +--------------A1FCDEE154E03D875E5D6779 +Content-Type: image/png; + name="blueball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgA +AAAACCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQ +MZQAGFIQMYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYY +QsYQMaUAACHO5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9K +e+8YOaUYSsaMvee15++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB +Mg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAGI +SURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbsscebL5xznTsh +5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18PyqqW +Uw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/M +jRxmT6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1C +SYoOiMOSGwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIom +H3NCKX0lnI+B1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0N +xW62p+lT+Yi747sD/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBi +eSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= +--------------A1FCDEE154E03D875E5D6779-- diff --git a/sources/testing/samples/messages/m1004.txt b/sources/testing/samples/messages/m1004.txt new file mode 100644 index 0000000..266fd19 --- /dev/null +++ b/sources/testing/samples/messages/m1004.txt @@ -0,0 +1,133 @@ +Message-ID: <39235EAD.E48E2160@example.com> +Date: Wed, 17 May 2000 23:08:29 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Joe Blow +Subject: Test message from Netscape Communicator 4.7 +Content-Type: multipart/mixed; + boundary="------------C78F594988075E36AE03C243" + +This is a multi-part message in MIME format. +--------------C78F594988075E36AE03C243 +Content-Type: multipart/alternative; + boundary="------------D74AE2393FB01D1B284AE257" + + +--------------D74AE2393FB01D1B284AE257 +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach = +ein +Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, +der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als de= +r +Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben." + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs +schnellste untertauchten. + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ei= +n +wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere, +welche also wohl noch ungl=FCcklicher sein m=FCssen als wir." + + + +--------------D74AE2393FB01D1B284AE257 +Content-Type: text/html; charset=us-ascii +Content-Transfer-Encoding: 7bit + + + +Die Hasen und die Frösche +

Die Hasen klagten einst über ihre mißliche Lage; "wir leben", +sprach ein Redner, "in steter Furcht vor Menschen und Tieren, eine Beute +der Hunde, der Adler, ja fast aller Raubtiere! Unsere stete Angst ist ärger +als der Tod selbst. Auf, laßt uns ein für allemal sterben." +

In einem nahen Teich wollten sie sich nun ersäufen; sie eilten +ihm zu; allein das außerordentliche Getöse und ihre wunderbare +Gestalt erschreckte eine Menge Frösche, die am Ufer saßen, so +sehr, daß sie aufs schnellste untertauchten. +

"Halt", rief nun eben dieser Sprecher, "wir wollen das Ersäufen +noch ein wenig aufschieben, denn auch uns fürchten, wie ihr seht, +einige Tiere, welche also wohl noch unglücklicher sein müssen +als wir." +
  +
  + +--------------D74AE2393FB01D1B284AE257-- + +--------------C78F594988075E36AE03C243 +Content-Type: image/png; + name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="greenball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAA +IQAACAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAh +rQBCvRhjxjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBC +zhDO55Te563G55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY +1gAx5wBS7yFr7zlK7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp +1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAFt +SURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy+N9ery1b +Ve9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh0fHJ +aTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJV +G9EaEjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyex +I0ZxBP3afE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWr +YUO23hMANUKRRl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0 +RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5C +YII= +--------------C78F594988075E36AE03C243 +Content-Type: image/png; + name="blueball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgA +AAAACCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQ +MZQAGFIQMYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYY +QsYQMaUAACHO5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9K +e+8YOaUYSsaMvee15++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB +Mg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAGI +SURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbsscebL5xznTsh +5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18PyqqW +Uw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/M +jRxmT6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1C +SYoOiMOSGwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIom +H3NCKX0lnI+B1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0N +xW62p+lT+Yi747sD/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBi +eSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= +--------------C78F594988075E36AE03C243-- + diff --git a/sources/testing/samples/messages/m1005.txt b/sources/testing/samples/messages/m1005.txt new file mode 100644 index 0000000..8825c16 --- /dev/null +++ b/sources/testing/samples/messages/m1005.txt @@ -0,0 +1,212 @@ +Message-ID: <39235FC5.276CCE00@example.com> +Date: Wed, 17 May 2000 23:13:09 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Heinz =?iso-8859-1?Q?M=FCller?= +Subject: Die Hasen und die =?iso-8859-1?Q?Fr=F6sche?= (Netscape Messenger 4.7) +Content-Type: multipart/mixed; + boundary="------------A1E83A41894D3755390B838A" + +This is a multi-part message in MIME format. +--------------A1E83A41894D3755390B838A +Content-Type: multipart/alternative; + boundary="------------F03F94BA73D3B9E8C1B94D92" + + +--------------F03F94BA73D3B9E8C1B94D92 +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +[blue ball] + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach = +ein +Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, +der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als de= +r +Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben." + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs +schnellste untertauchten. + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ei= +n +wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere, +welche also wohl noch ungl=FCcklicher sein m=FCssen als wir." + +[Image] + + + +--------------F03F94BA73D3B9E8C1B94D92 +Content-Type: multipart/related; + boundary="------------C02FA3D0A04E95F295FB25EB" + + +--------------C02FA3D0A04E95F295FB25EB +Content-Type: text/html; charset=us-ascii +Content-Transfer-Encoding: 7bit + + + +blue ball +

Die Hasen und die Frösche +

Die Hasen klagten einst über ihre mißliche Lage; "wir leben", +sprach ein Redner, "in steter Furcht vor Menschen und Tieren, eine Beute +der Hunde, der Adler, ja fast aller Raubtiere! Unsere stete Angst ist ärger +als der Tod selbst. Auf, laßt uns ein für allemal sterben." +

In einem nahen Teich wollten sie sich nun ersäufen; sie eilten +ihm zu; allein das außerordentliche Getöse und ihre wunderbare +Gestalt erschreckte eine Menge Frösche, die am Ufer saßen, so +sehr, daß sie aufs schnellste untertauchten. +

"Halt", rief nun eben dieser Sprecher, "wir wollen das Ersäufen +noch ein wenig aufschieben, denn auch uns fürchten, wie ihr seht, +einige Tiere, welche also wohl noch unglücklicher sein müssen +als wir." +

+
  +
  + +--------------C02FA3D0A04E95F295FB25EB +Content-Type: image/png +Content-ID: +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="C:\TEMP\nsmailEG.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgA +AAAACCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQ +MZQAGFIQMYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYY +QsYQMaUAACHO5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9K +e+8YOaUYSsaMvee15++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB +Mg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAGI +SURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbsscebL5xznTsh +5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18PyqqW +Uw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/M +jRxmT6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1C +SYoOiMOSGwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIom +H3NCKX0lnI+B1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0N +xW62p+lT+Yi747sD/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBi +eSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= +--------------C02FA3D0A04E95F295FB25EB +Content-Type: image/png +Content-ID: +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="C:\TEMP\nsmail39.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAV +AAAaAAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACH +AAB9AAB0AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABb +AAAuAAAIAABMAAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACa +AAC7JCTRYWHfhITmf3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5Pl +rKzpmZntZWXvJSXXAADBAACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzr +pqbtiorvUVHvFBTRAADDAAC2AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjv +V1fvJibhAADOAAC3AACnAACVAABHAAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQ +AADJAAC1AACXAACEAABsAABPAAASAAACAABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAAT +AAAkAABYAADIAADTAADNAACzAACDAABuAAAeAAB+AADAAACkAACNAAB/AABpAABQAAAwAACR +AACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACsAACvAACtAACmAACJAAB6AABrAABaAAA+ +AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABVAACOAACKAAA4AAAQAAA/AAByAACA +AABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAMAAAdAAANAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8 +LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAII +SURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iKiUtI8koJ +Scsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ29ja +2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2Yn +OAj+d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/ +uXLzVJ2qm6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW +0g0bN63crGqVtWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36Kw +bNmRo7O3zpHkPSZwHBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8 +YVOlI+CJ4/9/joOyYed5QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms +1y9evXid7QZacgOxmSxktNzdtSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21t +ZW50AGNsaXAyZ2lmIHYuMC42IGJ5IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== +--------------C02FA3D0A04E95F295FB25EB-- + +--------------F03F94BA73D3B9E8C1B94D92-- + +--------------A1E83A41894D3755390B838A +Content-Type: image/png; + name="redball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="redball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAV +AAAaAAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACH +AAB9AAB0AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABb +AAAuAAAIAABMAAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACa +AAC7JCTRYWHfhITmf3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5Pl +rKzpmZntZWXvJSXXAADBAACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzr +pqbtiorvUVHvFBTRAADDAAC2AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjv +V1fvJibhAADOAAC3AACnAACVAABHAAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQ +AADJAAC1AACXAACEAABsAABPAAASAAACAABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAAT +AAAkAABYAADIAADTAADNAACzAACDAABuAAAeAAB+AADAAACkAACNAAB/AABpAABQAAAwAACR +AACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACsAACvAACtAACmAACJAAB6AABrAABaAAA+ +AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABVAACOAACKAAA4AAAQAAA/AAByAACA +AABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAMAAAdAAANAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8 +LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAII +SURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iKiUtI8koJ +Scsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ29ja +2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2Yn +OAj+d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/ +uXLzVJ2qm6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW +0g0bN63crGqVtWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36Kw +bNmRo7O3zpHkPSZwHBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8 +YVOlI+CJ4/9/joOyYed5QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms +1y9evXid7QZacgOxmSxktNzdtSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21t +ZW50AGNsaXAyZ2lmIHYuMC42IGJ5IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== +--------------A1E83A41894D3755390B838A +Content-Type: image/png; + name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="greenball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAA +IQAACAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAh +rQBCvRhjxjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBC +zhDO55Te563G55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY +1gAx5wBS7yFr7zlK7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp +1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAFt +SURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy+N9ery1b +Ve9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh0fHJ +aTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJV +G9EaEjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyex +I0ZxBP3afE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWr +YUO23hMANUKRRl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0 +RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5C +YII= +--------------A1E83A41894D3755390B838A-- + diff --git a/sources/testing/samples/messages/m1006.txt b/sources/testing/samples/messages/m1006.txt new file mode 100644 index 0000000..bd31d2e --- /dev/null +++ b/sources/testing/samples/messages/m1006.txt @@ -0,0 +1,171 @@ +Message-ID: <39236103.FFE674FC@example.com> +Date: Wed, 17 May 2000 23:18:27 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: =?iso-8859-1?Q?J=FCrgen=20Schm=FCrgen?= +Subject: Test message from Netscape Communicator 4.7 +Content-Type: multipart/mixed; + boundary="------------B7133A01A6B323BF00DBC9A7" + +This is a multi-part message in MIME format. +--------------B7133A01A6B323BF00DBC9A7 +Content-Type: multipart/related; + boundary="------------8E6A06810565BCAB5E1F7D97" + + +--------------8E6A06810565BCAB5E1F7D97 +Content-Type: text/html; charset=us-ascii +Content-Transfer-Encoding: 7bit + + + +blue ball +

Die Hasen und die Frösche +

Die Hasen klagten einst über ihre mißliche Lage; "wir leben", +sprach ein Redner, "in steter Furcht vor Menschen und Tieren, eine Beute +der Hunde, der Adler, ja fast aller Raubtiere! Unsere stete Angst ist ärger +als der Tod selbst. Auf, laßt uns ein für allemal sterben." +

In einem nahen Teich wollten sie sich nun ersäufen; sie eilten +ihm zu; allein das außerordentliche Getöse und ihre wunderbare +Gestalt erschreckte eine Menge Frösche, die am Ufer saßen, so +sehr, daß sie aufs schnellste untertauchten. +

"Halt", rief nun eben dieser Sprecher, "wir wollen das Ersäufen +noch ein wenig aufschieben, denn auch uns fürchten, wie ihr seht, +einige Tiere, welche also wohl noch unglücklicher sein müssen +als wir." +

red ball + +--------------8E6A06810565BCAB5E1F7D97 +Content-Type: image/png +Content-ID: +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="C:\TEMP\nsmailV0.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgA +AAAACCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQ +MZQAGFIQMYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYY +QsYQMaUAACHO5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9K +e+8YOaUYSsaMvee15++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB +Mg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAGI +SURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbsscebL5xznTsh +5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18PyqqW +Uw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/M +jRxmT6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1C +SYoOiMOSGwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIom +H3NCKX0lnI+B1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0N +xW62p+lT+Yi747sD/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBi +eSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= +--------------8E6A06810565BCAB5E1F7D97 +Content-Type: image/png +Content-ID: +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="C:\TEMP\nsmailNM.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAV +AAAaAAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACH +AAB9AAB0AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABb +AAAuAAAIAABMAAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACa +AAC7JCTRYWHfhITmf3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5Pl +rKzpmZntZWXvJSXXAADBAACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzr +pqbtiorvUVHvFBTRAADDAAC2AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjv +V1fvJibhAADOAAC3AACnAACVAABHAAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQ +AADJAAC1AACXAACEAABsAABPAAASAAACAABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAAT +AAAkAABYAADIAADTAADNAACzAACDAABuAAAeAAB+AADAAACkAACNAAB/AABpAABQAAAwAACR +AACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACsAACvAACtAACmAACJAAB6AABrAABaAAA+ +AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABVAACOAACKAAA4AAAQAAA/AAByAACA +AABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAMAAAdAAANAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8 +LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAII +SURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iKiUtI8koJ +Scsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ29ja +2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2Yn +OAj+d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/ +uXLzVJ2qm6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW +0g0bN63crGqVtWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36Kw +bNmRo7O3zpHkPSZwHBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8 +YVOlI+CJ4/9/joOyYed5QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms +1y9evXid7QZacgOxmSxktNzdtSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21t +ZW50AGNsaXAyZ2lmIHYuMC42IGJ5IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== +--------------8E6A06810565BCAB5E1F7D97-- + +--------------B7133A01A6B323BF00DBC9A7 +Content-Type: image/png; + name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="greenball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAA +IQAACAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAh +rQBCvRhjxjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBC +zhDO55Te563G55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY +1gAx5wBS7yFr7zlK7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp +1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAFt +SURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy+N9ery1b +Ve9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh0fHJ +aTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJV +G9EaEjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyex +I0ZxBP3afE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWr +YUO23hMANUKRRl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0 +RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5C +YII= +--------------B7133A01A6B323BF00DBC9A7 +Content-Type: image/png; + name="blueball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgA +AAAACCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQ +MZQAGFIQMYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYY +QsYQMaUAACHO5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9K +e+8YOaUYSsaMvee15++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB +Mg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAGI +SURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbsscebL5xznTsh +5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18PyqqW +Uw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/M +jRxmT6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1C +SYoOiMOSGwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIom +H3NCKX0lnI+B1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0N +xW62p+lT+Yi747sD/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBi +eSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= +--------------B7133A01A6B323BF00DBC9A7-- + diff --git a/sources/testing/samples/messages/m1007.txt b/sources/testing/samples/messages/m1007.txt new file mode 100644 index 0000000..f30e8dc --- /dev/null +++ b/sources/testing/samples/messages/m1007.txt @@ -0,0 +1,17 @@ +Message-ID: <3923625B.59F0C3AC@example.com> +Date: Wed, 17 May 2000 23:24:11 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Joe Blow +Subject: Test message from Netscape Communicator 4.7 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +The Hare and the Tortoise + +A HARE one day ridiculed the short feet and slow pace of the Tortoise, who replied, laughing: "Though you be swift as the wind, I will beat you in a race." The Hare, believing her assertion to be simply impossible, assented to the proposal; and they agreed that the Fox should choose the course and fix the goal. On the day appointed for the race the two started together. The Tortoise never for a moment stopped, but went on with a slow but steady pace straight to the end of the course. The Hare, lying down by the wayside, fell fast asleep. At last waking up, and moving as fast as he could, he saw the Tortoise had reached the goal, and was comfortably dozing after her fatigue. + +Slow but steady wins the race. diff --git a/sources/testing/samples/messages/m1008.txt b/sources/testing/samples/messages/m1008.txt new file mode 100644 index 0000000..572f3ce --- /dev/null +++ b/sources/testing/samples/messages/m1008.txt @@ -0,0 +1,27 @@ +Message-ID: <392362D8.B650789F@example.com> +Date: Wed, 17 May 2000 23:26:16 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Joe Blow +Subject: Test message from Netscape Communicator 4.7 +X-Priority: 1 (Highest) +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +The Hare and the Tortoise + +A HARE one day ridiculed the short feet and slow pace of the Tortoise, +who replied, laughing: "Though you be swift as the wind, I will beat +you in a race." The Hare, believing her assertion to be simply +impossible, assented to the proposal; and they agreed that the Fox +should choose the course and fix the goal. On the day appointed for the +race the two started together. The Tortoise never for a moment stopped, +but went on with a slow but steady pace straight to the end of the +course. The Hare, lying down by the wayside, fell fast asleep. At last +waking up, and moving as fast as he could, he saw the Tortoise had +reached the goal, and was comfortably dozing after her fatigue. + +Slow but steady wins the race. diff --git a/sources/testing/samples/messages/m1009.txt b/sources/testing/samples/messages/m1009.txt new file mode 100644 index 0000000..2b0db85 --- /dev/null +++ b/sources/testing/samples/messages/m1009.txt @@ -0,0 +1,111 @@ +Message-ID: <3923635B.85C58444@example.com> +Date: Wed, 17 May 2000 23:28:27 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Joe Blow +Subject: Test message from Netscape Communicator 4.7 +Content-Type: multipart/mixed; + boundary="------------0BBC657DDC74A0B8454627FD" + +This is a multi-part message in MIME format. +--------------0BBC657DDC74A0B8454627FD +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +The Hare and the Tortoise + +A HARE one day ridiculed the short feet and slow pace of the Tortoise, +who replied, laughing: "Though you be swift as the wind, I will beat +you in a race." The Hare, believing her assertion to be simply +impossible, assented to the proposal; and they agreed that the Fox +should choose the course and fix the goal. On the day appointed for the +race the two started together. The Tortoise never for a moment stopped, +but went on with a slow but steady pace straight to the end of the +course. The Hare, lying down by the wayside, fell fast asleep. At last +waking up, and moving as fast as he could, he saw the Tortoise had +reached the goal, and was comfortably dozing after her fatigue. + +Slow but steady wins the race. +--------------0BBC657DDC74A0B8454627FD +Content-Type: image/png; + name="blueball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgA +AAAACCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQ +MZQAGFIQMYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYY +QsYQMaUAACHO5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9K +e+8YOaUYSsaMvee15++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB +Mg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAGI +SURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbsscebL5xznTsh +5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18PyqqW +Uw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/M +jRxmT6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1C +SYoOiMOSGwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIom +H3NCKX0lnI+B1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0N +xW62p+lT+Yi747sD/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBi +eSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= +--------------0BBC657DDC74A0B8454627FD +Content-Type: image/png; + name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="greenball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAA +IQAACAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAh +rQBCvRhjxjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBC +zhDO55Te563G55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY +1gAx5wBS7yFr7zlK7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp +1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAFt +SURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy+N9ery1b +Ve9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh0fHJ +aTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJV +G9EaEjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyex +I0ZxBP3afE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWr +YUO23hMANUKRRl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0 +RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5C +YII= +--------------0BBC657DDC74A0B8454627FD +Content-Type: text/plain; charset=us-ascii; + name="hareandtoroise.txt" +Content-Transfer-Encoding: 7bit +Content-Disposition: inline; + filename="hareandtoroise.txt" + + +The Hare and the Tortoise + +A HARE one day ridiculed the short feet and slow pace of the Tortoise, who replied, laughing: "Though you be swift as the wind, I will beat you in a race." The Hare, believing her assertion to be simply impossible, assented to the proposal; and they agreed that the Fox should choose the course and fix the goal. On the day appointed for the race the two started together. The Tortoise never for a moment stopped, but went on with a slow but steady pace straight to the end of the course. The Hare, lying down by the wayside, fell fast asleep. At last waking up, and moving as fast as he could, he saw the Tortoise had reached the goal, and was comfortably dozing after her fatigue. + +Slow but steady wins the race. + +--------------0BBC657DDC74A0B8454627FD-- + diff --git a/sources/testing/samples/messages/m1010.txt b/sources/testing/samples/messages/m1010.txt new file mode 100644 index 0000000..f84c55d --- /dev/null +++ b/sources/testing/samples/messages/m1010.txt @@ -0,0 +1,33 @@ +Message-ID: <392363E1.2AC47AB3@example.com> +Date: Wed, 17 May 2000 23:30:41 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Joe Blow +Subject: Test message from Netscape Communicator 4.7 +X-Priority: 5 (Lowest) +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach = +ein +Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, +der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als de= +r +Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben." = + + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs +schnellste untertauchten. = + + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ei= +n +wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere, +welche also wohl noch ungl=FCcklicher sein m=FCssen als wir." diff --git a/sources/testing/samples/messages/m1011.txt b/sources/testing/samples/messages/m1011.txt new file mode 100644 index 0000000..b1d2a62 --- /dev/null +++ b/sources/testing/samples/messages/m1011.txt @@ -0,0 +1,26 @@ +Message-ID: <39236497.12FA2A89@example.com> +Date: Wed, 17 May 2000 23:33:43 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: "Heinz M+APw-ller" +Subject: Die Hasen und die Fr+APY-sche +Content-Type: text/plain; charset=UTF-7 +Content-Transfer-Encoding: 7bit + +Die Hasen und die Fr+XKM-he + +Die Hasen klagten einst +uGU-r ihre mi+B6w-iche Lage; "wir leben", sprach ein +Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, +der Adler, ja fast aller Raubtiere! Unsere stete Angst ist +PGc-er als der +Tod selbst. Auf, la+B7Q- uns ein f+vCA-allemal sterben." + +In einem nahen Teich wollten sie sich nun ers+PSY-en; sie eilten ihm zu; +allein das au+B6U-rordentliche Get+XKU- und ihre wunderbare Gestalt +erschreckte eine Menge Fr+XKM-he, die am Ufer sa+B6U-n, so sehr, da+B2A-sie aufs +schnellste untertauchten. + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers+PSY-en noch ein +wenig aufschieben, denn auch uns f+vGM-hten, wie ihr seht, einige Tiere, +welche also wohl noch ungl+uKs-licher sein m+vLM-en als wir." diff --git a/sources/testing/samples/messages/m1012.txt b/sources/testing/samples/messages/m1012.txt new file mode 100644 index 0000000..cd04d23 --- /dev/null +++ b/sources/testing/samples/messages/m1012.txt @@ -0,0 +1,28 @@ +Message-ID: <39236653.CAA8A090@example.com> +Date: Wed, 17 May 2000 23:41:07 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: =?iso-8859-1?Q?J=FCrgen=20Schm=FCrgen?= +Subject: Die Hasen und die =?iso-8859-1?Q?Fr=F6sche?= +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach = +ein Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hun= +de, der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger al= +s der Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben." = + + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; = +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt erschr= +eckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie aufs = +schnellste untertauchten. = + + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ei= +n wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere= +, welche also wohl noch ungl=FCcklicher sein m=FCssen als wir." diff --git a/sources/testing/samples/messages/m1013.txt b/sources/testing/samples/messages/m1013.txt new file mode 100644 index 0000000..027f4b8 --- /dev/null +++ b/sources/testing/samples/messages/m1013.txt @@ -0,0 +1,51 @@ +Message-ID: <392366A7.21929EA7@example.com> +Date: Wed, 17 May 2000 23:42:31 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Joe Blow +Subject: Test message from Netscape Communicator 4.7 +Content-Type: multipart/mixed; + boundary="------------E7889DDF0F75D34163841C59" + +This is a multi-part message in MIME format. +--------------E7889DDF0F75D34163841C59 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +--------------E7889DDF0F75D34163841C59 +Content-Type: image/png; + name="blueball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgA +AAAACCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQ +MZQAGFIQMYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYY +QsYQMaUAACHO5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9K +e+8YOaUYSsaMvee15++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB +Mg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAGI +SURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbsscebL5xznTsh +5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18PyqqW +Uw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/M +jRxmT6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1C +SYoOiMOSGwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIom +H3NCKX0lnI+B1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0N +xW62p+lT+Yi747sD/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBi +eSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= +--------------E7889DDF0F75D34163841C59-- + diff --git a/sources/testing/samples/messages/m1014.txt b/sources/testing/samples/messages/m1014.txt new file mode 100644 index 0000000..4ea7a65 --- /dev/null +++ b/sources/testing/samples/messages/m1014.txt @@ -0,0 +1,155 @@ +Message-ID: <3923670C.67D83A99@example.com> +Date: Wed, 17 May 2000 23:44:12 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Joe Blow +Subject: Test message from Netscape Communicator 4.7 +Content-Type: multipart/mixed; + boundary="------------177483472E7788F4AD46AB1A" + +This is a multi-part message in MIME format. +--------------177483472E7788F4AD46AB1A +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +The Hare and the Tortoise + +A HARE one day ridiculed the short feet and slow pace of the Tortoise, +who replied, laughing: "Though you be swift as the wind, I will beat +you in a race." The Hare, believing her assertion to be simply +impossible, assented to the proposal; and they agreed that the Fox +should choose the course and fix the goal. On the day appointed for the +race the two started together. The Tortoise never for a moment stopped, +but went on with a slow but steady pace straight to the end of the +course. The Hare, lying down by the wayside, fell fast asleep. At last +waking up, and moving as fast as he could, he saw the Tortoise had +reached the goal, and was comfortably dozing after her fatigue. + +Slow but steady wins the race. +--------------177483472E7788F4AD46AB1A +Content-Type: image/png; + name="redball.png" +Content-Transfer-Encoding: x-uuencode +Content-Disposition: inline; + filename="redball.png" + +begin 644 redball.png +MB5!.1PT*&@H -24A$4@ !L ;" , "Z"@1G # %!,5$7___\ +M ! + 5 : 7 1 * # < R !$ !- !( ] C +M 6 F !A ![ "& "' !] !T !@ Y 4 & G !+ !O +M "0 "E "[ "_ "K "A ", !S !; N ( !, !W "9 "T +M&1G*.#C5/3W;*2G=!06X "H !Y Q 8 $ !% ": "[)"3186'? +MA(3F?W_F5E;J'Q_2 "Y "C != !" H ) !G "Z#P_05%3>DY/E +MK*SIF9GM967O)277 #! "Q "< !M !3 V ; % !* "! #+ +M("#=9V?HG)SKIJ;MBHKO45'O%!31 ## "V !X !> ! B !7 "2 +M #" #:&QOH557L>'CO>'CO5U?O)B;A #. "W "G "5 !' K / +M "= #% #A!07K*"CO/#SO-37O&QOC #0 #) "U "7 "$ !L !/ +M 2 " !B #I #O @+G #8 #+ "Z "P !P 3 D !8 #( +M #3 #- "S "# !N > !^ # "D "- !_ !I !0 P "1 +M "I "\ "J "; !E !) J . T "L "O "M "F ") !Z +M !K !: ^ I !J "" "? "> "6 "/ !\ 9 ' !5 ". +M "* X 0 _ !R " !< W L !F !# !6 @ S \ +M Z ? , = - +M +M #\ +M+ME% 7123E, 0.;89@ !9T15AT4V]F='=AOF@ ((241!5'B<8V" @_\0P( !F)A96%G9V#DX,:4YN+AY>'GY^+D% +M!#E0)?\S"_$(BXB*B4M(\DH)2?D%A47%R26E9E$=Y1258[C^3D&15=4UM77U#8U.SJWW[10_(IZTQ.W@($E7_+;BD#F\3WZ*P;-F1H[.WSI'D/29P'!J+__\? +MESUQV..DR*)3B[:J;SNM?49(V@(N%\%VEK_#Y=PV^?9=PIE\85.E(^")X_]_ +MCH.R8>=Y0S,OA&JGFV5T6B"EF___#TE?7'M)ZO+E*]P[KQ[O0DY20);%M2FL +MUR]>O7B=[09:<@.QF2QDM-S=M2PP4^)_5(":"/%(84W7 )A(.MC?YL?R +M)71%6'1#;VUM96YT &-L:7 R9VEF('8N,"XV(&)Y(%EV97,@4&EG=65T-G,[ +-O !)14Y$KD)@@@ +end + +--------------177483472E7788F4AD46AB1A +Content-Type: image/png; + name="greenball.png" +Content-Transfer-Encoding: x-uuencode +Content-Disposition: inline; + filename="greenball.png" + +begin 644 greenball.png +MB5!.1PT*&@H -24A$4@ !L ;" , "Z"@1G # %!,5$7___\ +M $ & (0 " ,0 0@ 4@ 6@ 2@ (8P (

P 0C (:P +M.0 8P 0E 0G AI0 0I0 AK0!"O1ACQC%CQCE2QB$IS@ 8O0 0K0 8K0 A +MO0"4UF.MUGN$UE)*WA@AU@ 8Q@ 8M0 *0!"SA#.YY3>YZW&YY24YV-2YR$A +MW@ 8S@!2WB&KRU;5>]/60 ]SX,0^'\8HC[& 29]Y('?:!&F+.2< +MAR(@R.NVVF@02C4<#N-8#:LMN[^]G#XU-FDB)VC_PW8#K614(E9K+_6\Z7)DR)5 +M&]$:$C(QV7R9F9)&BH>4:+^V 8\38\HF1ZGD+*AN[TSC4*5)7LIWJN2V#52) +M6-AWSS)*"R>Q(T9Q!/W:?$T$ERJ.E9)RP\KW!$V2%B)"RQ XWX2"D@ZY1A=> +M0!EC0M"@W]+6K ]T1##&U$6K84.VWA, -4*11EZU$YV]<1^^#V&Y+*TTV=K_ +MJK>-T@@L+ "5T15AT0V]M;65N= !C;&EP,F=I +F9B!V+C N-B!B>2!9=F5S(%!I9W5E=#9S.[P 245.1*Y"8(( +end + +--------------177483472E7788F4AD46AB1A +Content-Type: image/png; + name="blueball.png" +Content-Transfer-Encoding: x-uuencode +Content-Disposition: inline; + filename="blueball.png" + +begin 644 blueball.png +MB5!.1PT*&@H -24A$4@ !L ;" , "Z"@1G # %!,5$7___\ +M @ ! !@ ""D $$( $$H "#$ $%(((7,(*7L(*80((6L &%H "#D( +M(6,0.9P80J480JT80K40.:40,90 &%(0,8PI4K4Y8\8Y8\XI6LX82LX80LX8 +M0KU":\9[G-:\ZMSMZESN]:C.\0,9Q:A-:MUN?.[^^,O>]*>^\8.:482L:, +MO>>UY^^4O>\ "E:C.=SI>]KG.\(*8PQ8^\I6N\((7L +M +M +M +M +M +M +M +M +M +M +M +M #! +M,@U5 7123E, 0.;89@ !9T15AT4V]F='=AOF@ &(241!5'B<==)M5YLP& ;@$DZ )A!2DX;,"4RG!2+:JHO;K&_; +M?/G_ORA[TE;LL<>;+YQSG3LAY!F-A@0H1"APHYT$.(HA48+#CXS'A*80DK$H +M0=OHT(*,54I*9+0#>J23(LO^U\/RJJ64PV(!L1,[WT[//I^?#)K6MGI +M+#)O>P8QG>P?GO:G9^?SIFP7BC"\*;J$:W%QV??]U8]Y4]7"4I:#>0PB,I'7 +M/_O^U^^;65/60H&%:PMCNKR]OKN_?_"U5B[>S3T66I0'?_[^N_&[U:+3A*_, +MC1QF3Z*=S9]?&D]R:M,L3@9+EW75@%1 "ZLIC_UW>GLLJ)5M65:W-=!2:<+\ +M^59F8J*F==U"28H.B,.2&PMSEJJE%%(N.JOT:U9$9K4D6("++%6VZZRU*O6$ +MUS5_B# 'U$HIG5+"BR@9"(HF'W-"*7TEG(^!UBNNS"&3QP7CG#.X=[-I;7XV +M8))'D!PF FV3QP 9@XT)W^7M;OUK@. 9P&T-Q6ZVI^E3^8B[X[L#_P$45,S8 +MF6D.OP "5T15AT0V]M;65N= !C;&EP,F=I9B!V+C N-B!B>2!9=F5S(%!I +49W5E=#9S.[P 245.1*Y"8(( +end + +--------------177483472E7788F4AD46AB1A-- + diff --git a/sources/testing/samples/messages/m1015.txt b/sources/testing/samples/messages/m1015.txt new file mode 100644 index 0000000..ccc7d8d --- /dev/null +++ b/sources/testing/samples/messages/m1015.txt @@ -0,0 +1,60 @@ +Message-ID: <392367BC.3D075C95@example.com> +Date: Wed, 17 May 2000 23:47:08 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Joe Blow +Subject: Test message from Netscape Communicator 4.7 +Content-Type: multipart/mixed; + boundary="------------CA611088711119FBDB3473B4" + +This is a multi-part message in MIME format. +--------------CA611088711119FBDB3473B4 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +The Hare and the Tortoise + +A HARE one day ridiculed the short feet and slow pace of the Tortoise, +who replied, laughing: "Though you be swift as the wind, I will beat +you in a race." The Hare, believing her assertion to be simply +impossible, assented to the proposal; and they agreed that the Fox +should choose the course and fix the goal. On the day appointed for the +race the two started together. The Tortoise never for a moment stopped, +but went on with a slow but steady pace straight to the end of the +course. The Hare, lying down by the wayside, fell fast asleep. At last +waking up, and moving as fast as he could, he saw the Tortoise had +reached the goal, and was comfortably dozing after her fatigue. + +Slow but steady wins the race. +--------------CA611088711119FBDB3473B4 +Content-Type: text/plain; charset=iso-8859-1; + name="=?iso-8859-1?Q?HasenundFr=F6sche=2Etxt?=" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline; + filename="=?iso-8859-1?Q?HasenundFr=F6sche=2Etxt?=" + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach = +ein Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hun= +de, der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger al= +s der Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben." = + + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; = +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt erschr= +eckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie aufs = +schnellste untertauchten. = + + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ei= +n wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere= +, welche also wohl noch ungl=FCcklicher sein m=FCssen als wir." = + + + +--------------CA611088711119FBDB3473B4-- + diff --git a/sources/testing/samples/messages/m1016.txt b/sources/testing/samples/messages/m1016.txt new file mode 100644 index 0000000..b4667c2 --- /dev/null +++ b/sources/testing/samples/messages/m1016.txt @@ -0,0 +1,58 @@ +Message-ID: <3923686C.99FB5E36@example.com> +Date: Wed, 17 May 2000 23:50:04 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Joe Blow +Subject: Test message from Netscape Communicator 4.7 +Content-Type: multipart/mixed; + boundary="------------77060D866A66DC8D0921E051" + +This is a multi-part message in MIME format. +--------------77060D866A66DC8D0921E051 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +The Hare and the Tortoise + +A HARE one day ridiculed the short feet and slow pace of the Tortoise, +who replied, laughing: "Though you be swift as the wind, I will beat +you in a race." The Hare, believing her assertion to be simply +impossible, assented to the proposal; and they agreed that the Fox +should choose the course and fix the goal. On the day appointed for the +race the two started together. The Tortoise never for a moment stopped, +but went on with a slow but steady pace straight to the end of the +course. The Hare, lying down by the wayside, fell fast asleep. At last +waking up, and moving as fast as he could, he saw the Tortoise had +reached the goal, and was comfortably dozing after her fatigue. + +Slow but steady wins the race. +--------------77060D866A66DC8D0921E051 +Content-Type: text/plain; charset=us-ascii; + name="farmerandstork.txt" +Content-Transfer-Encoding: 7bit +Content-Disposition: inline; + filename="farmerandstork.txt" + + +The Farmer and the Stork + +A FARMER placed nets on his newly-sown plowlands and caught a +number of Cranes, which came to pick up his seed. With them he +trapped a Stork that had fractured his leg in the net and was +earnestly beseeching the Farmer to spare his life. "Pray save +me, Master," he said, "and let me go free this once. My broken +limb should excite your pity. Besides, I am no Crane, I am a +Stork, a bird of excellent character; and see how I love and +slave for my father and mother. Look too, at my feathers-- +they are not the least like those of a Crane." The Farmer +laughed aloud and said, "It may be all as you say, I only know +this: I have taken you with these robbers, the Cranes, and you +must die in their company." + +Birds of a feather flock together. + +--------------77060D866A66DC8D0921E051-- + diff --git a/sources/testing/samples/messages/m2001.txt b/sources/testing/samples/messages/m2001.txt new file mode 100644 index 0000000..d21c893 --- /dev/null +++ b/sources/testing/samples/messages/m2001.txt @@ -0,0 +1,29 @@ +Message-Id: <4.2.0.58.20000519001217.00a85b60@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +Date: Fri, 19 May 2000 00:17:39 -0400 +To: =?iso-8859-1?Q?J=FCrgen?= =?iso-8859-1?Q?_Schm=FCrgen?= + +From: Doug Sauder +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Fr=F6sche?= +Mime-Version: 1.0 +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach ein= + Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde,= + der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der= + Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu;= + allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt= + erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs schnellste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ein= + wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere,= + welche also wohl noch ungl=FCcklicher sein m=FCssen als wir."=20 + + diff --git a/sources/testing/samples/messages/m2002.txt b/sources/testing/samples/messages/m2002.txt new file mode 100644 index 0000000..16a779e --- /dev/null +++ b/sources/testing/samples/messages/m2002.txt @@ -0,0 +1,63 @@ +Message-Id: <4.2.0.58.20000519002557.00a88870@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +X-Priority: 2 (High) +Date: Fri, 19 May 2000 00:29:55 -0400 +To: Heinz =?iso-8859-1?Q?M=FCller?= +From: Doug Sauder +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Fr=F6sche?= +Mime-Version: 1.0 +Content-Type: multipart/alternative; + boundary="=====================_714967308==_.ALT" + +--=====================_714967308==_.ALT +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach ein= + Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde,= + der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der= + Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu;= + allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt= + erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs schnellste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ein= + wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere,= + welche also wohl noch ungl=FCcklicher sein m=FCssen als wir."=20 + + +--=====================_714967308==_.ALT +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + +Die Hasen und = +die +Fr=F6sche
+
+
Die Hasen klagten einst =FCber +ihre mi=DFliche Lage; "wir leben", sprach ein Redner, "in +steter Furcht vor Menschen und Tieren, eine Beute der Hunde, der Adler, +ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der Tod selbst. +Auf, la=DFt uns ein f=FCr allemal sterben."
+
+In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie au= +fs +schnellste untertauchten.
+
+"Halt", rief nun eben dieser Sprecher, "wir wollen das +Ers=E4ufen noch ein wenig aufschieben, denn auch uns f=FCrchten, wie ihr +seht, einige Tiere, welche also wohl noch ungl=FCcklicher sein m=FCssen als +wir."
+
+
+ +--=====================_714967308==_.ALT-- + diff --git a/sources/testing/samples/messages/m2003.txt b/sources/testing/samples/messages/m2003.txt new file mode 100644 index 0000000..dc0e99d --- /dev/null +++ b/sources/testing/samples/messages/m2003.txt @@ -0,0 +1,36 @@ +Message-Id: <4.2.0.58.20000519003052.00a89c40@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +X-Priority: 2 (High) +Date: Fri, 19 May 2000 00:31:00 -0400 +To: Heinz =?iso-8859-1?Q?M=FCller?= +From: Doug Sauder +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Fr=F6sche?= +Mime-Version: 1.0 +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + +Die Hasen und = +die +Fr=F6sche
+
+
Die Hasen klagten einst =FCber +ihre mi=DFliche Lage; "wir leben", sprach ein Redner, "in +steter Furcht vor Menschen und Tieren, eine Beute der Hunde, der Adler, +ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der Tod selbst. +Auf, la=DFt uns ein f=FCr allemal sterben."
+
+In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie au= +fs +schnellste untertauchten.
+
+"Halt", rief nun eben dieser Sprecher, "wir wollen das +Ers=E4ufen noch ein wenig aufschieben, denn auch uns f=FCrchten, wie ihr +seht, einige Tiere, welche also wohl noch ungl=FCcklicher sein m=FCssen als +wir."
+
+
+ diff --git a/sources/testing/samples/messages/m2004.txt b/sources/testing/samples/messages/m2004.txt new file mode 100644 index 0000000..a73ab64 --- /dev/null +++ b/sources/testing/samples/messages/m2004.txt @@ -0,0 +1,138 @@ +Message-Id: <4.2.0.58.20000519003143.00a8d550@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +X-Priority: 2 (High) +Date: Fri, 19 May 2000 00:33:01 -0400 +To: Heinz =?iso-8859-1?Q?Müller?= +From: Doug Sauder +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Frösche?= +Mime-Version: 1.0 +Content-Type: multipart/related; + type="multipart/alternative"; + boundary="=====================_715157131==_.REL" + +--=====================_715157131==_.REL +Content-Type: multipart/alternative; + boundary="=====================_715157141==_.ALT" + +--=====================_715157141==_.ALT +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +blueball.png + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach ein= + Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde,= + der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der= + Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu;= + allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt= + erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs schnellste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ein= + wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere,= + welche also wohl noch ungl=FCcklicher sein m=FCssen als wir."=20 + +redball.png +--=====================_715157141==_.ALT +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + +3D"blueball.png"
+
+Die Hasen und = +die +Fr=F6sche
+
+
Die Hasen klagten einst =FCber +ihre mi=DFliche Lage; "wir leben", sprach ein Redner, "in +steter Furcht vor Menschen und Tieren, eine Beute der Hunde, der Adler, +ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der Tod selbst. +Auf, la=DFt uns ein f=FCr allemal sterben."
+
+In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie au= +fs +schnellste untertauchten.
+
+"Halt", rief nun eben dieser Sprecher, "wir wollen das +Ers=E4ufen noch ein wenig aufschieben, denn auch uns f=FCrchten, wie ihr +seht, einige Tiere, welche also wohl noch ungl=FCcklicher sein m=FCssen als +wir."
+
+3D"redball.png"
+ +--=====================_715157141==_.ALT-- + +--=====================_715157131==_.REL +Content-Type: image/png; name="blueball.png" +Content-ID: <4.2.0.58.20000519003143.00a8d550@pop.example.com.0> +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= +--=====================_715157131==_.REL +Content-Type: image/png; name="redball.png" +Content-ID: <4.2.0.58.20000519003143.00a8d550@pop.example.com.1> +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="redball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa +AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0 +AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM +AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm +f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB +AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2 +AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH +AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC +AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe +AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs +AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV +AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM +AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK +iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ +29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+ +d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q +m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV +tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw +HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5 +QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd +tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5 +IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== +--=====================_715157131==_.REL-- + diff --git a/sources/testing/samples/messages/m2005.txt b/sources/testing/samples/messages/m2005.txt new file mode 100644 index 0000000..4a11dfe --- /dev/null +++ b/sources/testing/samples/messages/m2005.txt @@ -0,0 +1,198 @@ +Message-Id: <4.2.0.58.20000519003556.00a918e0@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +X-Priority: 2 (High) +Date: Fri, 19 May 2000 00:36:58 -0400 +To: Heinz =?iso-8859-1?Q?Müller?= +From: Doug Sauder +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Frösche?= +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=====================_715392540==_" + +--=====================_715392540==_ +Content-Type: multipart/related; + type="multipart/alternative"; + boundary="=====================_715392540==_.REL" + +--=====================_715392540==_.REL +Content-Type: multipart/alternative; + boundary="=====================_715392550==_.ALT" + +--=====================_715392550==_.ALT +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach ein= + Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde,= + der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der= + Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu;= + allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt= + erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs schnellste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ein= + wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere,= + welche also wohl noch ungl=FCcklicher sein m=FCssen als wir."=20 + +2aa3ed95.png2aa3edd1.png +--=====================_715392550==_.ALT +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + +Die Hasen und = +die +Fr=F6sche
+
+
Die Hasen klagten einst =FCber +ihre mi=DFliche Lage; "wir leben", sprach ein Redner, "in +steter Furcht vor Menschen und Tieren, eine Beute der Hunde, der Adler, +ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der Tod selbst. +Auf, la=DFt uns ein f=FCr allemal sterben."
+
+In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie au= +fs +schnellste untertauchten.
+
+"Halt", rief nun eben dieser Sprecher, "wir wollen das +Ers=E4ufen noch ein wenig aufschieben, denn auch uns f=FCrchten, wie ihr +seht, einige Tiere, welche also wohl noch ungl=FCcklicher sein m=FCssen als +wir."
+
+
3D"2aa3ed95.png" + +--=====================_715392550==_.ALT-- + +--=====================_715392540==_.REL +Content-Type: image/png; name="2aa3ed95.png" +Content-ID: <4.2.0.58.20000519003556.00a918e0@pop.example.com.2> +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="2aa3ed95.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= +--=====================_715392540==_.REL +Content-Type: image/png; name="2aa3edd1.png" +Content-ID: <4.2.0.58.20000519003556.00a918e0@pop.example.com.3> +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="2aa3edd1.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa +AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0 +AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM +AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm +f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB +AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2 +AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH +AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC +AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe +AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs +AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV +AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM +AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK +iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ +29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+ +d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q +m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV +tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw +HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5 +QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd +tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5 +IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== +--=====================_715392540==_.REL-- + +--=====================_715392540==_ +Content-Type: image/png; name="blueball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= +--=====================_715392540==_ +Content-Type: image/png; name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="greenball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAAIQAA +CAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAhrQBCvRhj +xjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBCzhDO55Te563G +55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY1gAx5wBS7yFr7zlK +7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAp1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAFtSURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy ++N9ery1bVe9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh +0fHJaTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJVG9Ea +EjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyexI0ZxBP3a +fE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWrYUO23hMANUKR +Rl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0RVh0Q29tbWVudABj +bGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= +--=====================_715392540==_-- + diff --git a/sources/testing/samples/messages/m2006.txt b/sources/testing/samples/messages/m2006.txt new file mode 100644 index 0000000..cc76017 --- /dev/null +++ b/sources/testing/samples/messages/m2006.txt @@ -0,0 +1,109 @@ +Message-Id: <4.2.0.58.20000519003735.00a8d7e0@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +X-Priority: 2 (High) +Date: Fri, 19 May 2000 00:37:39 -0400 +To: Heinz =?iso-8859-1?Q?Müller?= +From: Doug Sauder +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Frösche?= +Mime-Version: 1.0 +Content-Type: multipart/related; + type="text/html"; + boundary="=====================_715429112==_.REL" + +--=====================_715429112==_.REL +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + +3D"2aa48eb6.png"
+
+Die Hasen und = +die +Fr=F6sche
+
+
Die Hasen klagten einst =FCber +ihre mi=DFliche Lage; "wir leben", sprach ein Redner, "in +steter Furcht vor Menschen und Tieren, eine Beute der Hunde, der Adler, +ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der Tod selbst. +Auf, la=DFt uns ein f=FCr allemal sterben."
+
+In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie au= +fs +schnellste untertauchten.
+
+"Halt", rief nun eben dieser Sprecher, "wir wollen das +Ers=E4ufen noch ein wenig aufschieben, denn auch uns f=FCrchten, wie ihr +seht, einige Tiere, welche also wohl noch ungl=FCcklicher sein m=FCssen als +wir."
+
+3D"2aa48ec0.png"
+ +--=====================_715429112==_.REL +Content-Type: image/png; name="2aa48eb6.png" +Content-ID: <4.2.0.58.20000519003735.00a8d7e0@pop.example.com.2> +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="2aa48eb6.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= +--=====================_715429112==_.REL +Content-Type: image/png; name="2aa48ec0.png" +Content-ID: <4.2.0.58.20000519003735.00a8d7e0@pop.example.com.3> +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="2aa48ec0.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa +AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0 +AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM +AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm +f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB +AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2 +AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH +AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC +AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe +AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs +AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV +AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM +AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK +iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ +29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+ +d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q +m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV +tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw +HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5 +QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd +tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5 +IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== +--=====================_715429112==_.REL-- + diff --git a/sources/testing/samples/messages/m2007.txt b/sources/testing/samples/messages/m2007.txt new file mode 100644 index 0000000..dfb9f4a --- /dev/null +++ b/sources/testing/samples/messages/m2007.txt @@ -0,0 +1,171 @@ +Message-Id: <4.2.0.58.20000519003809.00a85140@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +X-Priority: 2 (High) +Date: Fri, 19 May 2000 00:38:13 -0400 +To: Heinz =?iso-8859-1?Q?Müller?= +From: Doug Sauder +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Frösche?= +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=====================_715462801==_" + +--=====================_715462801==_ +Content-Type: multipart/related; + type="text/html"; + boundary="=====================_715462801==_.REL" + +--=====================_715462801==_.REL +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + +Die Hasen und = +die +Fr=F6sche
+
+
Die Hasen klagten einst =FCber +ihre mi=DFliche Lage; "wir leben", sprach ein Redner, "in +steter Furcht vor Menschen und Tieren, eine Beute der Hunde, der Adler, +ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der Tod selbst. +Auf, la=DFt uns ein f=FCr allemal sterben."
+
+In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie au= +fs +schnellste untertauchten.
+
+"Halt", rief nun eben dieser Sprecher, "wir wollen das +Ers=E4ufen noch ein wenig aufschieben, denn auch uns f=FCrchten, wie ihr +seht, einige Tiere, welche also wohl noch ungl=FCcklicher sein m=FCssen als +wir."
+
+
3D"2aa51367.png" + +--=====================_715462801==_.REL +Content-Type: image/png; name="2aa51367.png" +Content-ID: <4.2.0.58.20000519003809.00a85140@pop.example.com.2> +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="2aa51367.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= +--=====================_715462801==_.REL +Content-Type: image/png; name="2aa51371.png" +Content-ID: <4.2.0.58.20000519003809.00a85140@pop.example.com.3> +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="2aa51371.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa +AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0 +AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM +AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm +f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB +AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2 +AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH +AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC +AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe +AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs +AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV +AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM +AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK +iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ +29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+ +d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q +m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV +tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw +HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5 +QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd +tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5 +IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== +--=====================_715462801==_.REL-- + +--=====================_715462801==_ +Content-Type: image/png; name="blueball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= +--=====================_715462801==_ +Content-Type: image/png; name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="greenball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAAIQAA +CAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAhrQBCvRhj +xjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBCzhDO55Te563G +55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY1gAx5wBS7yFr7zlK +7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAp1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAFtSURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy ++N9ery1bVe9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh +0fHJaTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJVG9Ea +EjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyexI0ZxBP3a +fE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWrYUO23hMANUKR +Rl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0RVh0Q29tbWVudABj +bGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= +--=====================_715462801==_-- + diff --git a/sources/testing/samples/messages/m2008.txt b/sources/testing/samples/messages/m2008.txt new file mode 100644 index 0000000..bd1df32 --- /dev/null +++ b/sources/testing/samples/messages/m2008.txt @@ -0,0 +1,161 @@ +Message-Id: <4.2.0.58.20000519003903.00a859b0@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +X-Priority: 2 (High) +Date: Fri, 19 May 2000 00:39:05 -0400 +To: Heinz =?iso-8859-1?Q?Müller?= +From: Doug Sauder +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Frösche?= +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=====================_715515186==_" + +--=====================_715515186==_ +Content-Type: multipart/related; + type="text/plain"; + boundary="=====================_715515186==_.REL" + +--=====================_715515186==_.REL +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach ein= + Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde,= + der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der= + Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu;= + allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt= + erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs schnellste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ein= + wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere,= + welche also wohl noch ungl=FCcklicher sein m=FCssen als wir."=20 + +2aa5e03a.png2aa5e044.png +--=====================_715515186==_.REL +Content-Type: image/png; name="2aa5e03a.png" +Content-ID: <4.2.0.58.20000519003903.00a859b0@pop.example.com.2> +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="2aa5e03a.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= +--=====================_715515186==_.REL +Content-Type: image/png; name="2aa5e044.png" +Content-ID: <4.2.0.58.20000519003903.00a859b0@pop.example.com.3> +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="2aa5e044.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa +AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0 +AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM +AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm +f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB +AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2 +AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH +AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC +AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe +AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs +AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV +AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM +AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK +iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ +29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+ +d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q +m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV +tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw +HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5 +QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd +tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5 +IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== +--=====================_715515186==_.REL-- + +--=====================_715515186==_ +Content-Type: image/png; name="blueball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= +--=====================_715515186==_ +Content-Type: image/png; name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="greenball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAAIQAA +CAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAhrQBCvRhj +xjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBCzhDO55Te563G +55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY1gAx5wBS7yFr7zlK +7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAp1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAFtSURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy ++N9ery1bVe9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh +0fHJaTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJVG9Ea +EjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyexI0ZxBP3a +fE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWrYUO23hMANUKR +Rl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0RVh0Q29tbWVudABj +bGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= +--=====================_715515186==_-- + diff --git a/sources/testing/samples/messages/m2009.txt b/sources/testing/samples/messages/m2009.txt new file mode 100644 index 0000000..7a79746 --- /dev/null +++ b/sources/testing/samples/messages/m2009.txt @@ -0,0 +1,109 @@ +Message-Id: <4.2.0.58.20000519003934.00a866f0@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +X-Priority: 2 (High) +Date: Fri, 19 May 2000 00:39:36 -0400 +To: Heinz =?iso-8859-1?Q?Müller?= +From: Doug Sauder +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Frösche?= +Mime-Version: 1.0 +Content-Type: multipart/related; + type="text/html"; + boundary="=====================_715546120==_.REL" + +--=====================_715546120==_.REL +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + +3D"2aa658b6.png"
+
+Die Hasen und = +die +Fr=F6sche
+
+
Die Hasen klagten einst =FCber +ihre mi=DFliche Lage; "wir leben", sprach ein Redner, "in +steter Furcht vor Menschen und Tieren, eine Beute der Hunde, der Adler, +ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der Tod selbst. +Auf, la=DFt uns ein f=FCr allemal sterben."
+
+In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie au= +fs +schnellste untertauchten.
+
+"Halt", rief nun eben dieser Sprecher, "wir wollen das +Ers=E4ufen noch ein wenig aufschieben, denn auch uns f=FCrchten, wie ihr +seht, einige Tiere, welche also wohl noch ungl=FCcklicher sein m=FCssen als +wir."
+
+3D"2aa658c0.png"
+ +--=====================_715546120==_.REL +Content-Type: image/png; name="2aa658b6.png" +Content-ID: <4.2.0.58.20000519003934.00a866f0@pop.example.com.2> +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="2aa658b6.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= +--=====================_715546120==_.REL +Content-Type: image/png; name="2aa658c0.png" +Content-ID: <4.2.0.58.20000519003934.00a866f0@pop.example.com.3> +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="2aa658c0.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa +AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0 +AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM +AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm +f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB +AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2 +AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH +AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC +AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe +AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs +AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV +AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM +AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK +iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ +29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+ +d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q +m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV +tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw +HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5 +QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd +tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5 +IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== +--=====================_715546120==_.REL-- + diff --git a/sources/testing/samples/messages/m2010.txt b/sources/testing/samples/messages/m2010.txt new file mode 100644 index 0000000..e33eebc --- /dev/null +++ b/sources/testing/samples/messages/m2010.txt @@ -0,0 +1,102 @@ +Message-Id: <4.2.0.58.20000519004755.00a86100@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +Date: Fri, 19 May 2000 00:50:08 -0400 +To: Jürgen Schmürgen +From: Doug Sauder +Subject: The Hare and the Tortoise +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=====================_716177458==_" + +--=====================_716177458==_ +Content-Type: text/plain; charset="us-ascii" + + +The Hare and the Tortoise + +A HARE one day ridiculed the short feet and slow pace of the Tortoise, who replied, laughing: "Though you be swift as the wind, I will beat you in a race." The Hare, believing her assertion to be simply impossible, assented to the proposal; and they agreed that the Fox should choose the course and fix the goal. On the day appointed for the race the two started together. The Tortoise never for a moment stopped, but went on with a slow but steady pace straight to the end of the course. The Hare, lying down by the wayside, fell fast asleep. At last waking up, and moving as fast as he could, he saw the Tortoise had reached the goal, and was comfortably dozing after her fatigue. + +Slow but steady wins the race. + +--=====================_716177458==_ +Content-Type: image/png; name="greenball.png" +Content-Transfer-Encoding: x-uuencode +Content-Disposition: attachment; filename="greenball.png" + + +begin 600 greenball.png +MB5!.1PT*&@H````-24A$4@```!L````;"`,```"Z"@1G```#`%!,5$7___\` +M````$```&```(0``"```,0``0@``4@``6@``2@`(8P`(P`0C``(:P`` +M.0``8P`0E``0G``AI0`0I0`AK0!"O1ACQC%CQCE2QB$IS@`8O0`0K0`8K0`A +MO0"4UF.MUGN$UE)*WA@AU@`8Q@`8M0``*0!"SA#.YY3>YZW&YY24YV-2YR$A +MW@`8S@!2WB&KRU;5>]/60`]SX,0^'\8HC[&`29]Y('?:!&F+.2< +MAR(@R.NVVF@02C4<#N-8#:LMN[^]G#XU-FDB)VC_PW8#K614(E9K+_6\Z7)DR)5 +M&]$:$C(QV7R9F9)&BH>4:+^V`8\38\HF1ZGD+*AN[TSC4*5)7LIWJN2V#52) +M6-AWSS)*"R>Q(T9Q!/W:?$T$ERJ.E9)RP\KW!$V2%B)"RQ`XWX2"D@ZY1A=> +M0!EC0M"@W]+6K`]T1##&U$6K84.VWA,`-4*11EZU$YV]<1^^#V&Y+*TTV=K_ +MJK>-T@@L+````"5T15AT0V]M;65N=`!C;&EP,F=I +F9B!V+C`N-B!B>2!9=F5S(%!I9W5E=#9S.[P`````245.1*Y"8((` +` +end + +--=====================_716177458==_ +Content-Type: image/png; name="blueball.png" +Content-Transfer-Encoding: x-uuencode +Content-Disposition: attachment; filename="blueball.png" + + +begin 600 blueball.png +MB5!.1PT*&@H````-24A$4@```!L````;"`,```"Z"@1G```#`%!,5$7___\` +M``@``!```!@`````""D`$$(`$$H`"#$`$%(((7,(*7L(*80((6L`&%H`"#D( +M(6,0.9P80J480JT80K40.:40,90`&%(0,8PI4K4Y8\8Y8\XI6LX82LX80LX8 +M0KU":\9[G-:\ZMSMZESN]:C.\0,9Q:A-:MUN?.[^^,O>]*>^\8.:482L:, +MO>>UY^^4O>\``"E:C.=SI>]KG.\(*8PQ8^\I6N\((7L````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M``````````````````````````````````````````````````````````#! +M,@U5`````7123E,`0.;89@```!9T15AT4V]F='=AOF@```&(241!5'B<==)M5YLP&`;@$DZ`)A!2DX;,"4RG!2+:JHO;K&_; +M?/G_ORA[TE;LL<>;+YQSG3LAY!F-A@0H1"APHYT$.(HA48+#CXS'A*80DK$H +M0=OHT(*,54I*9+0#>J23(LO^U\/RJJ64PV(!L1,[WT[//I^?#)K6MGI +M+#)O>P8QG>P?GO:G9^?SIFP7BC"\*;J$:W%QV??]U8]Y4]7"4I:#>0PB,I'7 +M/_O^U^^;65/60H&%:PMCNKR]OKN_?_"U5B[>S3T66I0'?_[^N_&[U:+3A*_, +MC1QF3Z*=S9]?&D]R:M,L3@9+EW75@%1`"ZLIC_UW>GLLJ)5M65:W-=!2:<+\ +M^59F8J*F==U"28H.B,.2&PMSEJJE%%(N.JOT:U9$9K4D6("++%6VZZRU*O6$ +MUS5_B#`'U$HIG5+"BR@9"(HF'W-"*7TEG(^!UBNNS"&3QP7CG#.X=[-I;7XV +M8))'D!PF`FV3QP`9@XT)W^7M;OUK@.`9P&T-Q6ZVI^E3^8B[X[L#_P$45,S8 +MF6D.OP```"5T15AT0V]M;65N=`!C;&EP,F=I9B!V+C`N-B!B>2!9=F5S(%!I +49W5E=#9S.[P`````245.1*Y"8((` +` +end + +--=====================_716177458==_-- + diff --git a/sources/testing/samples/messages/m2011.txt b/sources/testing/samples/messages/m2011.txt new file mode 100644 index 0000000..dacdc97 --- /dev/null +++ b/sources/testing/samples/messages/m2011.txt @@ -0,0 +1,95 @@ +Message-Id: <4.2.0.58.20000519005150.00a85230@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +Date: Fri, 19 May 2000 00:53:25 -0400 +To: =?iso-8859-1?Q?Jürgen?= =?iso-8859-1?Q?_Schmürgen?= + +From: Doug Sauder +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Frösche?= +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=====================_716373961==_" + +--=====================_716373961==_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach ein= + Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde,= + der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der= + Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu;= + allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt= + erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs schnellste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ein= + wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere,= + welche also wohl noch ungl=FCcklicher sein m=FCssen als wir."=20 + + +--=====================_716373961==_ +Content-Type: application/mac-binhex40; name="blueball.png" +Content-Disposition: attachment; filename="blueball.png" + + +(This file must be converted with BinHex 4.0) +:$'*XG@9LB@aX,R"ZC`"#58j"E@4[F`!!!!!&,3!!!!"'dSP36NF0#KS+!!!!$8P +)4&)!!!!E!!!!'`J$!!!!ZJS%C`!!!`"36&4&rrrr!!!)!!!3!!!B!!!!!!JT!"" +#!""+!!Ja!""5##&c##Pl##Q%##&V!"KD!!Jj##&M%$QF'%+P'%+Y'%+e%$QP%$' +8!"K5%$'-+9+e1@2'1@21+9V1'%V1'%,1'%+p3Q['Hjc@R,hHM,AHBj6R-@[H)9, +H'%V@'%,'%$'P!!!KcZI[jqr[R-E[8S6[+9VR%$QY5R[1VFlHTFl[@Sc[%$'F@S6 +@VGERcZr[M,h[5R[['$QP'%V'M,hRYHI[P,h[!!!T@ScRFkA[Djc[##Q--@2[+9V +[##&l!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`6)093!!!!&d8Nj6!%$Qf'B!!!! +@G%9BG&0[CR4hBA*P!'GTCM*`EQFJ-Li`,M%pAVjS!!!"L%P%394iR(A5E9HE-"J +'i"*1J#B38T1'c!P-T`8LfUU,fka[fhcjrlmSHp*@l,((QbqFFjdl)H3CMBB%+%3 +SF+1G"$L+)9'#`iq-ai5Q%*+a+%(Ek(*1*hY##M&9+5Q5d!hUNNb,,rYI$mUUPP- +0L!E%61pp1cckIR`bDeVCk5`bEhX'-ChX(jlfTfIRmkCX&iS`[#QkK'YaFGRhrG@ +2H929`P+@JhN-)M+4ecrlrYI[QeP6eN+"K@X,BlUm[Ekl[hr`Y9BZhXdp&PU8"hr +qrV[aZp@Ldi5[c)dFCNqLRFfIAaT2FQV6,%i'5jGeeB"83!ZV+BrpGhTl,+L9E9P +@YcA38QR#r2P@CQ+LTRAG3NQ+$SM$NKX,FjDUT445,MUVp'Y@4'De*&L!Lba9YZZ +XY5VeK0FeIiJ`"p4++Ce5`SXS'3L+*Kpc3LPp*Cb2JGBVVX`KNmF&ij`cZ(HcD@e +q0Q#54j!!(#B#EC2(!"Q$M3RIjHeZr@Z!i"R!E3h&EVDRk92jL,[MZ`2r!448c0L +CD3kr!!!!*A4&@(4$EfeYC@jd!'0XDA!bCfPQ)(BZ-#if)'*j)&PfCA-J8'PRG@9 +d0R-l[!!!!!"*48j%VN*JJYqd!!!!: + +--=====================_716373961==_ +Content-Type: application/mac-binhex40; name="HasenundFrösche.txt" +Content-Disposition: attachment; filename="HasenundFrösche.txt" + + +(This file must be converted with BinHex 4.0) +:%dKKFf9ZG@jN4R,fFf0SC5jdH(3!9%9B9(4dH(3!!!!!!Z-!!!!!4Pj%D@8J5'& +cC@iJG@jN)'4TC5"'F[CcBfKP$3e%D@8J5'&cC@iJDfaKCh4PEL"PD@jcG#$mBQ9 +b)'PSFQ8JE@RIE'PMD'8J6'&RC6XJ)RGTFL"XC@*PEL)X)(0`FQ&MD#"PD@iJ8Q9 +NEQ9b,#!LD@iJFh4PG'9b)%CeFQ0SG#"fEh)J6@9ZFf0SC@iJG@jN)&4TCA*PEL` +JC@PZC5"#CA9dC5"NCA)J5(9ZC'8X)'4PFL""C'aPFL`JDQ%JCQ&cG#"KE'aPFL" +5BA9LG'PPFQ8K)&9ZFf9bC5"cG'9dC5""EQGcG#"TFh3Jj(*RCA)JB@ac)'4PFL" +8Ef3JFf9XBR0d,L""G@BX)'aKhh3JG@jc)'9TEL"Qr()JB@aXC@eKE#"cG'9bBQ9 +Z,L)J$3e*EL"PD@jPE5"ZB@KPEL"8C@PMD#"hEfaXG'9Z)(0TC5"cD@0S)'jeEL" +PFR2NG@CPEMXJFfPP)'9TE(4PEL"TD'dJHR8l)'&XE'9TEL"NBA-JBAAICA*[FQ4 +PER4XD@0SC5"(CA6fFf8JG@jN)'PSFQ8JGh9ZC'9bBQ&bC5"(CA0dB@ad)'9bFf0 +SFQ9MDh4P)'9TEQ8J6@9ZCf8J4R,fFf0SC5`JC'PP)'&Y)&9QCA)JFf(IC@iX)(0 +[)(0PD()X)'4Khb"cD@8JBA9QFb"cBfKZC@aXFh4P)(9ZG'9bG'&eBfKdC@iZ)!d +0)NKKE(3L,#"bD@9Q)'jeEL"PBQ9Z)'4TCA0PFL"6F(*PBfKPFL`J)RGTFL"hEfa +XC@iJC'&c)%9bFq4eCQ9Z)'j[BfJJC@PZ)(GPEQPR)'&eCR0MD'PPBQ9Z,#"NC@j +Z)'&eBfJJG@jc)'EmFQ0SG'9Z,#"hD@8JD@Kb)(0PD(3X)'9TEQPRC5"8D@9bC5` +JGf9XBfKP)'&XFfmJGfpSE#"ZEf0S)(9ZCfcmBfYXD@0SCA)JFf9TEL"Yr(0cC@i +JB@ac)(GTFLiL)!d09'8!!!: + +--=====================_716373961==_-- + diff --git a/sources/testing/samples/messages/m2012.txt b/sources/testing/samples/messages/m2012.txt new file mode 100644 index 0000000..0d2abbd --- /dev/null +++ b/sources/testing/samples/messages/m2012.txt @@ -0,0 +1,108 @@ +Message-Id: <4.2.0.58.20000519005505.00a853a0@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +Date: Fri, 19 May 2000 00:56:13 -0400 +To: =?iso-8859-1?Q?Jürgen?= =?iso-8859-1?Q?_Schmürgen?= + +From: Doug Sauder +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Frösche?= +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=====================_716541962==_" + +--=====================_716541962==_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach ein= + Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde,= + der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der= + Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu;= + allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt= + erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs schnellste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ein= + wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere,= + welche also wohl noch ungl=FCcklicher sein m=FCssen als wir."=20 + + +--=====================_716541962==_ +Content-Type: image/png; name="blueball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= +--=====================_716541962==_ +Content-Type: text/plain; charset="us-ascii" +Content-Disposition: attachment; filename="farmerandstork.txt" + + +The Farmer and the Stork + +A FARMER placed nets on his newly-sown plowlands and caught a +number of Cranes, which came to pick up his seed. With them he +trapped a Stork that had fractured his leg in the net and was +earnestly beseeching the Farmer to spare his life. "Pray save +me, Master," he said, "and let me go free this once. My broken +limb should excite your pity. Besides, I am no Crane, I am a +Stork, a bird of excellent character; and see how I love and +slave for my father and mother. Look too, at my feathers-- +they are not the least like those of a Crane." The Farmer +laughed aloud and said, "It may be all as you say, I only know +this: I have taken you with these robbers, the Cranes, and you +must die in their company." + +Birds of a feather flock together. + +--=====================_716541962==_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: attachment; filename="HasenundFrösche.txt" + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach ein= + Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde,= + der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der= + Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu;= + allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt= + erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs schnellste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ein= + wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere,= + welche also wohl noch ungl=FCcklicher sein m=FCssen als wir."=20 + + +--=====================_716541962==_-- + diff --git a/sources/testing/samples/messages/m2013.txt b/sources/testing/samples/messages/m2013.txt new file mode 100644 index 0000000..5cfc61c --- /dev/null +++ b/sources/testing/samples/messages/m2013.txt @@ -0,0 +1,84 @@ +Message-Id: <4.2.0.58.20000519005818.00a86aa0@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +Date: Fri, 19 May 2000 00:59:31 -0400 +To: Heinz =?iso-8859-1?Q?Müller?= +From: Doug Sauder +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Frösche?= +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=====================_716740438==_" + +--=====================_716740438==_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach ein= + Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde,= + der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der= + Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu;= + allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt= + erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs schnellste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ein= + wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere,= + welche also wohl noch ungl=FCcklicher sein m=FCssen als wir."=20 + + +--=====================_716740438==_ +Content-Type: text/plain; name="HasenundFrösche.txt"; + x-mac-type="54455854"; x-mac-creator="74747874" +Content-Transfer-Encoding: x-uuencode +Content-Disposition: attachment; filename="HasenundFrösche.txt" + + +begin 600 HasenundFrösche.txt +M1&EE($AA +X-Sender: dwsauder@pop.example.com (Unverified) +Disposition-Notification-To: +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +X-Priority: 1 (Highest) +Date: Fri, 19 May 2000 01:06:18 -0400 +To: =?iso-8859-1?Q?Jürgen?= =?iso-8859-1?Q?_Schmürgen?= + +From: Doug Sauder +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Frösche?= +Mime-Version: 1.0 +Content-Type: text/plain; charset="iso-8859-1"; format=flowed +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach ein= +=20 +Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde,=20 +der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der= +=20 +Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben." + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu;=20 +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt= + erschreckte=20 +eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie aufs= + schnellste=20 +untertauchten. + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ein= +=20 +wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere,=20 +welche also wohl noch ungl=FCcklicher sein m=FCssen als wir." + + diff --git a/sources/testing/samples/messages/m2015.txt b/sources/testing/samples/messages/m2015.txt new file mode 100644 index 0000000..7f06960 --- /dev/null +++ b/sources/testing/samples/messages/m2015.txt @@ -0,0 +1,21 @@ +Message-Id: <4.2.0.58.20000519010842.00a8c7f0@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +X-Priority: 1 (Highest) +Date: Fri, 19 May 2000 01:10:27 -0400 +To: Heinz Müller +From: Doug Sauder +Subject: Die Hasen und die Frösche +Mime-Version: 1.0 +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: 8bit + +Die Hasen und die Frösche + +Die Hasen klagten einst über ihre mißliche Lage; "wir leben", sprach ein Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, der Adler, ja fast aller Raubtiere! Unsere stete Angst ist ärger als der Tod selbst. Auf, laßt uns ein für allemal sterben." + +In einem nahen Teich wollten sie sich nun ersäufen; sie eilten ihm zu; allein das außerordentliche Getöse und ihre wunderbare Gestalt erschreckte eine Menge Frösche, die am Ufer saßen, so sehr, daß sie aufs schnellste untertauchten. + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ersäufen noch ein wenig aufschieben, denn auch uns fürchten, wie ihr seht, einige Tiere, welche also wohl noch unglücklicher sein müssen als wir." + + diff --git a/sources/testing/samples/messages/m2016.txt b/sources/testing/samples/messages/m2016.txt new file mode 100644 index 0000000..43e73e7 --- /dev/null +++ b/sources/testing/samples/messages/m2016.txt @@ -0,0 +1,18 @@ +Message-Id: <4.2.0.58.20000519011207.00a8cd70@pop.example.com> +X-Sender: dwsauder@pop.example.com (Unverified) +X-Mailer: QUALCOMM Windows Eudora Pro Version 4.2.0.58 +X-Priority: 5 (Lowest) +Date: Fri, 19 May 2000 01:13:19 -0400 +To: Joe Blow +From: Doug Sauder +Subject: The Hare and the Tortoise +Mime-Version: 1.0 +Content-Type: text/plain; charset="us-ascii" + + +The Hare and the Tortoise + +A HARE one day ridiculed the short feet and slow pace of the Tortoise, who replied, laughing: "Though you be swift as the wind, I will beat you in a race." The Hare, believing her assertion to be simply impossible, assented to the proposal; and they agreed that the Fox should choose the course and fix the goal. On the day appointed for the race the two started together. The Tortoise never for a moment stopped, but went on with a slow but steady pace straight to the end of the course. The Hare, lying down by the wayside, fell fast asleep. At last waking up, and moving as fast as he could, he saw the Tortoise had reached the goal, and was comfortably dozing after her fatigue. + +Slow but steady wins the race. + diff --git a/sources/testing/samples/messages/m3001.txt b/sources/testing/samples/messages/m3001.txt new file mode 100644 index 0000000..30f74e5 --- /dev/null +++ b/sources/testing/samples/messages/m3001.txt @@ -0,0 +1,96 @@ +Date: Fri, 19 May 2000 09:55:48 -0400 (EDT) +From: Doug Sauder +To: Joe Blow +Subject: Test message from PINE +Message-ID: +MIME-Version: 1.0 +Content-Type: MULTIPART/MIXED; BOUNDARY="-1463757054-952513540-958744548=:8452" + + This message is in MIME format. The first part should be readable text, + while the remaining parts are likely unreadable without MIME-aware tools. + Send mail to mime@docserver.cac.washington.edu for more info. + +---1463757054-952513540-958744548=:8452 +Content-Type: TEXT/PLAIN; charset=US-ASCII + +This is a test message from PINE MUA. + + +---1463757054-952513540-958744548=:8452 +Content-Type: APPLICATION/octet-stream; name="redball.png" +Content-Transfer-Encoding: BASE64 +Content-ID: +Content-Description: A PNG graphic file +Content-Disposition: attachment; filename="redball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8A +AAABAAALAAAVAAAaAAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAj +AAAWAAAmAABhAAB7AACGAACHAAB9AAB0AABgAAA5AAAUAAAGAAAnAABLAABv +AACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABMAAB3AACZAAC0 +GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHf +hITmf3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5Pl +rKzpmZntZWXvJSXXAADBAACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADL +ICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2AAB4AABeAABAAAAiAABXAACS +AADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABHAAArAAAP +AACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABP +AAASAAACAABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADI +AADTAADNAACzAACDAABuAAAeAAB+AADAAACkAACNAAB/AABpAABQAAAwAACR +AACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACsAACvAACtAACmAACJAAB6 +AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABVAACO +AACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8 +AAA6AAAfAAAMAAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8 +LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkF +BDlQJf8zC/EIi4iKiUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp +6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ29ja2Ts4Ojkr6Li4urFDNf53N/Ow +8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFWSE1LF4A69n9G +ZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2Yn +OAj+d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1 +a/acUG5piNz/uXLzVJ2qm6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2T +VjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqVtWXrtu07BJihcsw71+zanRW8 +Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZwHBqL//8f +lz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/ +joOyYed5QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms +1y9evXid7QZacgOxmSxktNzdtSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAA +JXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5IFl2ZXMgUGlndWV0NnM7 +vAAAAABJRU5ErkJggg== +---1463757054-952513540-958744548=:8452 +Content-Type: APPLICATION/octet-stream; name="blueball.png" +Content-Transfer-Encoding: BASE64 +Content-ID: +Content-Description: A PNG graphic file +Content-Disposition: attachment; filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8A +AAgAABAAABgAAAAACCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkI +IWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQMYwpUrU5Y8Y5Y84pWs4YSs4YQs4Y +Qr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO5+/n7++cxu9S +hO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaM +vee15++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB +Mg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/b +fPn/vyh70lbsscebL5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEo +Qdvock4ne0IKMVUpKZLQDeqSTIsv+18PyqqWUw2IBsRM7307PPp+fDJrWtnp +LDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XCUpaDeQwiMpHX +P/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/M +jRxmT6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8 ++VZmYqKmdd1CSYoOiMOSGwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE +1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B1iuuzCGTxwXjnDO4d7NpbX42 +YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD/wEUVMzY +mWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBp +Z3VldDZzO7wAAAAASUVORK5CYII= +---1463757054-952513540-958744548=:8452-- diff --git a/sources/testing/samples/messages/m3002.txt b/sources/testing/samples/messages/m3002.txt new file mode 100644 index 0000000..e18c1ae --- /dev/null +++ b/sources/testing/samples/messages/m3002.txt @@ -0,0 +1,26 @@ +Date: Fri, 19 May 2000 10:18:03 -0400 (EDT) +From: Doug Sauder +To: =?iso-8859-1?Q?J=FCrgen_Schm=FCrgen?= +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Fr=F6sche?= +Message-ID: +MIME-Version: 1.0 +Content-Type: TEXT/PLAIN; charset=iso-8859-1 +Content-Transfer-Encoding: QUOTED-PRINTABLE + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach ei= +n Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, = +der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als der = +Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben."=20 + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; al= +lein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt erschreckt= +e eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie aufs schnel= +lste untertauchten.=20 + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ein = +wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere, we= +lche also wohl noch ungl=FCcklicher sein m=FCssen als wir."=20 + + diff --git a/sources/testing/samples/messages/m3003.txt b/sources/testing/samples/messages/m3003.txt new file mode 100644 index 0000000..640da6e --- /dev/null +++ b/sources/testing/samples/messages/m3003.txt @@ -0,0 +1,57 @@ +Date: Fri, 19 May 2000 10:23:16 -0400 (EDT) +From: Doug Sauder +To: =?iso-8859-1?Q?Heinz_M=FCller?= +Subject: PNG graphic +Message-ID: +MIME-Version: 1.0 +Content-Type: MULTIPART/MIXED; BOUNDARY="-1463757054-170444605-958746196=:8452" + + This message is in MIME format. The first part should be readable text, + while the remaining parts are likely unreadable without MIME-aware tools. + Send mail to mime@docserver.cac.washington.edu for more info. + +---1463757054-170444605-958746196=:8452 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + +---1463757054-170444605-958746196=:8452 +Content-Type: APPLICATION/octet-stream; name="redball.png" +Content-Transfer-Encoding: BASE64 +Content-ID: +Content-Description: red ball +Content-Disposition: attachment; filename="redball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8A +AAABAAALAAAVAAAaAAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAj +AAAWAAAmAABhAAB7AACGAACHAAB9AAB0AABgAAA5AAAUAAAGAAAnAABLAABv +AACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABMAAB3AACZAAC0 +GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHf +hITmf3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5Pl +rKzpmZntZWXvJSXXAADBAACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADL +ICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2AAB4AABeAABAAAAiAABXAACS +AADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABHAAArAAAP +AACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABP +AAASAAACAABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADI +AADTAADNAACzAACDAABuAAAeAAB+AADAAACkAACNAAB/AABpAABQAAAwAACR +AACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACsAACvAACtAACmAACJAAB6 +AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABVAACO +AACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8 +AAA6AAAfAAAMAAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8 +LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkF +BDlQJf8zC/EIi4iKiUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp +6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ29ja2Ts4Ojkr6Li4urFDNf53N/Ow +8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFWSE1LF4A69n9G +ZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2Yn +OAj+d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1 +a/acUG5piNz/uXLzVJ2qm6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2T +VjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqVtWXrtu07BJihcsw71+zanRW8 +Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZwHBqL//8f +lz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/ +joOyYed5QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms +1y9evXid7QZacgOxmSxktNzdtSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAA +JXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5IFl2ZXMgUGlndWV0NnM7 +vAAAAABJRU5ErkJggg== +---1463757054-170444605-958746196=:8452-- diff --git a/sources/testing/samples/messages/m3004.txt b/sources/testing/samples/messages/m3004.txt new file mode 100644 index 0000000..99d79e4 --- /dev/null +++ b/sources/testing/samples/messages/m3004.txt @@ -0,0 +1,42 @@ +Date: Fri, 19 May 2000 10:26:12 -0400 (EDT) +From: Doug Sauder +To: Joe Blow , + =?iso-8859-1?Q?Heinz_M=FCller?= +Subject: =?iso-8859-1?Q?Die_Hasen_und_die_Fr=F6sche?= +Message-ID: +MIME-Version: 1.0 +Content-Type: MULTIPART/MIXED; BOUNDARY="-1463757054-891160829-958746372=:8452" + + This message is in MIME format. The first part should be readable text, + while the remaining parts are likely unreadable without MIME-aware tools. + Send mail to mime@docserver.cac.washington.edu for more info. + +---1463757054-891160829-958746372=:8452 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + +---1463757054-891160829-958746372=:8452 +Content-Type: TEXT/PLAIN; charset=iso-8859-1; name*="iso-8859-1''HasenundFr%F6sche.txt" +Content-Transfer-Encoding: BASE64 +Content-ID: +Content-Description: Short story in German +Content-Disposition: attachment; filename*="iso-8859-1''HasenundFr%F6sche.txt" + +RGllIEhhc2VuIHVuZCBkaWUgRnL2c2NoZQ0NCg0NCkRpZSBIYXNlbiBrbGFn +dGVuIGVpbnN0IPxiZXIgaWhyZSBtad9saWNoZSBMYWdlOyAid2lyIGxlYmVu +Iiwgc3ByYWNoIGVpbiBSZWRuZXIsICJpbiBzdGV0ZXIgRnVyY2h0IHZvciBN +ZW5zY2hlbiB1bmQgVGllcmVuLCBlaW5lIEJldXRlIGRlciBIdW5kZSwgZGVy +IEFkbGVyLCBqYSBmYXN0IGFsbGVyIFJhdWJ0aWVyZSEgVW5zZXJlIHN0ZXRl +IEFuZ3N0IGlzdCDkcmdlciBhbHMgZGVyIFRvZCBzZWxic3QuIEF1ZiwgbGHf +dCB1bnMgZWluIGb8ciBhbGxlbWFsIHN0ZXJiZW4uIiANDQoNDQpJbiBlaW5l +bSBuYWhlbiBUZWljaCB3b2xsdGVuIHNpZSBzaWNoIG51biBlcnPkdWZlbjsg +c2llIGVpbHRlbiBpaG0genU7IGFsbGVpbiBkYXMgYXXfZXJvcmRlbnRsaWNo +ZSBHZXT2c2UgdW5kIGlocmUgd3VuZGVyYmFyZSBHZXN0YWx0IGVyc2NocmVj +a3RlIGVpbmUgTWVuZ2UgRnL2c2NoZSwgZGllIGFtIFVmZXIgc2HfZW4sIHNv +IHNlaHIsIGRh3yBzaWUgYXVmcyBzY2huZWxsc3RlIHVudGVydGF1Y2h0ZW4u +IA0NCg0NCiJIYWx0IiwgcmllZiBudW4gZWJlbiBkaWVzZXIgU3ByZWNoZXIs +ICJ3aXIgd29sbGVuIGRhcyBFcnPkdWZlbiBub2NoIGVpbiB3ZW5pZyBhdWZz +Y2hpZWJlbiwgZGVubiBhdWNoIHVucyBm/HJjaHRlbiwgd2llIGlociBzZWh0 +LCBlaW5pZ2UgVGllcmUsIHdlbGNoZSBhbHNvIHdvaGwgbm9jaCB1bmds/GNr +bGljaGVyIHNlaW4gbfxzc2VuIGFscyB3aXIuIiANDQoNDQo= +---1463757054-891160829-958746372=:8452-- diff --git a/sources/testing/samples/messages/m4000.txt b/sources/testing/samples/messages/m4000.txt new file mode 100644 index 0000000..25bb317 --- /dev/null +++ b/sources/testing/samples/messages/m4000.txt @@ -0,0 +1,629 @@ +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on xxxxxxx +X-Spam-Level: +X-Spam-Status: No, score=0.1 required=5.0 tests=ALL_TRUSTED,HTML_MESSAGE, + MIME_HTML_ONLY autolearn=no autolearn_force=no version=3.4.0 +Delivered-To: xxxxx +Received: from localhost (xxxxxxxx [127.0.0.1]) + by xxxxxxxx (Postfix) with ESMTP id 128C9500AAB + for ; Thu, 24 Jul 2014 14:39:01 +0200 (CEST) +X-Virus-Scanned: Debian amavisd-new at xxxxxxxxxx +Received: from xxxxxxx ([127.0.0.1]) + by localhost (xxxxxxxxxxxxx [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id Q0uBkN509meP for ; + Thu, 24 Jul 2014 14:39:00 +0200 (CEST) +Received: from xxxxxxxxx (xxxxxxxxxx [192.168.1.21]) + (using TLSv1 with cipher ECDHE-RSA-AES256-SHA (256/256 bits)) + (Client CN "xxxxxxxxxxxxx", Issuer "Go Daddy Secure Certificate Authority - G2" (verified OK)) + by xxxxxxxxxxxxx (Postfix) with ESMTPS id B5372500AA0 + for ; Thu, 24 Jul 2014 14:39:00 +0200 (CEST) +Received: from xxxxxxxxxxx ([192.168.1.21]) by post ([192.168.1.21]) + with mapi id 14.03.0195.001; Thu, 24 Jul 2014 14:39:03 +0200 +From: xxxxxxxxxxx +To: xxxxxxxx +Subject: Fwd: test +Thread-Topic: test +Thread-Index: Ac+nKMy+QShsqrH0RSe5w7IViN06kAAE3Mrp +Date: Thu, 24 Jul 2014 12:39:03 +0000 +Message-ID: <620D4BDC-9F59-4476-A07C-1AC7A065375E@xxxxxxxxxx> +References: <35DB5704EA64874BACB0731D6103030C4EB26D@post> +In-Reply-To: <35DB5704EA64874BACB0731D6103030C4EB26D@post> +Accept-Language: nb-NO, en-US +Content-Language: nb-NO +X-MS-Has-Attach: +X-MS-TNEF-Correlator: +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable +MIME-Version: 1.0 + + + + + + +


+
+Sendt fra min iPhone
+

+Videresendt melding:
+
+
+
+
Fra: xxxxxxxxxxxx <xxxxxxx@xxxxxxxxxx>
+Dato: 24. juli 2014 12:19:49 CEST
+Til: xxxxxxxxxx <xxxx.= +xxxxxxxxxxxxxx>
+Emne: test
+
+
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

xxxxxxxxxxx<= +/span>

+
+

Tlf dagsenter: xxxxxxxx= +

+
+

Rute 5243

+
+

L=F8yve

+
+

L=F8yve

+
+
+

Dere m=E5 gi beskjed til dag= +senter hvis noen ikke vil bli med i dag

+
+
+

Fredag

+
+

Uke 30

+
+

Mrk +

+

08:15

+
+

14:30

+
+

Telefon

+
+

Vi setter av ved dags= +enteret mellom klokken 08:40 og 09:00

+
+ + +
+

Kj=F8reliste for S=F8rsia/= + Krossen

+
+ + + +
+

xxxxxxx, leil 304

+
+

xxxxxxxxxxxxxx

+
+

rlt

+
+

T-2267

+
+

T-2267

+
+

xxx +54139

+
+

xxxxxxxx 16

+
+

xxxxxxxxxxxx

+
+ +

T-2267

+
+

T-2267

+
+

xxxxx= +54410

+
+

xxxxxxxxxxx

+
+

xxxxxxxxxx

+
+ +

T-2267

+
+

T-2267

+
+

xxxxxxxx

+
+

xxxxxxxxx

+
+

xxxxxxx

+
+

rlt

+
+

T-2267

+
+

T-2267

+
+

xxxxx +54220

+
+

xxxxxxx

+
+

xxxxxxxx

+
+ +

T-2267

+
+

T-2267

+
+

5xxxxxx7

+
+

xxxxxxxx

+
+

xxxxxxxx

+
+ +

T-2267

+
+

T-2267

+
+

xxxx +50392

+
+

xxxxxxx, inng. A

+
+

xxxxxxxxxx

+
+ +

T-2267

+
+

T-2267

+
+

xxxx= +96170

+
+

xxxxxxxx. B

+
+

xxxxxxxx

+
+

rlt. ring 5min f= +=F8r

+
+

T-2267

+
+

T-2267

+
+

xxxxx +50976

+
+

R=E5dxxxxxx, leil 401

+
+

xxxxxxx

+
+ +

T-2267

+
+

T-2267

+
+

xxxx +57776

+
+

R=E5dhxxxxxxx28, leil 302

+
+

xxxxxxx

+
+

rlt

+
+

T-2267

+
+

T-2267

+
+

xxxx +50894

+
+

xxxxxxxxx m=E5 f=F8lges helt inn. Leilighetsn= +r 304.

+
+ + +
+

xxxxxxxxringes det til 5 min f=F8r

+
+ + + +
+

Husk at vi kan xxxxx.= +

+
+ + + +
+

 

+
+
+
+ + diff --git a/sources/testing/samples/messages/smime001.txt b/sources/testing/samples/messages/smime001.txt new file mode 100644 index 0000000..6f51aa3 --- /dev/null +++ b/sources/testing/samples/messages/smime001.txt @@ -0,0 +1,40 @@ +MIME-Version: 1.0 +To: User2@examples.com +From: aliceDss@examples.com +Subject: Example 4.8 +Message-Id: <020906002550300.249@examples.com> +Date: Fri, 06 Sep 2002 00:25:21 -0300 +Content-Type: multipart/signed; + micalg=SHA1; + boundary="----=_NextBoundry____Fri,_06_Sep_2002_00:25:21"; + protocol="application/pkcs7-signature" + +This is a multi-part message in MIME format. + +------=_NextBoundry____Fri,_06_Sep_2002_00:25:21 + +This is some sample content. +------=_NextBoundry____Fri,_06_Sep_2002_00:25:21 +Content-Type: application/pkcs7-signature; name=smime.p7s +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=smime.p7s + +MIIDdwYJKoZIhvcNAQcCoIIDaDCCA2QCAQExCTAHBgUrDgMCGjALBgkqhkiG9w0BBwGgggL +gMIIC3DCCApugAwIBAgICAMgwCQYHKoZIzjgEAzASMRAwDgYDVQQDEwdDYXJsRFNTMB4XDT +k5MDgxNzAxMTA0OVoXDTM5MTIzMTIzNTk1OVowEzERMA8GA1UEAxMIQWxpY2VEU1MwggG2M +IIBKwYHKoZIzjgEATCCAR4CgYEAgY3N7YPqCp45PsJIKKPkR5PdDteoDuxTxauECE//lOFz +SH4M1vNESNH+n6+koYkv4dkwyDbeP5u/t0zcX2mK5HXQNwyRCJWb3qde+fz0ny/dQ6iLVPE +/sAcIR01diMPDtbPjVQh11Tl2EMR4vf+dsISXN/LkURu15AmWXPN+W9sCFQDiR6YaRWa4E8 +baj7g3IStii/eTzQKBgCY40BSJMqo5+z5t2UtZakx2IzkEAjVc8ssaMMMeUF3dm1nizaoFP +VjAe6I2uG4Hr32KQiWn9HXPSgheSz6Q+G3qnMkhijt2FOnOLl2jB80jhbgvMAF8bUmJEYk2 +RL34yJVKU1a14vlz7BphNh8Rf8K97dFQ/5h0wtGBSmA5ujY5A4GEAAKBgFzjuVp1FJYLqXr +d4z+p7Kxe3L23ExE0phaJKBEj2TSGZ3V1ExI9Q1tv5VG/+onyohs+JH09B41bY8i7RaWgSu +OF1s4GgD/oI34a8iSrUxq4Jw0e7wi/ZhSAXGKsZfoVi/G7NNTSljf2YUeyxDKE8H5BQP1Gp +2NOM/Kl4vTyg+W4o4GBMH8wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwHwYDVR0j +BBgwFoAUcEQ+gi5vh95K03XjPSC8QyuT8R8wHQYDVR0OBBYEFL5sobPjwfftQ3CkzhMB4v3 +jl/7NMB8GA1UdEQQYMBaBFEFsaWNlRFNTQGV4YW1wbGUuY29tMAkGByqGSM44BAMDMAAwLQ +IUVQykGR9CK4lxIjONg2q1PWdrv0UCFQCfYVNSVAtcst3a53Yd4hBSW0NevTFjMGECAQEwG +DASMRAwDgYDVQQDEwdDYXJsRFNTAgIAyDAHBgUrDgMCGjAJBgcqhkjOOAQDBC4wLAIUM/mG +f6gkgp9Z0XtRdGimJeB/BxUCFGFFJqwYRt1WYcIOQoGiaowqGzVI + +------=_NextBoundry____Fri,_06_Sep_2002_00:25:21-- diff --git a/sources/testing/samples/messages/smime002.txt b/sources/testing/samples/messages/smime002.txt new file mode 100644 index 0000000..7934c48 --- /dev/null +++ b/sources/testing/samples/messages/smime002.txt @@ -0,0 +1,18 @@ +MIME-Version: 1.0 +Message-Id: <00103112005203.00349@amyemily.ig.com> +Date: Tue, 31 Oct 2000 12:00:52 -0600 (Central Standard Time) +From: User1 +To: User2 +Subject: Example 5.3 +Content-Type: application/pkcs7-mime; + name=smime.p7m; + smime-type=enveloped-data +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=smime.p7m + +MIIBHgYJKoZIhvcNAQcDoIIBDzCCAQsCAQAxgcAwgb0CAQAwJjASMRAwDgYDVQQDEwdDYXJ +sUlNBAhBGNGvHgABWvBHTbi7NXXHQMA0GCSqGSIb3DQEBAQUABIGAC3EN5nGIiJi2lsGPcP +2iJ97a4e8kbKQz36zg6Z2i0yx6zYC4mZ7mX7FBs3IWg+f6KgCLx3M1eCbWx8+MDFbbpXadC +DgO8/nUkUNYeNxJtuzubGgzoyEd8Ch4H/dd9gdzTd+taTEgS0ipdSJuNnkVY4/M652jKKHR +LFf02hosdR8wQwYJKoZIhvcNAQcBMBQGCCqGSIb3DQMHBAgtaMXpRwZRNYAgDsiSf8Z9P43 +LrY4OxUk660cu1lXeCSFOSOpOJ7FuVyU= diff --git a/sources/testing/samples/smime.txt b/sources/testing/samples/smime.txt new file mode 100644 index 0000000..7c846c7 --- /dev/null +++ b/sources/testing/samples/smime.txt @@ -0,0 +1,63 @@ +https://tools.ietf.org/html/rfc4134 + +MIME-Version: 1.0 +To: User2@examples.com +From: aliceDss@examples.com +Subject: Example 4.8 +Message-Id: <020906002550300.249@examples.com> +Date: Fri, 06 Sep 2002 00:25:21 -0300 +Content-Type: multipart/signed; + micalg=SHA1; + boundary="----=_NextBoundry____Fri,_06_Sep_2002_00:25:21"; + protocol="application/pkcs7-signature" + +This is a multi-part message in MIME format. + +------=_NextBoundry____Fri,_06_Sep_2002_00:25:21 + +This is some sample content. +------=_NextBoundry____Fri,_06_Sep_2002_00:25:21 +Content-Type: application/pkcs7-signature; name=smime.p7s +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=smime.p7s + +MIIDdwYJKoZIhvcNAQcCoIIDaDCCA2QCAQExCTAHBgUrDgMCGjALBgkqhkiG9w0BBwGgggL +gMIIC3DCCApugAwIBAgICAMgwCQYHKoZIzjgEAzASMRAwDgYDVQQDEwdDYXJsRFNTMB4XDT +k5MDgxNzAxMTA0OVoXDTM5MTIzMTIzNTk1OVowEzERMA8GA1UEAxMIQWxpY2VEU1MwggG2M +IIBKwYHKoZIzjgEATCCAR4CgYEAgY3N7YPqCp45PsJIKKPkR5PdDteoDuxTxauECE//lOFz +SH4M1vNESNH+n6+koYkv4dkwyDbeP5u/t0zcX2mK5HXQNwyRCJWb3qde+fz0ny/dQ6iLVPE +/sAcIR01diMPDtbPjVQh11Tl2EMR4vf+dsISXN/LkURu15AmWXPN+W9sCFQDiR6YaRWa4E8 +baj7g3IStii/eTzQKBgCY40BSJMqo5+z5t2UtZakx2IzkEAjVc8ssaMMMeUF3dm1nizaoFP +VjAe6I2uG4Hr32KQiWn9HXPSgheSz6Q+G3qnMkhijt2FOnOLl2jB80jhbgvMAF8bUmJEYk2 +RL34yJVKU1a14vlz7BphNh8Rf8K97dFQ/5h0wtGBSmA5ujY5A4GEAAKBgFzjuVp1FJYLqXr +d4z+p7Kxe3L23ExE0phaJKBEj2TSGZ3V1ExI9Q1tv5VG/+onyohs+JH09B41bY8i7RaWgSu +OF1s4GgD/oI34a8iSrUxq4Jw0e7wi/ZhSAXGKsZfoVi/G7NNTSljf2YUeyxDKE8H5BQP1Gp +2NOM/Kl4vTyg+W4o4GBMH8wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwHwYDVR0j +BBgwFoAUcEQ+gi5vh95K03XjPSC8QyuT8R8wHQYDVR0OBBYEFL5sobPjwfftQ3CkzhMB4v3 +jl/7NMB8GA1UdEQQYMBaBFEFsaWNlRFNTQGV4YW1wbGUuY29tMAkGByqGSM44BAMDMAAwLQ +IUVQykGR9CK4lxIjONg2q1PWdrv0UCFQCfYVNSVAtcst3a53Yd4hBSW0NevTFjMGECAQEwG +DASMRAwDgYDVQQDEwdDYXJsRFNTAgIAyDAHBgUrDgMCGjAJBgcqhkjOOAQDBC4wLAIUM/mG +f6gkgp9Z0XtRdGimJeB/BxUCFGFFJqwYRt1WYcIOQoGiaowqGzVI + +------=_NextBoundry____Fri,_06_Sep_2002_00:25:21-- + + + +MIME-Version: 1.0 +Message-Id: <00103112005203.00349@amyemily.ig.com> +Date: Tue, 31 Oct 2000 12:00:52 -0600 (Central Standard Time) +From: User1 +To: User2 +Subject: Example 5.3 +Content-Type: application/pkcs7-mime; + name=smime.p7m; + smime-type=enveloped-data +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=smime.p7m + +MIIBHgYJKoZIhvcNAQcDoIIBDzCCAQsCAQAxgcAwgb0CAQAwJjASMRAwDgYDVQQDEwdDYXJ +sUlNBAhBGNGvHgABWvBHTbi7NXXHQMA0GCSqGSIb3DQEBAQUABIGAC3EN5nGIiJi2lsGPcP +2iJ97a4e8kbKQz36zg6Z2i0yx6zYC4mZ7mX7FBs3IWg+f6KgCLx3M1eCbWx8+MDFbbpXadC +DgO8/nUkUNYeNxJtuzubGgzoyEd8Ch4H/dd9gdzTd+taTEgS0ipdSJuNnkVY4/M652jKKHR +LFf02hosdR8wQwYJKoZIhvcNAQcBMBQGCCqGSIb3DQMHBAgtaMXpRwZRNYAgDsiSf8Z9P43 +LrY4OxUk660cu1lXeCSFOSOpOJ7FuVyU= diff --git a/sources/testing/testing-caldav.php b/sources/testing/testing-caldav.php new file mode 100644 index 0000000..4eb1063 --- /dev/null +++ b/sources/testing/testing-caldav.php @@ -0,0 +1,52 @@ +DoOptionsRequest(); +print_r($options); + +$calendars = $caldav->FindCalendars(); +print_r($calendars); + +$path = $caldav_path . "personal" . "/"; +$val = $caldav->GetCalendarDetails($path); +print_r($val); + +$begin = gmdate("Ymd\THis\Z", time() - 24*7*60*60); +$finish = gmdate("Ymd\THis\Z", 2147483647); +$msgs = $caldav->GetEvents($begin, $finish, $path); +print_r($msgs); + +// Initial sync +$results = $caldav->GetSync($path, true, CALDAV_SUPPORTS_SYNC); +print_r($results); + +sleep(60); + +$results = $caldav->GetSync($path, false, CALDAV_SUPPORTS_SYNC); +print_r($results); + +?> \ No newline at end of file diff --git a/sources/testing/testing-carddav.php b/sources/testing/testing-carddav.php new file mode 100644 index 0000000..40e3df7 --- /dev/null +++ b/sources/testing/testing-carddav.php @@ -0,0 +1,65 @@ +set_auth($username, $password); +//$server->enable_debug(); +$raw = $server->get(false, false, true); +echo "$raw\n"; +//var_dump($server->get_debug()); + +if ($raw !== false) { + $xml = new SimpleXMLElement($raw); + foreach($xml->addressbook_element as $response) { + if ($gal_url !== false) { + if (strcmp(urldecode($response->url), $gal_url) == 0) { + echo sprintf("BackendCardDAV::discoverAddressbooks() Ignoring GAL addressbook '%s'\n", $this->gal_url); + continue; + } + } + + echo sprintf("BackendCardDAV::discoverAddressbooks() Found addressbook '%s'\n", urldecode($response->url)); + } + unset($xml); +} + +//$server->enable_debug(); +$server->set_url($default_url); +$vcards = $server->do_sync(true, false, false); +//var_dump($server->get_debug()); +echo "$vcards\n"; + +echo "-----------\n"; +//$server->enable_debug(); +// TODO: set to an existing vcard ID (you will get a list with the do_sync operation +$xml = $server->get_xml_vcard('131-52C19B00-7-7A512880'); +//var_dump($server->get_debug()); +echo "$xml\n"; + +?> diff --git a/sources/testing/testing-forward.php b/sources/testing/testing-forward.php new file mode 100644 index 0000000..9463277 --- /dev/null +++ b/sources/testing/testing-forward.php @@ -0,0 +1,223 @@ +decode(array('decode_headers' => false, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')); +unset($mobj); + +$finalEmail = new Mail_mimePart('', array('content_type' => 'multipart/mixed')); +printf("%s\n", sprintf("BackendIMAP->SendMail(): is a new message or we are replacing mime")); +addTextPartsMessage($finalEmail, $message); +if (isset($message->parts)) { + printf("%s\n", sprintf("BackendIMAP->SendMail(): we have extra parts")); + // We add extra parts from the new message + addExtraSubParts($finalEmail, $message->parts); +} + +// We encode the final message +$boundary = '=_' . md5(rand() . microtime()); +$finalEmail = $finalEmail->encode($boundary); + +$finalHeaders = array('Mime-Version' => '1.0'); +// We copy all the headers, minus content_type +printf("%s\n", sprintf("BackendIMAP->SendMail(): Copying new headers")); +foreach ($message->headers as $k => $v) { + if (strcasecmp($k, 'content-type') != 0 && strcasecmp($k, 'content-transfer-encoding') != 0 && strcasecmp($k, 'mime-version') != 0) { + $finalHeaders[ucwords($k)] = $v; + } +} +foreach ($finalEmail['headers'] as $k => $v) { + $finalHeaders[$k] = $v; +} + +$finalBody = "This is a multi-part message in MIME format.\n" . $finalEmail['body']; + +unset($sourceMail); +unset($message); +unset($sourceMessage); +unset($finalEmail); + +printf("%s\n", sprintf("BackendIMAP->SendMail(): Final mail to send:")); +foreach ($finalHeaders as $k => $v) + printf("%s\n", sprintf("%s: %s", $k, $v)); +printf("\n"); +foreach (preg_split("/((\r)?\n)/", $finalBody) as $bodyline) + printf("%s\n", sprintf("%s", $bodyline)); + + + + /** + * Add text parts to a mimepart object + * + * @param Mail_mimePart $email reference to the object + * @param Mail_mimeDecode $message reference to the message + * + * @access private + * @return void + */ + function addTextPartsMessage(&$email, &$message) { + $htmlBody = $plainBody = ''; + getBodyRecursive($message, "html", $htmlBody); + getBodyRecursive($message, "plain", $plainBody); + + $altEmail = new Mail_mimePart('', array('content_type' => 'multipart/alternative')); + + if (strlen($htmlBody) > 0) { + printf("%s\n", sprintf("BackendIMAP->addTextPartsMessage(): The message has HTML body")); + $altEmail->addSubPart($htmlBody, array('content_type' => 'text/html; charset=utf-8', 'encoding' => 'base64')); + } + if (strlen($plainBody) > 0) { + printf("%s\n", sprintf("BackendIMAP->addTextPartsMessage(): The message has PLAIN body")); + $altEmail->addSubPart($plainBody, array('content_type' => 'text/plain; charset=utf-8', 'encoding' => 'base64')); + } + + $boundary = '=_' . md5(rand() . microtime()); + $altEmail = $altEmail->encode($boundary); + + $email->addSubPart($altEmail['body'], array('content_type' => 'multipart/alternative;'."\n".' boundary="'.$boundary.'"')); + + unset($altEmail); + + unset($htmlBody); + unset($plainBody); + } + + /** + * Get all parts in the message with specified type and concatenate them together, unless the + * Content-Disposition is 'attachment', in which case the text is apparently an attachment + * + * @param string $message mimedecode message(part) + * @param string $message message subtype + * @param string &$body body reference + * + * @access protected + * @return + */ + function getBodyRecursive($message, $subtype, &$body) { + if(!isset($message->ctype_primary)) return; + if(strcasecmp($message->ctype_primary,"text")==0 && strcasecmp($message->ctype_secondary,$subtype)==0 && isset($message->body)) + $body .= $message->body; + + if(strcasecmp($message->ctype_primary,"multipart")==0 && isset($message->parts) && is_array($message->parts)) { + foreach($message->parts as $part) { + if(!isset($part->disposition) || strcasecmp($part->disposition,"attachment")) { + getBodyRecursive($part, $subtype, $body); + } + } + } + } + + /** + * Add extra parts (not text; inlined or attached parts) to a mimepart object. + * + * @param Mail_mimePart $email reference to the object + * @param array $parts array of parts + * + * @access private + * @return void + */ + function addExtraSubParts(&$email, $parts) { + if (isset($parts)) { + foreach ($parts as $part) { + $new_part = null; + // Only if it's an attachment we will add the text parts, because all the inline/no disposition have been already added + if (isset($part->disposition) && $part->disposition == "attachment") { + printf("%s\n", sprintf("BackendIMAP->addExtraSubParts(): extraSubPart attachment found")); + // it's an attachment + $new_part = addSubPart($email, $part); + } + else { + printf("%s\n", sprintf("BackendIMAP->addExtraSubParts(): extraSubPart no attachment found")); + if (isset($part->ctype_primary) && $part->ctype_primary != "text" && $part->ctype_primary != "multipart") { + printf("%s\n", sprintf("BackendIMAP->addExtraSubParts(): it's not a text part or a multipart")); + // it's not a text part or a multipart + $new_part = addSubPart($email, $part); + } + } + if (isset($part->parts)) { + printf("%s\n", sprintf("BackendIMAP->addExtraSubParts(): found subparts to my sub-part. Recursive calling")); + // We add sub-parts to the new part, not to the main message + addExtraSubParts($new_part === null ? $email : $new_part, $part->parts); + } + } + } + } + + /** + * Add a subpart to a mimepart object. + * + * @param Mail_mimePart $email reference to the object + * @param object $part message part + * + * @access private + * @return void + */ + function addSubPart(&$email, $part) { + //http://tools.ietf.org/html/rfc4021 + $new_part = null; + $params = array(); + if (isset($part) && isset($email)) { + if (isset($part->ctype_primary)) { + $params['content_type'] = $part->ctype_primary; + } + if (isset($part->ctype_secondary)) { + $params['content_type'] .= '/' . $part->ctype_secondary; + } + if (isset($part->ctype_parameters)) { + foreach ($part->ctype_parameters as $k => $v) { + if(strcasecmp($k, 'boundary') != 0) { + $params['content_type'] .= '; ' . $k . '=' . $v; + } + } + } + if (isset($part->disposition)) { + $params['disposition'] = $part->disposition; + } + //FIXME: dfilename => filename + if (isset($part->d_parameters)) { + foreach ($part->d_parameters as $k => $v) { + $params[$k] = $v; + } + } + foreach ($part->headers as $k => $v) { + switch($k) { + case "content-description": + $params['description'] = $v; + break; + case "content-type": + case "content-disposition": + case "content-transfer-encoding": + // Do nothing, we already did + break; + case "content-id": + $params['cid'] = str_replace('<', '', str_replace('>', '', $v)); + break; + default: + $params[$k] = $v; + break; + } + } + + // If not exist body, the part will be multipart/alternative, so we don't add encoding + if (!isset($params['encoding']) && isset($part->body)) { + $params['encoding'] = 'base64'; + } + // We could not have body; recursive messages + $new_part = $email->addSubPart(isset($part->body) ? $part->body : "", $params); + unset($params); + } + + // return the new part + return $new_part; + } + +?> \ No newline at end of file diff --git a/sources/testing/testing-imap.php b/sources/testing/testing-imap.php new file mode 100644 index 0000000..4b8be57 --- /dev/null +++ b/sources/testing/testing-imap.php @@ -0,0 +1,10 @@ +\n", $mail); +printf("EMPTY <%b>\n", empty($mail)); + +?> diff --git a/sources/testing/testing-imap_date.php b/sources/testing/testing-imap_date.php new file mode 100644 index 0000000..d7dbf7f --- /dev/null +++ b/sources/testing/testing-imap_date.php @@ -0,0 +1,53 @@ +Nmsgs}",0); +foreach ($result as $overview) { + echo "#{$overview->msgno} ({$overview->date}) - From: {$overview->from} {$overview->subject}\n"; + if (inside_cutoffdate($limit, $overview->uid, $mbox)) + echo "INSIDE\n"; + else + echo "OUTSIDE\n"; +} +imap_close($mbox); + +function inside_cutoffdate($cutoffdate, $id, $mbox) { + printf("Checking if the messages is withing the cutoffdate %d, %s", $cutoffdate, $id); + $is_inside = false; + + if ($cutoffdate == 0) { + // No cutoffdate, all the messages are in range + $is_inside = true; + printf("No cutoffdate, all the messages are in range"); + } + else { + $overview = imap_fetch_overview($mbox, $id, FT_UID); + if (is_array($overview)) { + if (isset($overview[0]->date)) { + $epoch_sent = strtotime($overview[0]->date); + $is_inside = ($cutoffdate <= $epoch_sent); + } + else { + // No sent date defined, that's a buggy message but we will think that the message is in range + $is_inside = true; + printf("No sent date defined, that's a buggy message but we will think that the message is in range"); + } + } + else { + // No overview, maybe the message is no longer there + $is_inside = false; + printf("No overview, maybe the message is no longer there"); + } + } + + return $is_inside; +} + +?> \ No newline at end of file diff --git a/sources/testing/testing-imap_from.php b/sources/testing/testing-imap_from.php new file mode 100644 index 0000000..f5d7da8 --- /dev/null +++ b/sources/testing/testing-imap_from.php @@ -0,0 +1,135 @@ +'); + +function get_from_ldap($username, $domain) { + $from = $username; + + $ldap_conn = null; + try { + $ldap_conn = ldap_connect(IMAP_FROM_LDAP_SERVER, IMAP_FROM_LDAP_SERVER_PORT); + if ($ldap_conn) { + printf("Connected to LDAP"); + ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3); + ldap_set_option($ldap_conn, LDAP_OPT_REFERRALS, 0); + $ldap_bind = ldap_bind($ldap_conn, IMAP_FROM_LDAP_USER, IMAP_FROM_LDAP_PASSWORD); + + if ($ldap_bind) { + printf("Authenticated in LDAP"); + $filter = str_replace('#username', $username, str_replace('#domain', $domain, IMAP_FROM_LDAP_QUERY)); + printf("Searching From with filter: %s", $filter); + $search = ldap_search($ldap_conn, IMAP_FROM_LDAP_BASE, $filter, unserialize(IMAP_FROM_LDAP_FIELDS)); + $items = ldap_get_entries($ldap_conn, $search); + if ($items['count'] > 0) { + printf("Found entry in LDAP. Generating From"); + $from = IMAP_FROM_LDAP_FROM; + // We get the first object. It's your responsability to make the query unique + foreach (unserialize(IMAP_FROM_LDAP_FIELDS) as $field) { + $from = str_replace('#'.$field, $items[0][$field][0], $from); + } + } + else { + printf("No entry found in LDAP"); + } + } + else { + printf("Not authenticated in LDAP server"); + } + } + else { + printf("Not connected to LDAP server"); + } + } + catch(Exception $ex) { + printf("Error getting From value from LDAP server: %s", $ex); + } + + ldap_close($ldap_conn); + + return $from; +} + +$from = get_from_ldap('fmbiete', 'zpush.org'); +printf("%s\n", $from); + + +define('IMAP_FROM_SQL_DSN', 'mysql:host=xxxxxxx;port=3306;dbname=xxxxxxx'); +define('IMAP_FROM_SQL_USER', 'xxxxxxx'); +define('IMAP_FROM_SQL_PASSWORD', 'xxxxxxx'); +define('IMAP_FROM_SQL_OPTIONS', serialize(array(PDO::ATTR_PERSISTENT => false))); +define('IMAP_FROM_SQL_QUERY', 'select role, sede, email from usuarios where email = "#username@#domain"'); +define('IMAP_FROM_SQL_FIELDS', serialize(array('role', 'sede', 'email'))); +define('IMAP_FROM_SQL_FROM', '#role #sede <#email>'); + +function get_from_sql($username, $domain) { + $from = $username; + + $dbh = $sth = $record = null; + try { + $dbh = new PDO(IMAP_FROM_SQL_DSN, IMAP_FROM_SQL_USER, IMAP_FROM_SQL_PASSWORD, unserialize(IMAP_FROM_SQL_OPTIONS)); + printf("Connected to SQL Database"); + + $sql = str_replace('#username', $username, str_replace('#domain', $domain, IMAP_FROM_SQL_QUERY)); + printf("Searching From with filter: %s", $sql); + $sth = $dbh->prepare($sql); + $sth->execute(); + $record = $sth->fetch(PDO::FETCH_ASSOC); + if ($record) { + printf("Found entry in SQL Database. Generating From"); + $from = IMAP_FROM_SQL_FROM; + foreach (unserialize(IMAP_FROM_SQL_FIELDS) as $field) { + $from = str_replace('#'.$field, $record[$field], $from); + } + } + else { + printf("No entry found in SQL Database"); + } + } + catch(PDOException $ex) { + printf("Error getting From value from SQL Database: %s", $ex); + } + + $dbh = $sth = $record = null; + + return $from; +} + +function encoding_from($from) { + $items = explode("<", $from); + $name = trim($items[0]); + return "=?UTF-8?B?" . base64_encode($name) . "?= <" . $items[1]; +} + +$from = get_from_sql('fmbiete', 'zpush.org'); +printf("%s\n", $from); + +$from = "Francisco Miguel Biete Bañón "; +$encoded_from = encoding_from($from); +printf("%s\n", $encoded_from); + + +function parseAddr($ad) { + $addr_string = ""; + if (isset($ad) && is_array($ad)) { + foreach($ad as $addr) { + if ($addr_string) $addr_string .= ","; + $addr_string .= $addr->mailbox . "@" . $addr->host; + } + } + return $addr_string; +} + +include_once('include/z_RFC822.php'); + +$Mail_RFC822 = new Mail_RFC822(); +$fromaddr = parseAddr($Mail_RFC822->parseAddressList($encoded_from)); +printf("%s\n", $fromaddr); + +?> \ No newline at end of file diff --git a/sources/testing/testing-imap_meeting.php b/sources/testing/testing-imap_meeting.php new file mode 100644 index 0000000..6d3926f --- /dev/null +++ b/sources/testing/testing-imap_meeting.php @@ -0,0 +1,67 @@ +ParseFrom($body); + + +$props = $ical->GetPropertiesByPath('!VTIMEZONE/ATTENDEE'); +if (count($props) == 1) { + if (isset($props[0]->Parameters()["PARTSTAT"])) { + printf("DOES THIS CAUSE ERROR? %s\n", $props[0]->Parameters()["PARTSTAT"]); + } +} + +// MODIFICATIONS + // METHOD +$ical->SetPValue("METHOD", "REPLY"); + //ATTENDEE +$ical->SetCPParameterValue("VEVENT", "ATTENDEE", "PARTSTAT", "ACCEPTED"); + +printf("%s\n", $ical->Render()); + + +include_once('include/mimePart.php'); + +$mail = new Mail_mimepart(); +$headers = array("MIME-version" => "1.0", + "From" => $mail->encodeHeader("from", "Pedro Picapiedra ", "UTF-8"), + "To" => $mail->encodeHeader("to", "Pablo Marmol ", "UTF-8"), + "Date" => gmdate("D, d M Y H:i:s", time())." GMT", + "Subject" => $mail->encodeHeader("subject", "This is a subject", "UTF-8"), + "Content-class" => "urn:content-classes:calendarmessage", + "Content-transfer-encoding" => "8BIT"); +$mail = new Mail_mimepart($ical->Render(), array("content_type" => "text/calendar; method=REPLY; charset=UTF-8", "headers" => $headers)); + +$message = ""; +$encoded_mail = $mail->encode(); +foreach ($encoded_mail["headers"] as $k => $v) { + $message .= $k . ": " . $v . "\r\n"; +} +$message .= "\r\n" . $encoded_mail["body"] . "\r\n"; + +printf("%s\n", $message); + + +include_once('lib/utils/utils.php'); +include_once('lib/core/zpushdefs.php'); +include_once('lib/core/zlog.php'); +include_once('lib/utils/timezoneutil.php'); + +define('LOGLEVEL', LOGLEVEL_DEBUG); +define('LOGUSERLEVEL', LOGLEVEL_DEVICEID); + +$props = $ical->GetPropertiesByPath("VTIMEZONE/TZID"); +if (count($props) > 0) { + $tzid = $props[0]->Value(); +// printf("TZID %s\n", $props[0]->Value()); +} +// print_r(TimezoneUtil::GetFullTZFromTZName($tzid)); + + + + +?> \ No newline at end of file diff --git a/sources/testing/testing-imap_smtp.php b/sources/testing/testing-imap_smtp.php new file mode 100644 index 0000000..5a21acc --- /dev/null +++ b/sources/testing/testing-imap_smtp.php @@ -0,0 +1,38 @@ + 'smtp.zpush.org', 'port' => 25, 'auth' => true, "username" => "fmbiete", "password" => "password_account", "debug" => true, "pipelining" => true); +$toaddr = "fmbiete@zpush.org"; +$headers = array('Subject' => 'Testing SMTP', 'From' => 'fmbiete@zpush.org', 'Return-Path' => 'fmbiete@zpush.org', 'To' => 'fmbiete@zpush.org', 'Cc' => 'fmbiete@zpush.org,fmbiete@zpush.net'); +$body = "This is a test"; + + +ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->sendMessage(): SendingMail with %s", "smtp")); +$mail =& Mail::factory("smtp", $imap_smtp_params); +$send = $mail->send($toaddr, $headers, $body); +ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->sendMessage(): send return value %s", $send)); + +if ($send !== true) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): The email could not be sent")); +} + +?> \ No newline at end of file diff --git a/sources/testing/testing-mime-mail-parse.php b/sources/testing/testing-mime-mail-parse.php new file mode 100644 index 0000000..a614db7 --- /dev/null +++ b/sources/testing/testing-mime-mail-parse.php @@ -0,0 +1,63 @@ +decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')); + $handle = fopen($new_file, "w"); + fwrite($handle, build_mime_message($message)); + fclose($handle); + + foreach ($message->headers as $k => $v) { + if (is_array($v)) { + foreach ($v as $vk => $vv) { + printf("Header <%s> <%s> <%s>\n", $k, $vk, $vv); + } + } + else { + printf("Header <%s> <%s>\n", $k, $v); + } + } + + $text = $html = ""; + Mail_mimeDecode::getBodyRecursive($message, "plain", $text); + Mail_mimeDecode::getBodyRecursive($message, "html", $html); + + printf("TEXT Body <%s>\n", $text); + + printf("HTML Body <%s>\n", $html); +} +?> \ No newline at end of file diff --git a/sources/testing/testing-mime-split.php b/sources/testing/testing-mime-split.php new file mode 100644 index 0000000..25a1096 --- /dev/null +++ b/sources/testing/testing-mime-split.php @@ -0,0 +1,24 @@ +getSendArray(); +if ($parts === false) { + printf("ERROR splitting message\n"); +} +else { + list($recipents,$headers,$body) = $parts; + printf("RECIPIENTS\n"); + print_r($recipents); + printf("\nHEADERS\n"); + print_r($headers); + printf("\nBODY\n"); + print_r($body); + printf("\n"); + //$mail = Mail::factory('smtp'); + //$mail->send($recipents,$headers,$body); +} + +?> \ No newline at end of file diff --git a/sources/testing/testing-mime.php b/sources/testing/testing-mime.php new file mode 100644 index 0000000..ae4a20e --- /dev/null +++ b/sources/testing/testing-mime.php @@ -0,0 +1,184 @@ +decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')); + + +function addSubPart(&$email, $part) { + //http://tools.ietf.org/html/rfc4021 + $new_part = null; + $params = array(); + if (isset($part)) { + if (isset($part->ctype_primary)) { + $params['content_type'] = $part->ctype_primary; + } + if (isset($part->ctype_secondary)) { + $params['content_type'] .= '/' . $part->ctype_secondary; + } + if (isset($part->ctype_parameters)) { + foreach ($part->ctype_parameters as $k => $v) { + if(strcasecmp($k, 'boundary') != 0) { + $params['content_type'] .= '; ' . $k . '=' . $v; + } + } + } + if (isset($part->disposition)) { + $params['disposition'] = $part->disposition; + } + //FIXME: dfilename => filename + if (isset($part->d_parameters)) { + foreach ($part->d_parameters as $k => $v) { + $params[$k] = $v; + } + } + foreach ($part->headers as $k => $v) { + switch($k) { + case "content-description": + $params['description'] = $v; + break; + case "content-type": + case "content-disposition": + case "content-transfer-encoding": + // Do nothing, we already did + break; + case "content-id": + $params['cid'] = str_replace('<', '', str_replace('>', '', $v)); + break; + default: + $params[$k] = $v; + break; + } + } + + // If not exist body, the part will be multipart/alternative, so we don't add encoding + if (!isset($params['encoding']) && isset($part->body)) { + $params['encoding'] = 'base64'; + } + // We could not have body; recursive messages + $new_part = $email->addSubPart(isset($part->body) ? $part->body : "", $params); + unset($params); + } + + // return the new part + return $new_part; +} + +function fixCharsetAndAddSubParts(&$email, $part) { + if (isset($part)) { + $new_part = null; + if (isset($part->ctype_parameters['charset'])) { + $part->ctype_parameters['charset'] = 'UTF-8'; + $new_part = addSubPart($email, $part); + } + else { + $new_part = addSubPart($email, $part); + } + + if (isset($part->parts)) { + foreach ($part->parts as $subpart) { + fixCharsetAndAddSubParts($new_part, $subpart); + } + } + } +} + + +$boundary = '=_' . md5(rand() . microtime()); +$mimeHeaders = Array(); +$mimeHeaders['headers'] = Array(); +$is_mime = false; +foreach ($message->headers as $key => $value) { + switch($key) { + case 'content-type': + $new_value = $message->ctype_primary . "/" . $message->ctype_secondary; + $is_mime = (strcasecmp($message->ctype_primary, 'multipart') == 0); + + foreach ($message->ctype_parameters as $ckey => $cvalue) { + switch($ckey) { + case 'charset': + $new_value .= '; charset="UTF-8"'; + break; + case 'boundary': + // Do nothing, we are encoding also the headers + break; + default: + $new_value .= '; ' . $ckey . '="' . $cvalue . '"'; + break; + } + } + + $mimeHeaders['content_type'] = $new_value; + break; + case 'content-transfer-encoding': + if (strcasecmp($value, "base64") == 0 || strcasecmp($value, "binary") == 0) { + $mimeHeaders['encoding'] = "base64"; + } + else { + $mimeHeaders['encoding'] = "8bit"; + } + break; + case 'content-id': + $mimeHeaders['cid'] = $value; + break; + case 'content-location': + $mimeHeaders['location'] = $value; + break; + case 'content-disposition': + $mimeHeaders['disposition'] = $value; + break; + case 'content-description': + $mimeHeaders['description'] = $value; + break; + default: + if (is_array($value)) { + foreach($value as $v) { + $mimeHeaders['headers'][$key] = $v; + } + } + else { + $mimeHeaders['headers'][$key] = $value; + } + break; + } +} + +$finalEmail = new Mail_mimePart(isset($message->body) ? $message->body : "", $mimeHeaders); +if (isset($message->parts)) { + foreach ($message->parts as $part) { + fixCharsetAndAddSubParts($finalEmail, $part); + } +} +$finalEmail = $finalEmail->encode($boundary); +$headers = ""; +foreach ($finalEmail['headers'] as $key => $value) { + $headers .= "$key: $value\n"; +} + +if ($is_mime) { + echo "$headers\nThis is a multi-part message in MIME format.\n".$finalEmail['body']; +} +else { + echo "$headers\n".$finalEmail['body']; +} + +unset($headers); +unset($mimeHeaders); +unset($finalEmail); +unset($message); +unset($mobj); +unset($mail); + +?> diff --git a/sources/testing/testing-mimetype.php b/sources/testing/testing-mimetype.php new file mode 100644 index 0000000..8e435fa --- /dev/null +++ b/sources/testing/testing-mimetype.php @@ -0,0 +1,40 @@ + diff --git a/sources/testing/testing-preg_split.php b/sources/testing/testing-preg_split.php new file mode 100644 index 0000000..9b02fb0 --- /dev/null +++ b/sources/testing/testing-preg_split.php @@ -0,0 +1,12 @@ + diff --git a/sources/testing/testing-ternary.php b/sources/testing/testing-ternary.php new file mode 100644 index 0000000..f616e22 --- /dev/null +++ b/sources/testing/testing-ternary.php @@ -0,0 +1,29 @@ + diff --git a/sources/tools/fix-meetings-2.0.8+2.1.0-ios7.py b/sources/tools/fix-meetings-2.0.8+2.1.0-ios7.py new file mode 100644 index 0000000..5b343ca --- /dev/null +++ b/sources/tools/fix-meetings-2.0.8+2.1.0-ios7.py @@ -0,0 +1,90 @@ +#!/usr/bin/python + +from MAPI.Util import * +from MAPI.Util.AddressBook import * +from MAPI.Time import * + +skcache = {} + +import inetmapi +import sys + + +print 'Usage: %s [ ] []' % __file__ +print 'If username is omitted, all users are scanned' +raw_input("Press to continue or CTRL-C to stop") + +s = OpenECSession('SYSTEM', '', 'file:///var/run/zarafa') + +sslkey_file = None +sslkey_pass = None +if len(sys.argv) > 1: + (sslkey_file, sslkey_pass) = sys.argv[1:3] + +if len(sys.argv) > 3: + users = [sys.argv[3]] +else: + users = GetUserList(s) + +for username in users: + print 'Processing user %s' % username + + try: + s = OpenECSession(username, '', 'file:///var/run/zarafa', sslkey_file = sslkey_file, sslkey_pass = sslkey_pass) + + st = GetDefaultStore(s) + + + ab = s.OpenAddressBook(0, None, 0) + identity = s.QueryIdentity() + gabid = ab.GetDefaultDir() + gabcontainer = ab.OpenEntry(gabid, None, 0) + gab = gabcontainer.GetContentsTable(0) + gab.SetColumns([PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_ENTRYID, PR_SEARCH_KEY, PR_ADDRTYPE], 0) + + root = st.OpenEntry(None, None, MAPI_MODIFY) + calid = root.GetProps([PR_IPM_APPOINTMENT_ENTRYID], 0)[0] + if calid.ulPropTag != PR_IPM_APPOINTMENT_ENTRYID: + print 'User has no calendar' + continue + + cal = st.OpenEntry(calid.Value, None, 0) + + t = cal.GetContentsTable(0) + + # Restrict to meetings only (AppointmentStateFlags >= 1) + + t.Restrict(SAndRestriction([ + SOrRestriction([ + SPropertyRestriction(RELOP_EQ, 0x8023000b, SPropValue(0x8023000b, True)), # Recurring OR + SPropertyRestriction(RELOP_GE, 0x800e0040, SPropValue(0x800e0040, unixtime(time.time() - 1*7*24*60*60))) # Starts after now()-'1 week' + ]) + ]), 0) + t.SetColumns([PR_ENTRYID], 0) + + rows = t.QueryRows(-1, 0) + + for row in rows: + modified = False + + message = st.OpenEntry(row[0].Value, None, MAPI_MODIFY) + + subject = message.GetProps([PR_SUBJECT], 0)[0].Value + nameprops = message.GetProps([PR_SENT_REPRESENTING_ENTRYID, 0x80180003], 0) + + prevstatus = nameprops[1].Value + + if nameprops[0].Value == identity and prevstatus != 1: + print "User is organizer of", "'"+subject+"'", "setting correct flag." + message.SetProps([SPropValue(0x80180003, 1)]) + message.SaveChanges(0) + elif nameprops[0].Value != identity and prevstatus == 1: + print "User is attendee of", "'"+subject+"'", "setting correct flag." + message.SetProps([SPropValue(0x80180003, 5)]) + message.SaveChanges(0) + else: + print "Correct property set for", "'"+subject+"'", "skipping." + + except MAPIError, e: + print e + pass diff --git a/sources/tools/migrate-2.0.x-2.1.0.php b/sources/tools/migrate-2.0.x-2.1.0.php index 22045cc..e9c7606 100755 --- a/sources/tools/migrate-2.0.x-2.1.0.php +++ b/sources/tools/migrate-2.0.x-2.1.0.php @@ -44,7 +44,7 @@ ************************************************/ // Please adjust to match your z-push installation directory, usually /usr/share/z-push -define('ZPUSH_BASE_PATH', "../"); +define('ZPUSH_BASE_PATH', "../src"); @@ -214,4 +214,4 @@ class StateMigrator20xto210 { } } -?> +?> \ No newline at end of file diff --git a/sources/version.php b/sources/version.php index b658850..9802487 100644 --- a/sources/version.php +++ b/sources/version.php @@ -41,6 +41,7 @@ * Consult LICENSE file for details ************************************************/ -define("ZPUSH_VERSION", "2.1.3-1892"); -?> \ No newline at end of file +define("ZPUSH_VERSION", "SVN-trunk-r1928"); + +?> diff --git a/sources/z-push-top.php b/sources/z-push-top.php index b0fbf93..9b71e67 100755 --- a/sources/z-push-top.php +++ b/sources/z-push-top.php @@ -658,9 +658,9 @@ class ZPushTop { */ private function getLine($l) { if ($this->wide === true) - return sprintf("%s%s%s%s%s%s%s%s", $this->ptStr($l['pid'],6), $this->ptStr($l['ip'],16), $this->ptStr($l['user'],24), $this->ptStr($l['command'],16), $this->ptStr($this->sec2min($l['time']),8), $this->ptStr($l['devagent'],28), $this->ptStr($l['devid'],30, true), $l['addinfo']); + return sprintf("%s%s%s%s%s%s%s%s", $this->ptStr($l['pid'],6), $this->ptStr($l['ip'],16), $this->ptStr($l['user'],24), $this->ptStr($l['command'],16), $this->ptStr($this->sec2min($l['time']),8), $this->ptStr($l['devagent'],28), $this->ptStr($l['devid'],33, true), $l['addinfo']); else - return sprintf("%s%s%s%s%s%s%s%s", $this->ptStr($l['pid'],6), $this->ptStr($l['ip'],10), $this->ptStr($l['user'],8), $this->ptStr($l['command'],11), $this->ptStr($this->sec2min($l['time']),6), $this->ptStr($l['devagent'],20), $this->ptStr($l['devid'],12, true), $l['addinfo']); + return sprintf("%s%s%s%s%s%s%s%s", $this->ptStr($l['pid'],6), $this->ptStr($l['ip'],16), $this->ptStr($l['user'],8), $this->ptStr($l['command'],8), $this->ptStr($this->sec2min($l['time']),6), $this->ptStr($l['devagent'],20), $this->ptStr($l['devid'],12, true), $l['addinfo']); } /** @@ -766,4 +766,4 @@ class ZPushTop { } -?> \ No newline at end of file +?>