commit 011be933a0f98a547d744ad3c98171cabc82ff09 Author: root Date: Wed Dec 17 15:40:48 2014 +0000 first commit diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..ff78c69 --- /dev/null +++ b/manifest.json @@ -0,0 +1,36 @@ +{ + "name": "Z-push", + "id": "z-push", + "description": { + "en": "A self hostable read-it-later app", + "fr": "Une application de lecture-plus-tard auto-hébergeable" + }, + "licence": "WTFPL-2", + "developer": { + "name": "beudbeud", + "email": "beudbeud@beudibox.fr", + "url": "http://www.z-push.org" + }, + "multi_instance": "true", + "arguments": { + "install" : [ + { + "name": "domain", + "ask": { + "en": "Choose a domain for Z-push", + "fr": "Choisissez un domaine pour Z-push" + }, + "example": "domain.org" + }, + { + "name": "path", + "ask": { + "en": "Choose a path for Z-push", + "fr": "Choisissez un chemin pour Z-push" + }, + "example": "/z-push", + "default": "/z-push" + } + ] + } +} diff --git a/scripts/install b/scripts/install new file mode 100644 index 0000000..2f175b3 --- /dev/null +++ b/scripts/install @@ -0,0 +1,35 @@ +#!/bin/bash + +# Retrieve arguments +domain=$1 +path=$2 + +# Check domain/path availability +sudo yunohost app checkurl $domain$path -a z-push +if [[ ! $? -eq 0 ]]; then +exit 1 +fi + +# Copy files to the right place +final_path=/var/www/z-push +sudo mkdir -p $final_path +sudo cp -a ../sources/* $final_path + +# Set permissions to roundcube directory +sudo chown -R www-data: $final_path + +# Configuration +#sudo cp ../conf/config.inc.php $final_path/inc/poche/ + +# Modify Nginx configuration file and copy it to Nginx conf directory +#sed -i "s@PATHTOCHANGE@$path@g" ../conf/nginx.conf* +#sed -i "s@ALIASTOCHANGE@$final_path/@g" ../conf/nginx.conf* +#sudo cp ../conf/nginx.conf /etc/nginx/conf.d/$domain.d/z-push.conf + +# Enable api for client +sudo yunohost app setting z-push skipped_uris -v "/" + +# Reload Nginx and regenerate SSOwat conf +sudo service nginx reload +sudo yunohost app ssowatconf + diff --git a/scripts/remove b/scripts/remove new file mode 100644 index 0000000..cd29099 --- /dev/null +++ b/scripts/remove @@ -0,0 +1,4 @@ +#!/bin/bash + +sudo rm -rf /var/www/z-push +#sudo rm -f /etc/nginx/conf.d/$domain.d/z-push.conf diff --git a/sources/INSTALL b/sources/INSTALL new file mode 100644 index 0000000..a989fc5 --- /dev/null +++ b/sources/INSTALL @@ -0,0 +1,288 @@ +Installing Z-Push +====================== + +Requirements +------------ + +Z-Push 2 runs only on PHP 5.1 or later +A PEAR dependency as in previous versions does not exist in Z-Push 2. + +The PHP version requirement is met in these distributions and versions (or later). + +Debian 4.0 (etch) +Ubuntu 8.04 (hardy heron) +RHEL/CentOS 5.5 +Fedora 5 (bordeaux) +OpenSuse 10.1 +Slackware 12.0 +Gentoo 2006.1 +FreeBSD 6.1 +OpenBSD 4.0 +Mandriva 2007 + +If your distribution is not listed here, you can check which PHP version +is default for it at http://distrowatch.com/. + +Additional informations can be found in the Zarafa Administrator Manual: +http://doc.zarafa.com/trunk/Administrator_Manual/en-US/html/_zpush.html + + +Additional php packages +---------------------- +To use the full featureset of Z-Push 2 and the z-push-top command line utility, +additional php packages are required. These provide SOAP support, access to +process control and shared memory. + +These packages vary in names between the distributions. +- Generally install the packages: php-cli php-soap +- On Suse (SLES & OpenSuse) install the packages: php53 php53-soap php53-pcntl php53-sysvshm php53-sysvsem php53-posix +- On RHEL based systems install the package: php-cli php-soap php-process + In order to install these packages you need to add an extra channel subscription + from the RHEL Server Optional channel. + + +How to install +-------------- + +To install Z-Push, simply untar the z-push archive, e.g. with: + tar -xzvf z-push-[version]-{buildnr}.tar.gz + +The tar contains a folder which has the following structure: + z-push-[version]-{buildnr} + +The contents of this folder should be copied to /usr/share/z-push. +In a case that /usr/share/z-push does not exist yet, create it with: + mkdir -p /usr/share/z-push + + cp -R z-push-[version]-{buildnr}/* /usr/share/z-push/ + +Edit the config.php file in the Z-Push directory to fit your needs. +If you intend to use Z-Push with Zarafa backend and Zarafa is installed +on the same server, it should work out of the box without changing anything. +Please also set your timezone in the config.php file. + +The parameters and their roles are also explained in the config.php file. + +By default the state directory is /var/lib/z-push, the log directory /var/log/z-push. +Make sure that these directories exist and are writeable for your webserver +process, so either change the owner of these directories to the UID of +your apache process or make the directories world writeable: + + chmod 755 /var/lib/z-push /var/log/z-push + chown apache:apache /var/lib/z-push /var/log/z-push + +For the default webserver user please refer to your distribution's manual. + +Now, you must configure Apache to redirect the URL +'Microsoft-Server-ActiveSync' to the index.php file in the Z-Push +directory. This can be done by adding the line: + + Alias /Microsoft-Server-ActiveSync /usr/share/z-push/index.php + +to your httpd.conf file. Make sure that you are adding the line to the +correct part of your Apache configuration, taking care of virtual hosts and +other Apache configurations. +Another possibility is to add this line to z-push.conf file inside the directory +which contents are automatically processed during the webserver start (by +default it is conf.d inside the /etc/apache2 or /etc/httpd depending on your +distribution). + +You have to reload your webserver after making these configurations. + +*WARNING* You CANNOT simply rename the z-push directory to +Microsoft-Server-ActiveSync. This will cause Apache to send redirects to the +mobile device, which will definitely break your mobile device synchronisation. + +Lastly, make sure that PHP has the following settings: + + php_flag magic_quotes_gpc off + php_flag register_globals off + php_flag magic_quotes_runtime off + php_flag short_open_tag on + +You can set this in the httpd.conf, in php.ini or in an .htaccess file in +the root of z-push. + +If you have several php applications on the same system, you could specify the +z-push directory so these settings are considered only there. + + php_flag magic_quotes_gpc off + php_flag register_globals off + php_flag magic_quotes_runtime off + php_flag short_open_tag on + + +If you don't set this up correctly, you will not be +able to login correctly via z-push. + +Please also set a memory_limit for php to 128M in php.ini. + +Z-Push writes files to your file system like logs or data from the +FileStateMachine (which is default). In order to make this possible, +you either need to disable the php-safe-mode in php.ini or .htaccess with + php_admin_flag safe_mode off +or configure it accordingly, so Z-Push is allowed to write to the +log and state directories. + +After doing this, you should be able to synchronize with your mobile device. + +To use the command line tools, access the installation directory +(usually /usr/share/z-push) and execute: + ./z-push-top.php and/or + ./z-push-admin.php + +To facilitate the access symbolic links can be created, by executing: + ln -s /usr/share/z-push/z-push-admin.php /usr/sbin/z-push-admin + ln -s /usr/share/z-push/z-push-top.php /usr/sbin/z-push-top + +With these symlinks in place the cli tools can be accessed from any +directory and without the php file extension. + + +Upgrade +------- +Upgrading to a newer Z-Push version follows the same path as the +initial installation. +When upgrading to a new minor version e.g. from Z-Push 1.4 to +Z-Push 1.4.1, the existing Z-Push directory can be overwritten +when extracting the archive. When installing a new major version +it is recommended to extract the tarball to another directory and +to copy the state from the existing installation. + +*Important* +It is crucial to always keep the data of the state directory in order +to ensure data consistency on already synchronized mobiles. + +Without the state information mobile devices, which already have an +ActiveSync profile, will receive duplicate items or the synchronization +will break completely. + +*Important* +Upgrading to Z-Push 2.X from 1.X it is not necessary to copy the state +directory because states are not compatible. However Z-Push 2 implements +a fully automatic resynchronizing of devices in the case states are +missing or faulty. + +*Important* +Downgrading from Z-Push 2.X to 1.X is not simple. As the states are not +compatible you would have to follow the procedure for a new installation +and re-create profiles on every device. + +*Important* +States of Z-Push 2.0 and Z-Push 2.1 are not compatible. A state migration +script called migrate-2.0.x-2.1.0.php is available in the tools folder. + +*Important* +When running Z-Push seperately from your Zarafa installation you had in +the past to configure MAPI_SERVER directly in the config.php of Z-Push. +This setting has now moved to the config.php file of the Zarafa backend +(backend/zarafa/config.php). + +Please also observe the published release notes of the new Z-Push version. +For some releases it is necessary to e.g. resynchronize the mobile. + + +S/MIME +------ +Z-Push supports signing and en-/decrypting of emails on mobile devices +since the version 2.0.7. + +*Important* +Currently only Android 4.X and higher and iOS 5 and higher devices are +known to support encryption/signing of emails. + +It might be possible that PHP functions require CA information in order +to validate certs. Therefore the CAINFO parameter in the config.php +must be configured properly. + +The major part of S/MIME deployment is the PKI setup. It includes the +public-private key/certificate obtaining, their management in directory +service and roll-out to the mobile devices. Individual certificates can +either be obtained from a local (company intern) or a public CA. There +are various public CAs offering certificates: commercial ones e.g. +Symantec or Comodo or community-driven e.g. CAcert.org. + +Both most popular directory services Microsoft Active Directory (MS AD) +and free open source solution OpenLDAP allow to save certificates. Private +keys/certificates reside in user’s directory or on a smartcard. Public +certificates are saved in directory. MS AD and OpenLDAP both use +serCertificate attribute to save it. + +In Active Directory the public key for contacts from GAB is saved in +PR_EMS_AB_TAGGED_X509_CERT (0x8C6A1102) property and if you save a key +in a contact it’s PR_USER_X509_CERTIFICATE (0x3A701102). + +In LDAP public key for contacts from GAB is saved in userCertificate +property. It should be mapped to 0x3A220102 in ldap.propmap.cfg +(0x3A220102 = userCertificate). Make sure it looks like this in LDAP: + +userCertificate;binary + MIIFGjCCBAKgAwIBAgIQbRnqpxlPa… + +*Important* +It is strongly recommended to use MS AD or LDAP to manage certificates. +Other user plugin options like db or unix might not work correctly and +are not supported. + +For in-depth information please refer to: +http://www.zarafa.com/blog/post/2013/05/smime-z-push-signing-and-en-decrypting-emails-mobile-devices + +Setting up your mobile device +----------------------------- + +This is simply a case of adding an 'exchange server' to your activesync +server list, specifying the IP address of the Z-Push's apache server, +disabling SSL, unless you have already setup SSL on your Apache server, +setting the correct username and password (the domain is ignored, you can +simply specify 'domain' or some other random string), and then going through +the standard activesync settings. + +Once you have done this, you should be able to synchronise your mobile +simply by clicking the 'Sync' button in ActiveSync on your mobile. + +*NOTE* using the synchronisation without SSL is not recommended because +your private data is transmitted in clear text over the net. Configuring +SSL on Apache is beyond of the scope of this document. Please refer to +Apache documention available at http://httpd.apache.org/docs/ + + +Troubleshooting +--------------- + +Most problems will be caused by incorrect Apache settings. To test whether +your Apache setup is working correctly, you can simply type the Z-Push URL +in your browser, to see if apache is correctly redirecting your request to +z-push. You can simply use: + + http:///Microsoft-Server-ActiveSync + +If correctly configured, you should see a username/password request and +when you specify a valid username and password, you should see a Z-Push +information page, saying that this kind of requests is not supported. +Without authentication credentials Z-Push displays general information. + +If not then check your PHP and Apache settings and Apache error logs. + +If you have other synchronisation problems, you can increase the LOGLEVEL +parameter in the config e.g. to LOGLEVEL_DEBUG or LOGLEVEL_WBXML. + +The z-push.log file will then collect detailed debug information from your +synchronisation. + +*NOTE* This setting will set Z-Push to log the detailed information for +*every* user on the system. You can set a different log level for particular +users by adding them comma separated to $specialLogUsers in the config.php + e.g. $specialLogUsers = array("user1", "user2", "user3"); + + *NOTE* Be aware that if you are using LOGLEVEL_DEBUG and LOGLEVEL_WBXML + Z-Push will be quite talkative, so it is advisable to use log-rotate + on the log file. + +*Repeated incorrect password messages* +If a password contains characters which are encoded differently in ISO-8859-1 +and Windows-1252 encodings (e.g. "§") the login might fail with Z-Push but +it works fine with the WebApp/Webaccess. The solution is to add: + +setlocale(LC_CTYPE, "en_US.UTF-8"); + +to the config.php file. diff --git a/sources/LICENSE b/sources/LICENSE new file mode 100644 index 0000000..2ca933b --- /dev/null +++ b/sources/LICENSE @@ -0,0 +1,696 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. + + +--- + +Copyright 2007 - 2013 Zarafa Deutschland GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License, version 3, +as published by the Free Software Foundation with the following additional +term according to sec. 7: + +According to sec. 7 of the GNU Affero General Public License, version 3, +the terms of the AGPL are supplemented with the following terms: + +"Zarafa" is a registered trademark of Zarafa B.V. +"Z-Push" is a registered trademark of Zarafa Deutschland GmbH +The licensing of the Program under the AGPL does not imply a trademark license. +Therefore any rights, title and interest in our trademarks remain entirely with us. + +However, if you propagate an unmodified version of the Program you are +allowed to use the term "Z-Push" to indicate that you distribute the Program. +Furthermore you may use our trademarks where it is necessary to indicate +the intended purpose of a product or service provided you use it in accordance +with honest practices in industrial or commercial matters. +If you want to propagate modified versions of the Program under the name "Z-Push", +you may only do so if you have a written permission by Zarafa Deutschland GmbH +(to acquire a permission please contact Zarafa at trademark@zarafa.com). + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/sources/backend/combined/combined.php b/sources/backend/combined/combined.php new file mode 100644 index 0000000..367e4e9 --- /dev/null +++ b/sources/backend/combined/combined.php @@ -0,0 +1,419 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// default backend +include_once('lib/default/backend.php'); + +//include the CombinedBackend's own config file +require_once("backend/combined/config.php"); +require_once("backend/combined/importer.php"); +require_once("backend/combined/exporter.php"); + +class BackendCombined extends Backend { + public $config; + public $backends; + private $activeBackend; + private $activeBackendID; + + /** + * Constructor of the combined backend + * + * @access public + */ + public function BackendCombined() { + 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'](); + } + ZLog::Write(LOGLEVEL_INFO, sprintf("Combined %d backends loaded.", count($this->backends))); + } + + /** + * Authenticates the user on each backend + * + * @param string $username + * @param string $domain + * @param string $password + * + * @access public + * @return boolean + */ + public function Logon($username, $domain, $password) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->Logon('%s', '%s',***))", $username, $domain)); + if(!is_array($this->backends)){ + return false; + } + foreach ($this->backends as $i => $b){ + $u = $username; + $d = $domain; + $p = $password; + if(isset($this->config['backends'][$i]['users'])){ + if(!isset($this->config['backends'][$i]['users'][$username])){ + unset($this->backends[$i]); + continue; + } + if(isset($this->config['backends'][$i]['users'][$username]['username'])) + $u = $this->config['backends'][$i]['users'][$username]['username']; + if(isset($this->config['backends'][$i]['users'][$username]['password'])) + $p = $this->config['backends'][$i]['users'][$username]['password']; + if(isset($this->config['backends'][$i]['users'][$username]['domain'])) + $d = $this->config['backends'][$i]['users'][$username]['domain']; + } + if($this->backends[$i]->Logon($u, $d, $p) == false){ + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->Logon() failed on %s ", $this->config['backends'][$i]['name'])); + return false; + } + } + ZLog::Write(LOGLEVEL_INFO, "Combined->Logon() success"); + return true; + } + + /** + * Setup the backend to work on a specific store or checks ACLs there. + * If only the $store is submitted, all Import/Export/Fetch/Etc operations should be + * performed on this store (switch operations store). + * If the ACL check is enabled, this operation should just indicate the ACL status on + * the submitted store, without changing the store for operations. + * For the ACL status, the currently logged on user MUST have access rights on + * - the entire store - admin access if no folderid is sent, or + * - on a specific folderid in the store (secretary/full access rights) + * + * The ACLcheck MUST fail if a folder of the authenticated user is checked! + * + * @param string $store target store, could contain a "domain\user" value + * @param boolean $checkACLonly if set to true, Setup() should just check ACLs + * @param string $folderid if set, only ACLs on this folderid are relevant + * + * @access public + * @return boolean + */ + public function Setup($store, $checkACLonly = false, $folderid = false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->Setup('%s', '%s', '%s')", $store, Utils::PrintAsString($checkACLonly), $folderid)); + if(!is_array($this->backends)){ + return false; + } + foreach ($this->backends as $i => $b){ + $u = $store; + if(isset($this->config['backends'][$i]['users']) && isset($this->config['backends'][$i]['users'][$store]['username'])){ + $u = $this->config['backends'][$i]['users'][$store]['username']; + } + if($this->backends[$i]->Setup($u, $checkACLonly, $folderid) == false){ + ZLog::Write(LOGLEVEL_WARN, "Combined->Setup() failed"); + return false; + } + } + ZLog::Write(LOGLEVEL_INFO, "Combined->Setup() success"); + return true; + } + + /** + * Logs off each backend + * + * @access public + * @return boolean + */ + public function Logoff() { + ZLog::Write(LOGLEVEL_DEBUG, "Combined->Logoff()"); + foreach ($this->backends as $i => $b){ + $this->backends[$i]->Logoff(); + } + ZLog::Write(LOGLEVEL_DEBUG, "Combined->Logoff() success"); + return true; + } + + /** + * Returns an array of SyncFolder types with the entire folder hierarchy + * from all backends combined + * + * provides AS 1.0 compatibility + * + * @access public + * @return array SYNC_FOLDER + */ + public function GetHierarchy(){ + ZLog::Write(LOGLEVEL_DEBUG, "Combined->GetHierarchy()"); + $ha = array(); + foreach ($this->backends as $i => $b){ + if(!empty($this->config['backends'][$i]['subfolder'])){ + $f = new SyncFolder(); + $f->serverid = $i.$this->config['delimiter'].'0'; + $f->parentid = '0'; + $f->displayname = $this->config['backends'][$i]['subfolder']; + $f->type = SYNC_FOLDER_TYPE_OTHER; + $ha[] = $f; + } + $h = $this->backends[$i]->GetHierarchy(); + if(is_array($h)){ + foreach($h as $j => $f){ + $h[$j]->serverid = $i.$this->config['delimiter'].$h[$j]->serverid; + if($h[$j]->parentid != '0' || !empty($this->config['backends'][$i]['subfolder'])){ + $h[$j]->parentid = $i.$this->config['delimiter'].$h[$j]->parentid; + } + if(isset($this->config['folderbackend'][$h[$j]->type]) && $this->config['folderbackend'][$h[$j]->type] != $i){ + $h[$j]->type = SYNC_FOLDER_TYPE_OTHER; + } + } + $ha = array_merge($ha, $h); + } + } + ZLog::Write(LOGLEVEL_DEBUG, "Combined->GetHierarchy() success"); + return $ha; + } + + /** + * Returns the importer to process changes from the mobile + * + * @param string $folderid (opt) + * + * @access public + * @return object(ImportChanges) + */ + public function GetImporter($folderid = false) { + if($folderid !== false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->GetImporter() Content: ImportChangesCombined:('%s')", $folderid)); + + // get the contents importer from the folder in a backend + // the importer is wrapped to check foldernames in the ImportMessageMove function + $backend = $this->GetBackend($folderid); + if($backend === false) + return false; + $importer = $backend->GetImporter($this->GetBackendFolder($folderid)); + if($importer){ + return new ImportChangesCombined($this, $folderid, $importer); + } + return false; + } + else { + ZLog::Write(LOGLEVEL_DEBUG, "Combined->GetImporter() -> Hierarchy: ImportChangesCombined()"); + //return our own hierarchy importer which send each change to the right backend + return new ImportChangesCombined($this); + } + } + + /** + * Returns the exporter to send changes to the mobile + * the exporter from right backend for contents exporter and our own exporter for hierarchy exporter + * + * @param string $folderid (opt) + * + * @access public + * @return object(ExportChanges) + */ + public function GetExporter($folderid = false){ + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->GetExporter('%s')", $folderid)); + if($folderid){ + $backend = $this->GetBackend($folderid); + if($backend == false) + return false; + return $backend->GetExporter($this->GetBackendFolder($folderid)); + } + return new ExportChangesCombined($this); + } + + /** + * Sends an e-mail + * This messages needs to be saved into the 'sent items' folder + * + * @param SyncSendMail $sm SyncSendMail object + * + * @access public + * @return boolean + * @throws StatusException + */ + public function SendMail($sm) { + ZLog::Write(LOGLEVEL_DEBUG, "Combined->SendMail()"); + foreach ($this->backends as $i => $b){ + if($this->backends[$i]->SendMail($sm) == true){ + return true; + } + } + return false; + } + + /** + * Returns all available data of a single message + * + * @param string $folderid + * @param string $id + * @param ContentParameters $contentparameters flag + * + * @access public + * @return object(SyncObject) + * @throws StatusException + */ + public function Fetch($folderid, $id, $contentparameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->Fetch('%s', '%s', CPO)", $folderid, $id)); + $backend = $this->GetBackend($folderid); + if($backend == false) + return false; + return $backend->Fetch($this->GetBackendFolder($folderid), $id, $contentparameters); + } + + /** + * Returns the waste basket + * If the wastebasket is set to one backend, return the wastebasket of that backend + * else return the first waste basket we can find + * + * @access public + * @return string + */ + function GetWasteBasket(){ + ZLog::Write(LOGLEVEL_DEBUG, "Combined->GetWasteBasket()"); + + if (isset($this->activeBackend)) { + if (!$this->activeBackend->GetWasteBasket()) + return false; + else + return $this->activeBackendID . $this->config['delimiter'] . $this->activeBackend->GetWasteBasket(); + } + + return false; + } + + /** + * Returns the content of the named attachment as stream. + * There is no way to tell which backend the attachment is from, so we try them all + * + * @param string $attname + * + * @access public + * @return SyncItemOperationsAttachment + * @throws StatusException + */ + public function GetAttachmentData($attname) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->GetAttachmentData('%s')", $attname)); + foreach ($this->backends as $i => $b) { + try { + $attachment = $this->backends[$i]->GetAttachmentData($attname); + if ($attachment instanceof SyncItemOperationsAttachment) + return $attachment; + } + catch (StatusException $s) { + // backends might throw StatusExceptions if it's not their attachment + } + } + throw new StatusException("Combined->GetAttachmentData(): no backend found", SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + } + + /** + * 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, $error) { + $backend = $this->GetBackend($folderid); + if($backend === false) + return false; + return $backend->MeetingResponse($requestid, $this->GetBackendFolder($folderid), $error); + } + + /** + * Finds the correct backend for a folder + * + * @param string $folderid combinedid of the folder + * + * @access public + * @return object + */ + public function GetBackend($folderid){ + $pos = strpos($folderid, $this->config['delimiter']); + if($pos === false) + return false; + $id = substr($folderid, 0, $pos); + if(!isset($this->backends[$id])) + return false; + + $this->activeBackend = $this->backends[$id]; + $this->activeBackendID = $id; + return $this->backends[$id]; + } + + /** + * Returns an understandable folderid for the backend + * + * @param string $folderid combinedid of the folder + * + * @access public + * @return string + */ + public function GetBackendFolder($folderid){ + $pos = strpos($folderid, $this->config['delimiter']); + if($pos === false) + return false; + return substr($folderid,$pos + strlen($this->config['delimiter'])); + } + + /** + * Returns backend id for a folder + * + * @param string $folderid combinedid of the folder + * + * @access public + * @return object + */ + public function GetBackendId($folderid){ + $pos = strpos($folderid, $this->config['delimiter']); + if($pos === false) + return false; + return substr($folderid,0,$pos); + } +} +?> \ No newline at end of file diff --git a/sources/backend/combined/config.php b/sources/backend/combined/config.php new file mode 100644 index 0000000..452cb5a --- /dev/null +++ b/sources/backend/combined/config.php @@ -0,0 +1,106 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class BackendCombinedConfig { + + // ************************* + // BackendCombined settings + // ************************* + /** + * Returns the configuration of the combined backend + * + * @access public + * @return array + * + */ + public static function GetBackendCombinedConfig() { + //use a function for it because php does not allow + //assigning variables to the class members (expecting T_STRING) + return array( + //the order in which the backends are loaded. + //login only succeeds if all backend return true on login + //sending mail: the mail is sent with first backend that is able to send the mail + 'backends' => array( + 'i' => array( + 'name' => 'BackendIMAP', + ), + 'z' => array( + 'name' => 'BackendZarafa', + ), + 'm' => array( + 'name' => 'BackendMaildir', + ), + 'v' => array( + 'name' => 'BackendVCardDir', + ), + ), + 'delimiter' => '/', + //force one type of folder to one backend + //it must match one of the above defined backends + 'folderbackend' => array( + SYNC_FOLDER_TYPE_INBOX => 'i', + SYNC_FOLDER_TYPE_DRAFTS => 'i', + SYNC_FOLDER_TYPE_WASTEBASKET => 'i', + SYNC_FOLDER_TYPE_SENTMAIL => 'i', + SYNC_FOLDER_TYPE_OUTBOX => 'i', + SYNC_FOLDER_TYPE_TASK => 'z', + SYNC_FOLDER_TYPE_APPOINTMENT => 'z', + SYNC_FOLDER_TYPE_CONTACT => 'z', + SYNC_FOLDER_TYPE_NOTE => 'z', + SYNC_FOLDER_TYPE_JOURNAL => 'z', + SYNC_FOLDER_TYPE_OTHER => 'i', + SYNC_FOLDER_TYPE_USER_MAIL => 'i', + SYNC_FOLDER_TYPE_USER_APPOINTMENT => 'z', + SYNC_FOLDER_TYPE_USER_CONTACT => 'z', + SYNC_FOLDER_TYPE_USER_TASK => 'z', + SYNC_FOLDER_TYPE_USER_JOURNAL => 'z', + SYNC_FOLDER_TYPE_USER_NOTE => 'z', + SYNC_FOLDER_TYPE_UNKNOWN => 'z', + ), + //creating a new folder in the root folder should create a folder in one backend + 'rootcreatefolderbackend' => 'i', + ); + } +} +?> \ No newline at end of file diff --git a/sources/backend/combined/exporter.php b/sources/backend/combined/exporter.php new file mode 100644 index 0000000..43ac379 --- /dev/null +++ b/sources/backend/combined/exporter.php @@ -0,0 +1,184 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +/** + * the ExportChangesCombined class is returned from GetExporter for changes. + * It combines the changes from all backends and prepends all folderids with the backendid + */ + +class ExportChangesCombined implements IExportChanges { + private $backend; + private $syncstates; + private $exporters; + private $importer; + private $importwraps; + + public function ExportChangesCombined(&$backend) { + $this->backend =& $backend; + $this->exporters = array(); + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined constructed"); + } + + /** + * Initializes the state and flags + * + * @param string $state + * @param int $flags + * + * @access public + * @return boolean status flag + */ + public function Config($syncstate, $flags = 0) { + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined->Config(...)"); + $this->syncstates = $syncstate; + if(!is_array($this->syncstates)){ + $this->syncstates = array(); + } + + foreach($this->backend->backends as $i => $b){ + if(isset($this->syncstates[$i])){ + $state = $this->syncstates[$i]; + } else { + $state = ''; + } + + $this->exporters[$i] = $this->backend->backends[$i]->GetExporter(); + $this->exporters[$i]->Config($state, $flags); + } + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined->Config() success"); + } + + /** + * Returns the amount of changes to be exported + * + * @access public + * @return int + */ + public function GetChangeCount() { + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined->GetChangeCount()"); + $c = 0; + foreach($this->exporters as $i => $e){ + $c += $this->exporters[$i]->GetChangeCount(); + } + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined->GetChangeCount() success"); + return $c; + } + + /** + * Synchronizes a change to the configured importer + * + * @access public + * @return array with status information + */ + public function Synchronize() { + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined->Synchronize()"); + foreach($this->exporters as $i => $e){ + if(!empty($this->backend->config['backends'][$i]['subfolder']) && !isset($this->syncstates[$i])){ + // first sync and subfolder backend + $f = new SyncFolder(); + $f->serverid = $i.$this->backend->config['delimiter'].'0'; + $f->parentid = '0'; + $f->displayname = $this->backend->config['backends'][$i]['subfolder']; + $f->type = SYNC_FOLDER_TYPE_OTHER; + $this->importer->ImportFolderChange($f); + } + while(is_array($this->exporters[$i]->Synchronize())); + } + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined->Synchronize() success"); + return true; + } + + /** + * Reads and returns the current state + * + * @access public + * @return string + */ + public function GetState() { + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined->GetState()"); + foreach($this->exporters as $i => $e){ + $this->syncstates[$i] = $this->exporters[$i]->GetState(); + } + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined->GetState() success"); + return $this->syncstates; + } + + /** + * Configures additional parameters used for content synchronization + * + * @param ContentParameters $contentparameters + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ConfigContentParameters($contentparameters) { + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined->ConfigContentParameters()"); + foreach($this->exporters as $i => $e){ + //call the ConfigContentParameters() of each exporter + $e->ConfigContentParameters($contentparameters); + } + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined->ConfigContentParameters() success"); + } + + /** + * Sets the importer where the exporter will sent its changes to + * This exporter should also be ready to accept calls after this + * + * @param object &$importer Implementation of IImportChanges + * + * @access public + * @return boolean + */ + public function InitializeExporter(&$importer) { + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined->InitializeExporter(...)"); + foreach ($this->exporters as $i => $e) { + if(!isset($this->_importwraps[$i])){ + $this->importwraps[$i] = new ImportHierarchyChangesCombinedWrap($i, $this->backend, $importer); + } + $e->InitializeExporter($this->importwraps[$i]); + } + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesCombined->InitializeExporter(...) success"); + } +} +?> \ No newline at end of file diff --git a/sources/backend/combined/importer.php b/sources/backend/combined/importer.php new file mode 100644 index 0000000..2f2f8f9 --- /dev/null +++ b/sources/backend/combined/importer.php @@ -0,0 +1,353 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class ImportChangesCombined implements IImportChanges { + private $backend; + private $folderid; + private $icc; + + /** + * Constructor of the ImportChangesCombined class + * + * @param object $backend + * @param string $folderid + * @param object $importer + * + * @access public + */ + public function ImportChangesCombined(&$backend, $folderid = false, $icc = false) { + $this->backend = $backend; + $this->folderid = $folderid; + $this->icc = &$icc; + } + + /** + * Loads objects which are expected to be exported with the state + * Before importing/saving the actual message from the mobile, a conflict detection should be done + * + * @param ContentParameters $contentparameters class of objects + * @param string $state + * + * @access public + * @return boolean + * @throws StatusException + */ + public function LoadConflicts($contentparameters, $state) { + if (!$this->icc) { + ZLog::Write(LOGLEVEL_ERROR, "ImportChangesCombined->LoadConflicts() icc not configured"); + return false; + } + $this->icc->LoadConflicts($contentparameters, $state); + } + + /** + * Imports a single message + * + * @param string $id + * @param SyncObject $message + * + * @access public + * @return boolean/string failure / id of message + */ + public function ImportMessageChange($id, $message) { + if (!$this->icc) { + ZLog::Write(LOGLEVEL_ERROR, "ImportChangesCombined->ImportMessageChange() icc not configured"); + return false; + } + return $this->icc->ImportMessageChange($id, $message); + } + + /** + * Imports a deletion. This may conflict if the local object has been modified + * + * @param string $id + * + * @access public + * @return boolean + */ + public function ImportMessageDeletion($id) { + if (!$this->icc) { + ZLog::Write(LOGLEVEL_ERROR, "ImportChangesCombined->ImportMessageDeletion() icc not configured"); + return false; + } + return $this->icc->ImportMessageDeletion($id); + } + + /** + * Imports a change in 'read' flag + * This can never conflict + * + * @param string $id + * @param int $flags + * + * @access public + * @return boolean + */ + public function ImportMessageReadFlag($id, $flags) { + if (!$this->icc) { + ZLog::Write(LOGLEVEL_ERROR, "ImportChangesCombined->ImportMessageReadFlag() icc not configured"); + return false; + } + return $this->icc->ImportMessageReadFlag($id, $flags); + } + + /** + * Imports a move of a message. This occurs when a user moves an item to another folder + * + * @param string $id + * @param string $newfolder + * + * @access public + * @return boolean + */ + public function ImportMessageMove($id, $newfolder) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesCombined->ImportMessageMove('%s', '%s')", $id, $newfolder)); + if (!$this->icc) { + ZLog::Write(LOGLEVEL_ERROR, "ImportChangesCombined->ImportMessageMove icc not configured"); + return false; + } + if($this->backend->GetBackendId($this->folderid) != $this->backend->GetBackendId($newfolder)){ + ZLog::Write(LOGLEVEL_WARN, "ImportChangesCombined->ImportMessageMove() cannot move message between two backends"); + return false; + } + return $this->icc->ImportMessageMove($id, $this->backend->GetBackendFolder($newfolder)); + } + + + /**---------------------------------------------------------------------------------------------------------- + * Methods to import hierarchy + */ + + /** + * Imports a change on a folder + * + * @param object $folder SyncFolder + * + * @access public + * @return boolean/string status/id of the folder + */ + public function ImportFolderChange($folder) { + $id = $folder->serverid; + $parent = $folder->parentid; + ZLog::Write(LOGLEVEL_DEBUG, "ImportChangesCombined->ImportFolderChange() ".print_r($folder, 1)); + if($parent == '0') { + if($id) { + $backendid = $this->backend->GetBackendId($id); + } + else { + $backendid = $this->backend->config['rootcreatefolderbackend']; + } + } + else { + $backendid = $this->backend->GetBackendId($parent); + $parent = $this->backend->GetBackendFolder($parent); + } + + if(!empty($this->backend->config['backends'][$backendid]['subfolder']) && $id == $backendid.$this->backend->config['delimiter'].'0') { + ZLog::Write(LOGLEVEL_WARN, "ImportChangesCombined->ImportFolderChange() cannot change static folder"); + return false; + } + + if($id != false) { + if($backendid != $this->backend->GetBackendId($id)) { + ZLog::Write(LOGLEVEL_WARN, "ImportChangesCombined->ImportFolderChange() cannot move folder between two backends"); + return false; + } + $id = $this->backend->GetBackendFolder($id); + } + + $this->icc = $this->backend->getBackend($backendid)->GetImporter(); + $res = $this->icc->ImportFolderChange($folder); + ZLog::Write(LOGLEVEL_DEBUG, 'ImportChangesCombined->ImportFolderChange() success'); + return $backendid.$this->backend->config['delimiter'].$res; + } + + /** + * Imports a folder deletion + * + * @param string $id + * @param string $parent id + * + * @access public + * @return boolean/int success/SYNC_FOLDERHIERARCHY_STATUS + */ + public function ImportFolderDeletion($id, $parent = false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesCombined->ImportFolderDeletion('%s', '%s'), $id, $parent")); + $backendid = $this->backend->GetBackendId($id); + if(!empty($this->backend->config['backends'][$backendid]['subfolder']) && $id == $backendid.$this->backend->config['delimiter'].'0') { + ZLog::Write(LOGLEVEL_WARN, "ImportChangesCombined->ImportFolderDeletion() cannot change static folder"); + return false; //we can not change a static subfolder + } + + $backend = $this->backend->GetBackend($id); + $id = $this->backend->GetBackendFolder($id); + + if($parent != '0') + $parent = $this->backend->GetBackendFolder($parent); + + $this->icc = $backend->GetImporter(); + $res = $this->icc->ImportFolderDeletion($id, $parent); + ZLog::Write(LOGLEVEL_DEBUG, 'ImportChangesCombined->ImportFolderDeletion() success'); + return $res; + } + + + /** + * Initializes the state and flags + * + * @param string $state + * @param int $flags + * + * @access public + * @return boolean status flag + */ + public function Config($state, $flags = 0) { + ZLog::Write(LOGLEVEL_DEBUG, 'ImportChangesCombined->Config(...)'); + if (!$this->icc) { + ZLog::Write(LOGLEVEL_ERROR, "ImportChangesCombined->Config() icc not configured"); + return false; + } + $this->icc->Config($state, $flags); + ZLog::Write(LOGLEVEL_DEBUG, 'ImportChangesCombined->Config() success'); + } + + + /** + * Configures additional parameters used for content synchronization + * + * @param ContentParameters $contentparameters + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ConfigContentParameters($contentparameters) { + ZLog::Write(LOGLEVEL_DEBUG, "ImportChangesCombined->ConfigContentParameters()"); + if (!$this->icc) { + ZLog::Write(LOGLEVEL_ERROR, "ImportChangesCombined->ConfigContentParameters() icc not configured"); + return false; + } + $this->icc->ConfigContentParameters($contentparameters); + ZLog::Write(LOGLEVEL_DEBUG, "ImportChangesCombined->ConfigContentParameters() success"); + } + + /** + * Reads and returns the current state + * + * @access public + * @return string + */ + public function GetState() { + if (!$this->icc) { + ZLog::Write(LOGLEVEL_ERROR, "ImportChangesCombined->GetState() icc not configured"); + return false; + } + return $this->icc->GetState(); + } +} + + +/** + * The ImportHierarchyChangesCombinedWrap class wraps the importer given in ExportChangesCombined->Config. + * It prepends the backendid to all folderids and checks foldertypes. + */ + +class ImportHierarchyChangesCombinedWrap { + private $ihc; + private $backend; + private $backendid; + + /** + * Constructor of the ImportChangesCombined class + * + * @param string $backendid + * @param object $backend + * @param object $ihc + * + * @access public + */ + public function ImportHierarchyChangesCombinedWrap($backendid, &$backend, &$ihc) { + ZLog::Write(LOGLEVEL_DEBUG, "ImportHierarchyChangesCombinedWrap->ImportHierarchyChangesCombinedWrap('$backendid',...)"); + $this->backendid = $backendid; + $this->backend =& $backend; + $this->ihc = &$ihc; + } + + /** + * Imports a change on a folder + * + * @param object $folder SyncFolder + * + * @access public + * @return boolean/string status/id of the folder + */ + public function ImportFolderChange($folder) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportHierarchyChangesCombinedWrap->ImportFolderChange('%s')", $folder->serverid)); + $folder->serverid = $this->backendid.$this->backend->config['delimiter'].$folder->serverid; + if($folder->parentid != '0' || !empty($this->backend->config['backends'][$this->backendid]['subfolder'])){ + $folder->parentid = $this->backendid.$this->backend->config['delimiter'].$folder->parentid; + } + if(isset($this->backend->config['folderbackend'][$folder->type]) && $this->backend->config['folderbackend'][$folder->type] != $this->backendid){ + ZLog::Write(LOGLEVEL_DEBUG, sprintf("not using folder: '%s' ('%s')", $folder->displayname, $folder->serverid)); + return true; + } + ZLog::Write(LOGLEVEL_DEBUG, "ImportHierarchyChangesCombinedWrap->ImportFolderChange() success"); + return $this->ihc->ImportFolderChange($folder); + } + + /** + * Imports a folder deletion + * + * @param string $id + * + * @access public + * + * @return boolean/int success/SYNC_FOLDERHIERARCHY_STATUS + */ + public function ImportFolderDeletion($id) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportHierarchyChangesCombinedWrap->ImportFolderDeletion('%s')", $id)); + return $this->ihc->ImportFolderDeletion($this->backendid.$this->backend->config['delimiter'].$id); + } +} + +?> \ No newline at end of file diff --git a/sources/backend/imap/config.php b/sources/backend/imap/config.php new file mode 100644 index 0000000..234745a --- /dev/null +++ b/sources/backend/imap/config.php @@ -0,0 +1,78 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// ************************ +// BackendIMAP settings +// ************************ + +// Defines the server to which we want to connect +define('IMAP_SERVER', 'localhost'); + +// connecting to default port (143) +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 +// '@mydomain.com' - the username is used and the given string will be appended +define('IMAP_DEFAULTFROM', ''); + +// copy outgoing mail to this folder. If not set z-push will try the default folders +define('IMAP_SENTFOLDER', ''); + +// forward messages inline (default false - as attachment) +define('IMAP_INLINE_FORWARD', false); + +// use imap_mail() to send emails (default) - if false mail() is used +define('IMAP_USE_IMAPMAIL', 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 */ + +?> \ No newline at end of file diff --git a/sources/backend/imap/imap.php b/sources/backend/imap/imap.php new file mode 100644 index 0000000..d9b8160 --- /dev/null +++ b/sources/backend/imap/imap.php @@ -0,0 +1,1838 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// config file +require_once("backend/imap/config.php"); + +include_once('lib/default/diffbackend/diffbackend.php'); +include_once('include/mimeDecode.php'); +require_once('include/z_RFC822.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 */ + + /**---------------------------------------------------------------------------------------------------------- + * default backend methods + */ + + /** + * Authenticates the user + * + * @param string $username + * @param string $domain + * @param string $password + * + * @access public + * @return boolean + * @throws FatalException if php-imap module can not be found + */ + public function Logon($username, $domain, $password) { + $this->wasteID = false; + $this->sentID = false; + $this->server = "{" . IMAP_SERVER . ":" . IMAP_PORT . "/imap" . IMAP_OPTIONS . "}"; + + if (!function_exists("imap_open")) + throw new FatalException("BackendIMAP(): php-imap module is not installed", 0, null, LOGLEVEL_FATAL); + + /* BEGIN fmbiete's contribution r1527, ZP-319 */ + $this->excludedFolders = array(); + if (defined('IMAP_EXCLUDED_FOLDERS') && strlen(IMAP_EXCLUDED_FOLDERS) > 0) { + $this->excludedFolders = explode("|", IMAP_EXCLUDED_FOLDERS); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->Logon(): Excluding Folders (%s)", IMAP_EXCLUDED_FOLDERS)); + } + /* END fmbiete's contribution r1527, ZP-319 */ + + // open the IMAP-mailbox + $this->mbox = @imap_open($this->server , $username, $password, OP_HALFOPEN); + $this->mboxFolder = ""; + + if ($this->mbox) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->Logon(): User '%s' is authenticated on IMAP",$username)); + $this->username = $username; + $this->domain = $domain; + // set serverdelimiter + $this->serverdelimiter = $this->getServerDelimiter(); + return true; + } + else { + ZLog::Write(LOGLEVEL_ERROR, "BackendIMAP->Logon(): can't connect: " . imap_last_error()); + return false; + } + } + + /** + * Logs off + * Called before shutting down the request to close the IMAP connection + * writes errors to the log + * + * @access public + * @return boolean + */ + public function Logoff() { + if ($this->mbox) { + // list all errors + $errors = imap_errors(); + if (is_array($errors)) { + foreach ($errors as $e) { + if (stripos($e, "fail") !== false) { + $level = LOGLEVEL_WARN; + } + else { + $level = LOGLEVEL_DEBUG; + } + ZLog::Write($level, "BackendIMAP->Logoff(): IMAP said: " . $e); + } + } + @imap_close($this->mbox); + ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->Logoff(): IMAP connection closed"); + } + $this->SaveStorages(); + } + + /** + * Sends an e-mail + * This messages needs to be saved into the 'sent items' folder + * + * @param SyncSendMail $sm SyncSendMail object + * + * @access public + * @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), + 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); + + + // 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; + + } + else { + $mobj2 = new Mail_mimeDecode($origmail); + $mess2 = $mobj2->decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')); + + 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 ($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)); + } + else + $body = $nbody; + + + if(isset($mess2->parts)) { + $attached = false; + + 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; + } + + foreach($mess2->parts as $part) { + if(isset($part->disposition) && ($part->disposition == "attachment" || $part->disposition == "inline")) { + + 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"; + + // 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"; + } + + unset($mobj2); + } + + // unset origmail - free memory + unset($origmail); + + } + + // 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); + } + } + + // 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"]}"); + } + 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 ); + } + + // 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."); + } + + return $send; + } + + /** + * Returns the waste basket + * + * @access public + * @return string + */ + public function GetWasteBasket() { + // TODO this could be retrieved from the DeviceFolderCache + if ($this->wasteID == false) { + //try to get the waste basket without doing complete hierarchy sync + $wastebaskt = @imap_getmailboxes($this->mbox, $this->server, "Trash"); + if (isset($wastebaskt[0])) { + $this->wasteID = $this->convertImapId(substr($wastebaskt[0]->name, strlen($this->server))); + return $this->wasteID; + } + //try get waste id from hierarchy if it wasn't possible with above for some reason + $this->GetHierarchy(); + } + return $this->wasteID; + } + + /** + * Returns the content of the named attachment as stream. The passed attachment identifier is + * the exact string that is returned in the 'AttName' property of an SyncAttachment. + * Any information necessary to find the attachment must be encoded in that 'attname' property. + * Data is written directly (with print $data;) + * + * @param string $attname + * + * @access public + * @return SyncItemOperationsAttachment + * @throws StatusException + */ + public function GetAttachmentData($attname) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetAttachmentData('%s')", $attname)); + + list($folderid, $id, $part) = explode(":", $attname); + + if (!$folderid || !$id || !$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); + $mail = @imap_fetchheader($this->mbox, $id, FT_UID) . @imap_body($this->mbox, $id, FT_PEEK | FT_UID); + + $mobj = new Mail_mimeDecode($mail); + $message = $mobj->decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')); + + /* BEGIN fmbiete's contribution r1528, ZP-320 */ + //trying parts + $mparts = $message->parts; + for ($i = 0; $i < count($mparts); $i++) { + $auxpart = $mparts[$i]; + //recursively add parts + if($auxpart->ctype_primary == "multipart" && ($auxpart->ctype_secondary == "mixed" || $auxpart->ctype_secondary == "alternative" || $auxpart->ctype_secondary == "related")) { + foreach($auxpart->parts as $spart) + $mparts[] = $spart; + } + } + /* END fmbiete's contribution r1528, ZP-320 */ + + if (!isset($mparts[$part]->body)) + throw new StatusException(sprintf("BackendIMAP->GetAttachmentData('%s'): Error, requested part key can not be found: '%d'", $attname, $part), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + + // unset mimedecoder & mail + unset($mobj); + unset($mail); + + include_once('include/stringstreamwrapper.php'); + $attachment = new SyncItemOperationsAttachment(); + /* BEGIN fmbiete's contribution r1528, ZP-320 */ + $attachment->data = StringStreamWrapper::Open($mparts[$part]->body); + if (isset($mparts[$part]->ctype_primary) && isset($mparts[$part]->ctype_secondary)) + $attachment->contenttype = $mparts[$part]->ctype_primary .'/'.$mparts[$part]->ctype_secondary; + + unset($mparts); + unset($message); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetAttachmentData contenttype %s", $attachment->contenttype)); + /* END fmbiete's contribution r1528, ZP-320 */ + + return $attachment; + } + + /** + * Indicates if the backend has a ChangesSink. + * A sink is an active notification mechanism which does not need polling. + * The IMAP backend simulates a sink by polling status information of the folder + * + * @access public + * @return boolean + */ + public function HasChangesSink() { + $this->sinkfolders = array(); + $this->sinkstates = array(); + 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 found can not be found + */ + public function ChangesSinkInitialize($folderid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("IMAPBackend->ChangesSinkInitialize(): folderid '%s'", $folderid)); + + $imapid = $this->getImapIdFromFolderId($folderid); + + if ($imapid) { + $this->sinkfolders[] = $imapid; + return true; + } + + return false; + } + + /** + * 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; + + while($stopat > time() && empty($notifications)) { + foreach ($this->sinkfolders as $imapid) { + $this->imap_reopenFolder($imapid); + + // courier-imap only cleares the status cache after checking + @imap_check($this->mbox); + + $status = @imap_status($this->mbox, $this->server . $imapid, SA_ALL); + if (!$status) { + ZLog::Write(LOGLEVEL_WARN, sprintf("ChangesSink: could not stat folder '%s': %s ", $this->getFolderIdFromImapId($imapid), imap_last_error())); + } + else { + $newstate = "M:". $status->messages ."-R:". $status->recent ."-U:". $status->unseen; + + if (! isset($this->sinkstates[$imapid]) ) + $this->sinkstates[$imapid] = $newstate; + + if ($this->sinkstates[$imapid] != $newstate) { + $notifications[] = $this->getFolderIdFromImapId($imapid); + $this->sinkstates[$imapid] = $newstate; + } + } + } + + if (empty($notifications)) + sleep(5); + } + + return $notifications; + } + + + /**---------------------------------------------------------------------------------------------------------- + * implemented DiffBackend methods + */ + + + /** + * Returns a list (array) of folders. + * + * @access public + * @return array/boolean false if the list could not be retrieved + */ + public function GetFolderList() { + $folders = array(); + + $list = @imap_getmailboxes($this->mbox, $this->server, "*"); + if (is_array($list)) { + // reverse list to obtain folders in right order + $list = array_reverse($list); + + foreach ($list as $val) { + /* BEGIN fmbiete's contribution r1527, ZP-319 */ + // don't return the excluded folders + $notExcluded = true; + for ($i = 0, $cnt = count($this->excludedFolders); $notExcluded && $i < $cnt; $i++) { // expr1, expr2 modified by mku ZP-329 + // fix exclude folders with special chars by mku ZP-329 + if (strpos(strtolower($val->name), strtolower(Utils::Utf7_iconv_encode(Utils::Utf8_to_utf7($this->excludedFolders[$i])))) !== false) { + $notExcluded = false; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Pattern: <%s> found, excluding folder: '%s'", $this->excludedFolders[$i], $val->name)); // sprintf added by mku ZP-329 + } + } + + if ($notExcluded) { + $box = array(); + // cut off serverstring + $imapid = substr($val->name, strlen($this->server)); + $box["id"] = $this->convertImapId($imapid); + + $fhir = explode($val->delimiter, $imapid); + if (count($fhir) > 1) { + $this->getModAndParentNames($fhir, $box["mod"], $imapparent); + $box["parent"] = $this->convertImapId($imapparent); + } + else { + $box["mod"] = $imapid; + $box["parent"] = "0"; + } + $folders[]=$box; + /* END fmbiete's contribution r1527, ZP-319 */ + } + } + } + else { + ZLog::Write(LOGLEVEL_WARN, "BackendIMAP->GetFolderList(): imap_list failed: " . imap_last_error()); + return false; + } + + return $folders; + } + + /** + * Returns an actual SyncFolder object + * + * @param string $id id of the folder + * + * @access public + * @return object SyncFolder with information + */ + public function GetFolder($id) { + $folder = new SyncFolder(); + $folder->serverid = $id; + + // convert back to work on an imap-id + $imapid = $this->getImapIdFromFolderId($id); + + // explode hierarchy + $fhir = explode($this->serverdelimiter, $imapid); + + // compare on lowercase strings + $lid = strtolower($imapid); +// TODO WasteID or SentID could be saved for later ussage + if($lid == "inbox") { + $folder->parentid = "0"; // Root + $folder->displayname = "Inbox"; + $folder->type = SYNC_FOLDER_TYPE_INBOX; + } + // Zarafa IMAP-Gateway outputs + else if($lid == "drafts") { + $folder->parentid = "0"; + $folder->displayname = "Drafts"; + $folder->type = SYNC_FOLDER_TYPE_DRAFTS; + } + else if($lid == "trash") { + $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) { + $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") { + $folder->parentid = $this->convertImapId($fhir[0]); + $folder->displayname = "Drafts"; + $folder->type = SYNC_FOLDER_TYPE_DRAFTS; + } + else if($lid == "inbox.trash" || $lid == "inbox/trash") { + $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") { + $folder->parentid = $this->convertImapId($fhir[0]); + $folder->displayname = "Sent"; + $folder->type = SYNC_FOLDER_TYPE_SENTMAIL; + $this->sentID = $id; + } + + // define the rest as other-folders + else { + if (count($fhir) > 1) { + $this->getModAndParentNames($fhir, $folder->displayname, $imapparent); + $folder->parentid = $this->convertImapId($imapparent); + $folder->displayname = Utils::Utf7_to_utf8(Utils::Utf7_iconv_decode($folder->displayname)); + } + else { + $folder->displayname = Utils::Utf7_to_utf8(Utils::Utf7_iconv_decode($imapid)); + $folder->parentid = "0"; + } + $folder->type = SYNC_FOLDER_TYPE_USER_MAIL; + } + + //advanced debugging + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetFolder('%s'): '%s'", $id, $folder)); + + return $folder; + } + + /** + * 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) { + $folder = $this->GetFolder($id); + + $stat = array(); + $stat["id"] = $id; + $stat["parent"] = $folder->parentid; + $stat["mod"] = $folder->displayname; + + return $stat; + } + + /** + * Creates or modifies a folder + * The folder type is ignored in IMAP, as all folders are Email folders + * + * @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){ + ZLog::Write(LOGLEVEL_INFO, sprintf("BackendIMAP->ChangeFolder('%s','%s','%s','%s')", $folderid, $oldid, $displayname, $type)); + + // go to parent mailbox + $this->imap_reopenFolder($folderid); + + // build name for new mailboxBackendMaildir + $displayname = Utils::Utf7_iconv_encode(Utils::Utf8_to_utf7($displayname)); + $newname = $this->server . $folderid . $this->serverdelimiter . $displayname; + + $csts = false; + // if $id is set => rename mailbox, otherwise create + if ($oldid) { + // rename doesn't work properly with IMAP + // the activesync client doesn't support a 'changing ID' + // TODO this would be solved by implementing hex ids (Mantis #459) + //$csts = imap_renamemailbox($this->mbox, $this->server . imap_utf7_encode(str_replace(".", $this->serverdelimiter, $oldid)), $newname); + } + else { + $csts = @imap_createmailbox($this->mbox, $newname); + } + if ($csts) { + return $this->StatFolder($folderid . $this->serverdelimiter . $displayname); + } + else + return false; + } + + /** + * Deletes a folder + * + * @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){ + // TODO implement + 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("BackendIMAP->GetMessageList('%s','%s')", $folderid, $cutoffdate)); + + $folderid = $this->getImapIdFromFolderId($folderid); + + if ($folderid == false) + throw new StatusException("Folderid not found in cache", SYNC_STATUS_FOLDERHIERARCHYCHANGED); + + $messages = array(); + $this->imap_reopenFolder($folderid, true); + + $sequence = "1:*"; + if ($cutoffdate > 0) { + $search = @imap_search($this->mbox, "SINCE ". date("d-M-Y", $cutoffdate)); + if ($search !== false) + $sequence = implode(",", $search); + } + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessageList(): searching with sequence '%s'", $sequence)); + $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())); + return $messages; + } + + foreach($overviews as $overview) { + $date = ""; + $vars = get_object_vars($overview); + if (array_key_exists( "date", $vars)) { + // message is out of range for cutoffdate, ignore it + if ($this->cleanupDate($overview->date) < $cutoffdate) continue; + $date = $overview->date; + } + + // cut of deleted messages + if (array_key_exists("deleted", $vars) && $overview->deleted) + continue; + + if (array_key_exists("uid", $vars)) { + $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) + $message["flags"] = 1; + + array_push($messages, $message); + } + } + 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) { + $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation()); + $mimesupport = $contentparameters->GetMimeSupport(); + $bodypreference = $contentparameters->GetBodyPreference(); /* fmbiete's contribution r1528, ZP-320 */ + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage('%s','%s')", $folderid, $id)); + + $folderImapid = $this->getImapIdFromFolderId($folderid); + + // Get flags, etc + $stat = $this->StatMessage($folderid, $id); + + if ($stat) { + $this->imap_reopenFolder($folderImapid); + $mail = @imap_fetchheader($this->mbox, $id, FT_UID) . @imap_body($this->mbox, $id, FT_PEEK | FT_UID); + + $mobj = new Mail_mimeDecode($mail); + $message = $mobj->decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')); + + /* BEGIN fmbiete's contribution r1528, ZP-320 */ + $output = new SyncMail(); + + //Select body type preference + $bpReturnType = SYNC_BODYPREFERENCE_PLAIN; + if ($bodypreference !== false) { + $bpReturnType = Utils::GetBodyPreferenceBestMatch($bodypreference); // changed by mku ZP-330 + } + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage - getBodyPreferenceBestMatch: %d", $bpReturnType)); + + //Get body data + $this->getBodyRecursive($message, "plain", $plainBody); + $this->getBodyRecursive($message, "html", $htmlBody); + if ($plainBody == "") { + $plainBody = Utils::ConvertHtmlToText($htmlBody); + } + $htmlBody = str_replace("\n","\r\n", str_replace("\r","",$htmlBody)); + $plainBody = str_replace("\n","\r\n", str_replace("\r","",$plainBody)); + + if (Request::GetProtocolVersion() >= 12.0) { + $output->asbody = new SyncBaseBody(); + + switch($bpReturnType) { + case SYNC_BODYPREFERENCE_PLAIN: + $output->asbody->data = $plainBody; + break; + case SYNC_BODYPREFERENCE_HTML: + if ($htmlBody == "") { + $output->asbody->data = $plainBody; + $bpReturnType = SYNC_BODYPREFERENCE_PLAIN; + } + else { + $output->asbody->data = $htmlBody; + } + break; + case SYNC_BODYPREFERENCE_MIME: + //We don't need to create a new MIME mail, we already have one!! + $output->asbody->data = $mail; + 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) { + $output->asbody->data = Utils::Utf8_truncate($output->asbody->data, $truncsize); + $output->asbody->truncated = 1; + } + + $output->asbody->type = $bpReturnType; + $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; + } + $output->mimesize = strlen($output->mimedata); + } + else { + // truncate body, if requested + if (strlen($plainBody) > $truncsize) { + $output->body = Utils::Utf8_truncate($plainBody, $truncsize); + $output->bodytruncated = 1; + } + else { + $output->body = $plainBody; + $output->bodytruncated = 0; + } + $output->bodysize = strlen($output->body); + } + /* END fmbiete's contribution r1528, ZP-320 */ + } + + $output->datereceived = isset($message->headers["date"]) ? $this->cleanupDate($message->headers["date"]) : null; + $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; + + /* BEGIN fmbiete's contribution r1528, ZP-320 */ + if (isset($message->headers["thread-topic"])) { + $output->threadtopic = $message->headers["thread-topic"]; + } + + // 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"; + } + /* END fmbiete's contribution r1528, ZP-320 */ + + $Mail_RFC822 = new Mail_RFC822(); + $toaddr = $ccaddr = $replytoaddr = array(); + if(isset($message->headers["to"])) + $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"]); + + $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); + else { + if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') { + $fulladdr = "\"" . w2u($name) ."\" <" . w2u($address) . ">"; + } + else { + $fulladdr = w2u($name) ." <" . w2u($address) . ">"; + } + } + + array_push($output->$type, $fulladdr); + } + } + + // convert mime-importance to AS-importance + if (isset($message->headers["x-priority"])) { + $mimeImportance = preg_replace("/\D+/", "", $message->headers["x-priority"]); + //MAIL 1 - most important, 3 - normal, 5 - lowest + //AS 0 - low, 1 - normal, 2 - important + if ($mimeImportance > 3) + $output->importance = 0; + if ($mimeImportance == 3) + $output->importance = 1; + if ($mimeImportance < 3) + $output->importance = 2; + } 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)) { + $mparts = $message->parts; + for ($i=0; $ictype_primary == "multipart" && ($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(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(); + + $attachment = new SyncBaseAttachment(); + + $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; + } + else { + $attachment->isinline = 0; + } + + array_push($output->asattachments, $attachment); + } + 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 */ + } + } + } + // unset mimedecoder & mail + unset($mobj); + unset($mail); + return $output; + } + + return false; + } + + /** + * 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/boolean + */ + public function StatMessage($folderid, $id) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->StatMessage('%s','%s')", $folderid, $id)); + $folderImapid = $this->getImapIdFromFolderId($folderid); + + $this->imap_reopenFolder($folderImapid); + $overview = @imap_fetch_overview( $this->mbox , $id , FT_UID); + + if (!$overview) { + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->StatMessage('%s','%s'): Failed to retrieve overview: %s", $folderid, $id, imap_last_error())); + return false; + } + + // check if variables for this overview object are available + $vars = get_object_vars($overview[0]); + + // without uid it's not a valid message + if (! array_key_exists( "uid", $vars)) return false; + + $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) + $entry["flags"] = 1; + + return $entry; + } + + /** + * Called when a message has been changed on the mobile. + * Added support for FollowUp flag + * + * @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("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_reopenFolder($folderImapid); + + 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 ($status) { + ZLog::Write(LOGLEVEL_DEBUG, "Flagged changed"); + } + else { + ZLog::Write(LOGLEVEL_DEBUG, "Flagged failed"); + } + } + + return $this->StatMessage($folderid, $id); + /* END fmbiete's contribution r1529, ZP-321 */ + } + + /** + * Changes the 'read' 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 SetReadFlag($folderid, $id, $flags, $contentParameters) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SetReadFlag('%s','%s','%s')", $folderid, $id, $flags)); + $folderImapid = $this->getImapIdFromFolderId($folderid); + + // 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 + + $this->imap_reopenFolder($folderImapid); + + 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); + } + + return $status; + } + + /** + * 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("BackendIMAP->DeleteMessage('%s','%s')", $folderid, $id)); + $folderImapid = $this->getImapIdFromFolderId($folderid); + + // 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 + + $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); + } + + /** + * Called when the user moves an item on the PDA from one folder to another + * + * @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) { + 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 . ""; + } + } + + + /**---------------------------------------------------------------------------------------------------------- + * protected IMAP methods + */ + + /** + * Unmasks a hex folderid and returns the imap folder id + * + * @param string $folderid hex folderid generated by convertImapId() + * + * @access protected + * @return string imap folder id + */ + protected function getImapIdFromFolderId($folderid) { + $this->InitializePermanentStorage(); + + if (isset($this->permanentStorage->fmFidFimap)) { + if (isset($this->permanentStorage->fmFidFimap[$folderid])) { + $imapId = $this->permanentStorage->fmFidFimap[$folderid]; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getImapIdFromFolderId('%s') = %s", $folderid, $imapId)); + return $imapId; + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getImapIdFromFolderId('%s') = %s", $folderid, 'not found')); + return false; + } + } + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->getImapIdFromFolderId('%s') = %s", $folderid, 'not initialized!')); + return false; + } + + /** + * Retrieves a hex folderid previousily masked imap + * + * @param string $imapid Imap folder id + * + * @access protected + * @return string hex folder id + */ + protected function getFolderIdFromImapId($imapid) { + $this->InitializePermanentStorage(); + + if (isset($this->permanentStorage->fmFimapFid)) { + if (isset($this->permanentStorage->fmFimapFid[$imapid])) { + $folderid = $this->permanentStorage->fmFimapFid[$imapid]; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFolderIdFromImapId('%s') = %s", $imapid, $folderid)); + return $folderid; + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFolderIdFromImapId('%s') = %s", $imapid, 'not found')); + return false; + } + } + ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->getFolderIdFromImapId('%s') = %s", $imapid, 'not initialized!')); + return false; + } + + /** + * Masks a imap folder id into a generated hex folderid + * The method getFolderIdFromImapId() is consulted so that an + * imapid always returns the same hex folder id + * + * @param string $imapid Imap folder id + * + * @access protected + * @return string hex folder id + */ + protected function convertImapId($imapid) { + $this->InitializePermanentStorage(); + + // check if this imap id was converted before + $folderid = $this->getFolderIdFromImapId($imapid); + + // nothing found, so generate a new id and put it in the cache + if (!$folderid) { + // generate folderid and add it to the mapping + $folderid = sprintf('%04x%04x', mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )); + + // folderId to folderImap mapping + if (!isset($this->permanentStorage->fmFidFimap)) + $this->permanentStorage->fmFidFimap = array(); + + $a = $this->permanentStorage->fmFidFimap; + $a[$folderid] = $imapid; + $this->permanentStorage->fmFidFimap = $a; + + // folderImap to folderid mapping + if (!isset($this->permanentStorage->fmFimapFid)) + $this->permanentStorage->fmFimapFid = array(); + + $b = $this->permanentStorage->fmFimapFid; + $b[$imapid] = $folderid; + $this->permanentStorage->fmFimapFid = $b; + } + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->convertImapId('%s') = %s", $imapid, $folderid)); + + 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 + * + * @access protected + * @return string delimiter + */ + protected function getServerDelimiter() { + $list = @imap_getmailboxes($this->mbox, $this->server, "*"); + if (is_array($list)) { + $val = $list[0]; + + return $val->delimiter; + } + return "."; // default "." + } + + /** + * Helper to re-initialize the folder to speed things up + * Remember what folder is currently open and only change if necessary + * + * @param string $folderid id of the folder + * @param boolean $force re-open the folder even if currently opened + * + * @access protected + * @return + */ + protected function imap_reopenFolder($folderid, $force = false) { + // 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())); + return false; + } + $this->mboxFolder = $folderid; + } + } + + + /** + * Build a multipart RFC822, embedding body and one file (for attachments) + * + * @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 + * + * @access protected + * @return array with [0] => $mail_header and [1] => $mail_body + */ + protected function mail_attach($filenm,$filesize,$file_cont,$body, $body_ct, $body_cte, $boundary = false) { + if (!$boundary) $boundary = strtoupper(md5(uniqid(time()))); + + //remove the ending boundary because we will add it at the end + $body = str_replace("--$boundary--", "", $body); + + $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); + } + + /** + * Helper for mail_attach() + * + * @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 + */ + 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"; + + return $mail_body; + } + + /** + * 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; + } + + /** + * Adds a message with seen flag to a specified folder (used for saving sent items) + * + * @param string $folderid id of the folder + * @param string $header header of the message + * @param long $body body of the message + * + * @access protected + * @return boolean status + */ + protected function addSentMessage($folderid, $header, $body) { + $header_body = str_replace("\n", "\r\n", str_replace("\r", "", $header . "\n\n" . $body)); + + return @imap_append($this->mbox, $this->server . $folderid, $header_body, "\\Seen"); + } + + /** + * Parses an mimedecode address array back to a simple "," separated string + * + * @param array $ad addresses array + * + * @access protected + * @return string mail address(es) string + */ + protected 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; + } + + /** + * Recursive way to get mod and parent - repeat until only one part is left + * or the folder is identified as an IMAP folder + * + * @param string $fhir folder hierarchy string + * @param string &$displayname reference of the displayname + * @param long &$parent reference of the parent folder + * + * @access protected + * @return + */ + protected function getModAndParentNames($fhir, &$displayname, &$parent) { + // if mod is already set add the previous part to it as it might be a folder which has + // delimiter in its name + $displayname = (isset($displayname) && strlen($displayname) > 0) ? $displayname = array_pop($fhir).$this->serverdelimiter.$displayname : array_pop($fhir); + $parent = implode($this->serverdelimiter, $fhir); + + if (count($fhir) == 1 || $this->checkIfIMAPFolder($parent)) { + return; + } + //recursion magic + $this->getModAndParentNames($fhir, $displayname, $parent); + } + + /** + * Checks if a specified name is a folder in the IMAP store + * + * @param string $foldername a foldername + * + * @access protected + * @return boolean + */ + protected function checkIfIMAPFolder($folderName) { + $parent = imap_list($this->mbox, $this->server, $folderName); + if ($parent === false) return false; + return true; + } + + /** + * Removes parenthesis (comments) from the date string because + * strtotime returns false if received date has them + * + * @param string $receiveddate a date as a string + * + * @access protected + * @return string + */ + protected function cleanupDate($receiveddate) { + $receiveddate = strtotime(preg_replace("/\(.*\)/", "", $receiveddate)); + if ($receiveddate == false || $receiveddate == -1) { + debugLog("Received date is false. Message might be broken."); + return null; + } + + return $receiveddate; + } + + /* BEGIN fmbiete's contribution r1528, ZP-320 */ + /** + * Indicates which AS version is supported by the backend. + * + * @access public + * @return string AS version constant + */ + public function GetSupportedASVersion() { + return ZPush::ASV_14; + } + /* END fmbiete's contribution r1528, ZP-320 */ +}; + +?> \ No newline at end of file diff --git a/sources/backend/maildir/config.php b/sources/backend/maildir/config.php new file mode 100644 index 0000000..43d0675 --- /dev/null +++ b/sources/backend/maildir/config.php @@ -0,0 +1,51 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// ************************ +// BackendMaildir settings +// ************************ + +define('MAILDIR_BASE', '/tmp'); +define('MAILDIR_SUBDIR', 'Maildir'); + +?> \ No newline at end of file diff --git a/sources/backend/maildir/maildir.php b/sources/backend/maildir/maildir.php new file mode 100644 index 0000000..a3c337d --- /dev/null +++ b/sources/backend/maildir/maildir.php @@ -0,0 +1,723 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// config file +require_once("backend/maildir/config.php"); + +include_once('lib/default/diffbackend/diffbackend.php'); +include_once('include/mimeDecode.php'); +require_once('include/z_RFC822.php'); + +class BackendMaildir extends BackendDiff { + /**---------------------------------------------------------------------------------------------------------- + * default backend methods + */ + + /** + * 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) { + return true; + } + + /** + * Logs off + * + * @access public + * @return boolean + */ + public function Logoff() { + 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 + * + * @access public + * @return string + */ + public function GetWasteBasket() { + return false; + } + + /** + * Returns the content of the named attachment as stream. The passed attachment identifier is + * the exact string that is returned in the 'AttName' property of an SyncAttachment. + * Any information necessary to find the attachment must be encoded in that 'attname' property. + * Data is written directly (with print $data;) + * + * @param string $attname + * + * @access public + * @return SyncItemOperationsAttachment + * @throws StatusException + */ + public function GetAttachmentData($attname) { + list($id, $part) = explode(":", $attname); + + $fn = $this->findMessage($id); + if ($fn == false) + throw new StatusException(sprintf("BackendMaildir->GetAttachmentData('%s'): Error, requested message/attachment can not be found", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + + // Parse e-mail + $rfc822 = file_get_contents($this->getPath() . "/$fn"); + + $message = Mail_mimeDecode::decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'input' => $rfc822, 'crlf' => "\n", 'charset' => 'utf-8')); + + include_once('include/stringstreamwrapper.php'); + $attachment = new SyncItemOperationsAttachment(); + $attachment->data = StringStreamWrapper::Open($message->parts[$part]->body); + if (isset($message->parts[$part]->ctype_primary) && isset($message->parts[$part]->ctype_secondary)) + $attachment->contenttype = $message->parts[$part]->ctype_primary .'/'.$message->parts[$part]->ctype_secondary; + + return $attachment; + } + + /**---------------------------------------------------------------------------------------------------------- + * 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() { + $folders = array(); + + $inbox = array(); + $inbox["id"] = "root"; + $inbox["parent"] = "0"; + $inbox["mod"] = "Inbox"; + + $folders[]=$inbox; + + $sub = array(); + $sub["id"] = "sub"; + $sub["parent"] = "root"; + $sub["mod"] = "Sub"; + +// $folders[]=$sub; + + return $folders; + } + + /** + * Returns an actual SyncFolder object + * + * @param string $id id of the folder + * + * @access public + * @return object SyncFolder with information + */ + public function GetFolder($id) { + if($id == "root") { + $inbox = new SyncFolder(); + + $inbox->serverid = $id; + $inbox->parentid = "0"; // Root + $inbox->displayname = "Inbox"; + $inbox->type = SYNC_FOLDER_TYPE_INBOX; + + return $inbox; + } else if($id == "sub") { + $inbox = new SyncFolder(); + $inbox->serverid = $id; + $inbox->parentid = "root"; + $inbox->displayname = "Sub"; + $inbox->type = SYNC_FOLDER_TYPE_OTHER; + + return $inbox; + } else { + return false; + } + } + + + /** + * 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) { + $folder = $this->GetFolder($id); + + $stat = array(); + $stat["id"] = $id; + $stat["parent"] = $folder->parentid; + $stat["mod"] = $folder->displayname; + + return $stat; + } + + + /** + * Creates or modifies a folder + * not implemented + * + * @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 + * + * @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) { + $this->moveNewToCur(); + + if($folderid != "root") + return false; + + // return stats of all messages in a dir. We can do this faster than + // just calling statMessage() on each message; We still need fstat() + // information though, so listing 10000 messages is going to be + // rather slow (depending on filesystem, etc) + + // we also have to filter by the specified cutoffdate so only the + // last X days are retrieved. Normally, this would mean that we'd + // have to open each message, get the Received: header, and check + // whether that is in the filter range. Because this is much too slow, we + // are depending on the creation date of the message instead, which should + // normally be just about the same, unless you just did some kind of import. + + $messages = array(); + $dirname = $this->getPath(); + + $dir = opendir($dirname); + + if(!$dir) + return false; + + while($entry = readdir($dir)) { + if($entry{0} == ".") + continue; + + $message = array(); + + $stat = stat("$dirname/$entry"); + + if($stat["mtime"] < $cutoffdate) { + // message is out of range for curoffdate, ignore it + continue; + } + + $message["mod"] = $stat["mtime"]; + + $matches = array(); + + // Flags according to http://cr.yp.to/proto/maildir.html (pretty authoritative - qmail author's website) + if(!preg_match("/([^:]+):2,([PRSTDF]*)/",$entry,$matches)) + continue; + $message["id"] = $matches[1]; + $message["flags"] = 0; + + if(strpos($matches[2],"S") !== false) { + $message["flags"] |= 1; // 'seen' aka 'read' is the only flag we want to know about + } + + array_push($messages, $message); + } + + 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, $truncsize, $mimesupport = 0) { + if($folderid != 'root') + return false; + + $fn = $this->findMessage($id); + + // Get flags, etc + $stat = $this->StatMessage($folderid, $id); + + // Parse e-mail + $rfc822 = file_get_contents($this->getPath() . "/" . $fn); + + $message = Mail_mimeDecode::decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'input' => $rfc822, 'crlf' => "\n", 'charset' => 'utf-8')); + + $output = new SyncMail(); + + $output->body = str_replace("\n", "\r\n", $this->getBody($message)); + $output->bodysize = strlen($output->body); + $output->bodytruncated = 0; // We don't implement truncation in this backend + $output->datereceived = $this->parseReceivedDate($message->headers["received"][0]); + $output->messageclass = "IPM.Note"; + $output->subject = $message->headers["subject"]; + $output->read = $stat["flags"]; + $output->from = $message->headers["from"]; + + $Mail_RFC822 = new Mail_RFC822(); + $toaddr = $ccaddr = $replytoaddr = array(); + if(isset($message->headers["to"])) + $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"]); + + $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); + else { + if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') { + $fulladdr = "\"" . w2u($name) ."\" <" . w2u($address) . ">"; + } + else { + $fulladdr = w2u($name) ." <" . w2u($address) . ">"; + } + } + + array_push($output->$type, $fulladdr); + } + } + + // convert mime-importance to AS-importance + if (isset($message->headers["x-priority"])) { + $mimeImportance = preg_replace("/\D+/", "", $message->headers["x-priority"]); + if ($mimeImportance > 3) + $output->importance = 0; + if ($mimeImportance == 3) + $output->importance = 1; + if ($mimeImportance < 3) + $output->importance = 2; + } + + // Attachments are only searched in the top-level part + $n = 0; + if(isset($message->parts)) { + foreach($message->parts as $part) { + if($part->ctype_primary == "application") { + $attachment = new SyncAttachment(); + $attachment->attsize = strlen($part->body); + + 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"; + + $attachment->displayname = $attname; + $attachment->attname = $id . ":" . $n; + $attachment->attmethod = 1; + $attachment->attoid = isset($part->headers['content-id']) ? $part->headers['content-id'] : ""; + + array_push($output->attachments, $attachment); + } + $n++; + } + } + + return $output; + } + + /** + * 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) { + $dirname = $this->getPath(); + $fn = $this->findMessage($id); + if(!$fn) + return false; + + $stat = stat("$dirname/$fn"); + + $entry = array(); + $entry["id"] = $id; + $entry["flags"] = 0; + + if(strpos($fn,"S")) + $entry["flags"] |= 1; + $entry["mod"] = $stat["mtime"]; + + return $entry; + } + + /** + * 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) { + // 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 + return false; + } + + /** + * Changes the 'read' 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 SetReadFlag($folderid, $id, $flags, $contentParameters) { + if($folderid != 'root') + return false; + + // 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 + + $fn = $this->findMessage($id); + + if(!$fn) + return true; // message may have been deleted + + if(!preg_match("/([^:]+):2,([PRSTDF]*)/",$fn,$matches)) + return false; + + // remove 'seen' (S) flag + if(!$flags) { + $newflags = str_replace("S","",$matches[2]); + } else { + // make sure we don't double add the 'S' flag + $newflags = str_replace("S","",$matches[2]) . "S"; + } + + $newfn = $matches[1] . ":2," . $newflags; + // rename if required + if($fn != $newfn) + rename($this->getPath() ."/$fn", $this->getPath() . "/$newfn"); + + return true; + } + + /** + * 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) { + if($folderid != 'root') + return false; + + // 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 + + $fn = $this->findMessage($id); + + if(!$fn) + return true; // success because message has been deleted already + + if(!unlink($this->getPath() . "/$fn")) { + return true; // success - message may have been deleted in the mean time (since findMessage) + } + + return true; + } + + /** + * Called when the user moves an item on the PDA from one folder to another + * not implemented + * + * @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; + } + + + /**---------------------------------------------------------------------------------------------------------- + * private maildir-specific internals + */ + + /** + * Searches for the message + * + * @param string $id id of the message + * + * @access private + * @return string + */ + private function findMessage($id) { + // We could use 'this->folderid' for path info but we currently + // only support a single INBOX. We also have to use a glob '*' + // because we don't know the flags of the message we're looking for. + + $dirname = $this->getPath(); + $dir = opendir($dirname); + + while($entry = readdir($dir)) { + if(strpos($entry,$id) === 0) + return $entry; + } + return false; // not found + } + + /** + * Parses the message and return only the plaintext body + * + * @param string $message html message + * + * @access private + * @return string plaintext message + */ + private function getBody($message) { + $body = ""; + $htmlbody = ""; + + $this->getBodyRecursive($message, "plain", $body); + + if(!isset($body) || $body === "") { + $this->getBodyRecursive($message, "html", $body); + // remove css-style tags + $body = preg_replace("//is", "", $body); + // remove all other html + $body = strip_tags($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 private + * @return + */ + private 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); + } + } + } + } + + /** + * Parses the received date + * + * @param string $received received date string + * + * @access private + * @return long + */ + private function parseReceivedDate($received) { + $pos = strpos($received, ";"); + if(!$pos) + return false; + + $datestr = substr($received, $pos+1); + $datestr = ltrim($datestr); + + return strtotime($datestr); + } + + /** + * Moves everything in Maildir/new/* to Maildir/cur/ + * + * @access private + * @return + */ + private function moveNewToCur() { + $newdirname = MAILDIR_BASE . "/" . $this->store . "/" . MAILDIR_SUBDIR . "/new"; + + $newdir = opendir($newdirname); + + while($newentry = readdir($newdir)) { + if($newentry{0} == ".") + continue; + + // link/unlink == move. This is the way to move the message according to cr.yp.to + link($newdirname . "/" . $newentry, $this->getPath() . "/" . $newentry . ":2,"); + unlink($newdirname . "/" . $newentry); + } + } + + /** + * The path we're working on + * + * @access private + * @return string + */ + private function getPath() { + return MAILDIR_BASE . "/" . $this->store . "/" . MAILDIR_SUBDIR . "/cur"; + } +} + +?> \ No newline at end of file diff --git a/sources/backend/searchldap/config.php b/sources/backend/searchldap/config.php new file mode 100644 index 0000000..439cfac --- /dev/null +++ b/sources/backend/searchldap/config.php @@ -0,0 +1,75 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// LDAP host and port +define("LDAP_HOST", "ldap://127.0.0.1/"); +define("LDAP_PORT", "389"); + +// Set USER and PASSWORD if not using anonymous bind +define("ANONYMOUS_BIND", true); +define("LDAP_BIND_USER", "cn=searchuser,dc=test,dc=net"); +define("LDAP_BIND_PASSWORD", ""); + +// Search base & filter +// the SEARCHVALUE string is substituded by the value inserted into the search field +define("LDAP_SEARCH_BASE", "ou=global,dc=test,dc=net"); +define("LDAP_SEARCH_FILTER", "(|(cn=*SEARCHVALUE*)(mail=*SEARCHVALUE*))"); + +// LDAP field mapping. +// values correspond to an inetOrgPerson class +global $ldap_field_map; +$ldap_field_map = array( + SYNC_GAL_DISPLAYNAME => 'cn', + SYNC_GAL_PHONE => 'telephonenumber', + SYNC_GAL_OFFICE => '', + SYNC_GAL_TITLE => 'title', + SYNC_GAL_COMPANY => 'ou', + SYNC_GAL_ALIAS => 'uid', + SYNC_GAL_FIRSTNAME => 'givenname', + SYNC_GAL_LASTNAME => 'sn', + SYNC_GAL_HOMEPHONE => 'homephone', + SYNC_GAL_MOBILEPHONE => 'mobile', + SYNC_GAL_EMAILADDRESS => 'mail', + ); +?> \ No newline at end of file diff --git a/sources/backend/searchldap/searchldap.php b/sources/backend/searchldap/searchldap.php new file mode 100644 index 0000000..f908271 --- /dev/null +++ b/sources/backend/searchldap/searchldap.php @@ -0,0 +1,198 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +require_once("backend/searchldap/config.php"); + +class BackendSearchLDAP implements ISearchProvider { + private $connection; + + /** + * Initializes the backend to perform the search + * Connects to the LDAP server using the values from the configuration + * + * + * @access public + * @return + * @throws StatusException + */ + public function BackendSearchLDAP() { + if (!function_exists("ldap_connect")) + throw new StatusException("BackendSearchLDAP(): php-ldap is not installed. Search aborted.", SYNC_SEARCHSTATUS_STORE_SERVERERROR, null, LOGLEVEL_FATAL); + + // connect to LDAP + $this->connection = @ldap_connect(LDAP_HOST, LDAP_PORT); + @ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, 3); + + // Authenticate + if (constant('ANONYMOUS_BIND') === true) { + if(! @ldap_bind($this->connection)) { + $this->connection = false; + throw new StatusException("BackendSearchLDAP(): Could not bind anonymously to server! Search aborted.", SYNC_SEARCHSTATUS_STORE_CONNECTIONFAILED, null, LOGLEVEL_ERROR); + } + } + else if (constant('LDAP_BIND_USER') != "") { + if(! @ldap_bind($this->connection, LDAP_BIND_USER, LDAP_BIND_PASSWORD)) { + $this->connection = false; + throw new StatusException(sprintf("BackendSearchLDAP(): Could not bind to server with user '%s' and specified password! Search aborted.", LDAP_BIND_USER), SYNC_SEARCHSTATUS_STORE_ACCESSDENIED, null, LOGLEVEL_ERROR); + } + } + else { + // it would be possible to use the users login and password to authenticate on the LDAP server + // the main $backend has to keep these values so they could be used here + $this->connection = false; + throw new StatusException("BackendSearchLDAP(): neither anonymous nor default bind enabled. Other options not implemented.", SYNC_SEARCHSTATUS_STORE_CONNECTIONFAILED, null, LOGLEVEL_ERROR); + } + } + + /** + * 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) { + return ($searchtype == ISearchProvider::SEARCH_GAL); + } + + + /** + * 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) { + global $ldap_field_map; + if (isset($this->connection) && $this->connection !== false) { + $searchfilter = str_replace("SEARCHVALUE", $searchquery, LDAP_SEARCH_FILTER); + $result = @ldap_search($this->connection, LDAP_SEARCH_BASE, $searchfilter); + if (!$result) { + ZLog::Write(LOGLEVEL_ERROR, "BackendSearchLDAP: Error in search query. Search aborted"); + return false; + } + + // get entry data as array + $searchresult = ldap_get_entries($this->connection, $result); + + // 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 = $searchresult['count']; + //do not return more results as requested in range + $querylimit = (($rangeend + 1) < $querycnt) ? ($rangeend + 1) : $querycnt; + $items['range'] = $rangestart.'-'.($querycnt-1); + $items['searchtotal'] = $querycnt; + + $rc = 0; + for ($i = $rangestart; $i < $querylimit; $i++) { + foreach ($ldap_field_map as $key=>$value ) { + if (isset($searchresult[$i][$value])) { + if (is_array($searchresult[$i][$value])) + $items[$rc][$key] = $searchresult[$i][$value][0]; + else + $items[$rc][$key] = $searchresult[$i][$value]; + } + } + $rc++; + } + + return $items; + } + else + return false; + } + + /** + * Searches for the emails on the server + * + * @param ContentParameter $cpo + * + * @return array + */ + public function GetMailboxSearchResults($cpo) { + return array(); + } + + /** + * Terminates a search for a given PID + * + * @param int $pid + * + * @return boolean + */ + public function TerminateSearch($pid) { + return true; + } + + /** + * Disconnects from LDAP + * + * @access public + * @return boolean + */ + public function Disconnect() { + if ($this->connection) + @ldap_close($this->connection); + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/backend/vcarddir/config.php b/sources/backend/vcarddir/config.php new file mode 100644 index 0000000..cc9032f --- /dev/null +++ b/sources/backend/vcarddir/config.php @@ -0,0 +1,50 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// ********************** +// BackendVCardDir settings +// ********************** + +define('VCARDDIR_DIR', '/home/%u/.kde/share/apps/kabc/stdvcf'); + +?> \ No newline at end of file diff --git a/sources/backend/vcarddir/vcarddir.php b/sources/backend/vcarddir/vcarddir.php new file mode 100644 index 0000000..b1d8a0a --- /dev/null +++ b/sources/backend/vcarddir/vcarddir.php @@ -0,0 +1,681 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// config file +require_once("backend/vcarddir/config.php"); + +include_once('lib/default/diffbackend/diffbackend.php'); + +class BackendVCardDir extends BackendDiff { + /**---------------------------------------------------------------------------------------------------------- + * default backend methods + */ + + /** + * 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) { + return true; + } + + /** + * Logs off + * + * @access public + * @return boolean + */ + public function Logoff() { + 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 + * + * @access public + * @return string + */ + public function GetWasteBasket() { + return false; + } + + /** + * Returns the content of the named attachment as stream + * not implemented + * + * @param string $attname + * + * @access public + * @return SyncItemOperationsAttachment + * @throws StatusException + */ + public function GetAttachmentData($attname) { + return false; + } + + /**---------------------------------------------------------------------------------------------------------- + * 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, 'VCDir::GetFolderList()'); + $contacts = array(); + $folder = $this->StatFolder("root"); + $contacts[] = $folder; + + return $contacts; + } + + /** + * 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, 'VCDir::GetFolder('.$id.')'); + if($id == "root") { + $folder = new SyncFolder(); + $folder->serverid = $id; + $folder->parentid = "0"; + $folder->displayname = "Contacts"; + $folder->type = SYNC_FOLDER_TYPE_CONTACT; + + return $folder; + } else return false; + } + + /** + * 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, 'VCDir::StatFolder('.$id.')'); + $folder = $this->GetFolder($id); + + $stat = array(); + $stat["id"] = $id; + $stat["parent"] = $folder->parentid; + $stat["mod"] = $folder->displayname; + + return $stat; + } + + /** + * Creates or modifies a folder + * not implemented + * + * @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 + * + * @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, 'VCDir::GetMessageList('.$folderid.')'); + $messages = array(); + + $dir = opendir($this->getPath()); + if(!$dir) + return false; + + while($entry = readdir($dir)) { + if(is_dir($this->getPath() .'/'.$entry)) + continue; + + $message = array(); + $message["id"] = $entry; + $stat = stat($this->getPath() .'/'.$entry); + $message["mod"] = $stat["mtime"]; + $message["flags"] = 1; // always 'read' + + $messages[] = $message; + } + + 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, 'VCDir::GetMessage('.$folderid.', '.$id.', ..)'); + if($folderid != "root") + return; + + $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 = file_get_contents($this->getPath() . "/" . $id); + $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 = w2ui($adr['val'][2]); + } + if(!empty($adr['val'][3])){ + $b=$a.'city'; + $message->$b = w2ui($adr['val'][3]); + } + if(!empty($adr['val'][4])){ + $b=$a.'state'; + $message->$b = w2ui($adr['val'][4]); + } + if(!empty($adr['val'][5])){ + $b=$a.'postalcode'; + $message->$b = w2ui($adr['val'][5]); + } + if(!empty($adr['val'][6])){ + $b=$a.'country'; + $message->$b = w2ui($adr['val'][6]); + } + } + } + + if(!empty($vcard['fn'][0]['val'][0])) + $message->fileas = w2ui($vcard['fn'][0]['val'][0]); + if(!empty($vcard['n'][0]['val'][0])) + $message->lastname = w2ui($vcard['n'][0]['val'][0]); + if(!empty($vcard['n'][0]['val'][1])) + $message->firstname = w2ui($vcard['n'][0]['val'][1]); + if(!empty($vcard['n'][0]['val'][2])) + $message->middlename = w2ui($vcard['n'][0]['val'][2]); + if(!empty($vcard['n'][0]['val'][3])) + $message->title = w2ui($vcard['n'][0]['val'][3]); + if(!empty($vcard['n'][0]['val'][4])) + $message->suffix = w2ui($vcard['n'][0]['val'][4]); + 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 = w2ui($vcard['org'][0]['val'][0]); + if(!empty($vcard['note'][0]['val'][0])){ + $message->body = w2ui($vcard['note'][0]['val'][0]); + $message->bodysize = strlen($vcard['note'][0]['val'][0]); + $message->bodytruncated = 0; + } + if(!empty($vcard['role'][0]['val'][0])) + $message->jobtitle = w2ui($vcard['role'][0]['val'][0]);//$vcard['title'][0]['val'][0] + if(!empty($vcard['url'][0]['val'][0])) + $message->webpage = w2ui($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; + } + + /** + * 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, 'VCDir::StatMessage('.$folderid.', '.$id.')'); + if($folderid != "root") + return false; + + $stat = stat($this->getPath() . "/" . $id); + + $message = array(); + $message["mod"] = $stat["mtime"]; + $message["id"] = $id; + $message["flags"] = 1; + + 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, 'VCDir::ChangeMessage('.$folderid.', '.$id.', ..)'); + $mapping = array( + 'fileas' => 'FN', + 'lastname;firstname;middlename;title;suffix' => 'N', + 'email1address' => 'EMAIL;INTERNET', + 'email2address' => 'EMAIL;INTERNET', + 'email3address' => 'EMAIL;INTERNET', + 'businessphonenumber' => 'TEL;WORK', + 'business2phonenumber' => 'TEL;WORK', + 'businessfaxnumber' => 'TEL;WORK;FAX', + 'homephonenumber' => 'TEL;HOME', + 'home2phonenumber' => 'TEL;HOME', + 'homefaxnumber' => 'TEL;HOME;FAX', + 'mobilephonenumber' => 'TEL;CELL', + 'carphonenumber' => 'TEL;CAR', + 'pagernumber' => 'TEL;PAGER', + ';;businessstreet;businesscity;businessstate;businesspostalcode;businesscountry' => 'ADR;WORK', + ';;homestreet;homecity;homestate;homepostalcode;homecountry' => 'ADR;HOME', + ';;otherstreet;othercity;otherstate;otherpostalcode;othercountry' => 'ADR', + 'companyname' => 'ORG', + 'body' => 'NOTE', + 'jobtitle' => 'ROLE', + 'webpage' => 'URL', + ); + $data = "BEGIN:VCARD\nVERSION:2.1\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(empty($val)) + continue; + $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(',', $this->escape($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"; + +// not supported: anniversary, assistantname, assistnamephonenumber, children, department, officelocation, radiophonenumber, spouse, rtf + + if(!$id){ + if(!empty($message->fileas)){ + $name = u2wi($message->fileas); + }elseif(!empty($message->lastname)){ + $name = $name = u2wi($message->lastname); + }elseif(!empty($message->firstname)){ + $name = $name = u2wi($message->firstname); + }elseif(!empty($message->companyname)){ + $name = $name = u2wi($message->companyname); + }else{ + $name = 'unknown'; + } + $name = preg_replace('/[^a-z0-9 _-]/i', '', $name); + $id = $name.'.vcf'; + $i = 0; + while(file_exists($this->getPath().'/'.$id)){ + $i++; + $id = $name.$i.'.vcf'; + } + } + file_put_contents($this->getPath().'/'.$id, $data); + return $this->StatMessage($folderid, $id); + } + + /** + * Changes the 'read' 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 SetReadFlag($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) { + return unlink($this->getPath() . '/' . $id); + } + + /** + * Called when the user moves an item on the PDA from one folder to another + * not implemented + * + * @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; + } + + + /**---------------------------------------------------------------------------------------------------------- + * private vcard-specific internals + */ + + /** + * The path we're working on + * + * @access private + * @return string + */ + private function getPath() { + return str_replace('%u', $this->store, VCARDDIR_DIR); + } + + /** + * Escapes a string + * + * @param string $data string to be escaped + * + * @access private + * @return string + */ + 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 u2wi($data); + } + + /** + * Un-escapes a string + * + * @param string $data string to be un-escaped + * + * @access private + * @return string + */ + function unescape($data){ + $data = str_replace(array('\\\\', '\\;', '\\,', '\\n','\\N'),array('\\', ';', ',', "\n", "\n"),$data); + return $data; + } +}; +?> \ No newline at end of file diff --git a/sources/backend/zarafa/config.php b/sources/backend/zarafa/config.php new file mode 100644 index 0000000..e0a441c --- /dev/null +++ b/sources/backend/zarafa/config.php @@ -0,0 +1,51 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// ************************ +// BackendZarafa settings +// ************************ + +// Defines the server to which we want to connect +define('MAPI_SERVER', 'file:///var/run/zarafa'); + +?> \ No newline at end of file diff --git a/sources/backend/zarafa/exporter.php b/sources/backend/zarafa/exporter.php new file mode 100644 index 0000000..31eaa78 --- /dev/null +++ b/sources/backend/zarafa/exporter.php @@ -0,0 +1,298 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +/** + * This is our ICS exporter which requests the actual exporter from ICS and makes sure + * that the ImportProxies are used. + */ + +class ExportChangesICS implements IExportChanges{ + private $folderid; + private $store; + private $session; + private $restriction; + private $contentparameters; + private $flags; + private $exporterflags; + private $exporter; + + /** + * Constructor + * + * @param mapisession $session + * @param mapistore $store + * @param string (opt) + * + * @access public + * @throws StatusException + */ + public function ExportChangesICS($session, $store, $folderid = false) { + // Open a hierarchy or a contents exporter depending on whether a folderid was specified + $this->session = $session; + $this->folderid = $folderid; + $this->store = $store; + $this->restriction = false; + + try { + if($folderid) { + $entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid); + } + else { + $storeprops = mapi_getprops($this->store, array(PR_IPM_SUBTREE_ENTRYID)); + $entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID]; + } + + $folder = false; + if ($entryid) + $folder = mapi_msgstore_openentry($this->store, $entryid); + + // Get the actual ICS exporter + if($folderid) { + if ($folder) + $this->exporter = mapi_openproperty($folder, PR_CONTENTS_SYNCHRONIZER, IID_IExchangeExportChanges, 0 , 0); + else + $this->exporter = false; + } + else { + $this->exporter = mapi_openproperty($folder, PR_HIERARCHY_SYNCHRONIZER, IID_IExchangeExportChanges, 0 , 0); + } + } + catch (MAPIException $me) { + $this->exporter = false; + // We return the general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12) + // if this happened while doing content sync, the mobile will try to resync the folderhierarchy + throw new StatusException(sprintf("ExportChangesICS('%s','%s','%s'): Error, unable to open folder: 0x%X", $session, $store, Utils::PrintAsString($folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN); + } + } + + /** + * Configures the exporter + * + * @param string $state + * @param int $flags + * + * @access public + * @return boolean + * @throws StatusException + */ + public function Config($state, $flags = 0) { + $this->exporterflags = 0; + $this->flags = $flags; + + // this should never happen + if ($this->exporter === false || is_array($state)) + throw new StatusException("ExportChangesICS->Config(): Error, exporter not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR); + + // change exporterflags if we are doing a ContentExport + if($this->folderid) { + $this->exporterflags |= SYNC_NORMAL | SYNC_READ_STATE; + + // Initial sync, we don't want deleted items. If the initial sync is chunked + // we check the change ID of the syncstate (0 at initial sync) + // On subsequent syncs, we do want to receive delete events. + if(strlen($state) == 0 || bin2hex(substr($state,4,4)) == "00000000") { + if (!($this->flags & BACKEND_DISCARD_DATA)) + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesICS->Config(): synching inital data"); + $this->exporterflags |= SYNC_NO_SOFT_DELETIONS | SYNC_NO_DELETIONS; + } + } + + if($this->flags & BACKEND_DISCARD_DATA) + $this->exporterflags |= SYNC_CATCHUP; + + // Put the state information in a stream that can be used by ICS + $stream = mapi_stream_create(); + if(strlen($state) == 0) + $state = hex2bin("0000000000000000"); + + if (!($this->flags & BACKEND_DISCARD_DATA)) + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExportChangesICS->Config() initialized with state: 0x%s", bin2hex($state))); + + mapi_stream_write($stream, $state); + $this->statestream = $stream; + } + + /** + * Configures additional parameters used for content synchronization + * + * @param ContentParameters $contentparameters + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ConfigContentParameters($contentparameters){ + $filtertype = $contentparameters->GetFilterType(); + switch($contentparameters->GetContentClass()) { + case "Email": + $this->restriction = ($filtertype || !Utils::CheckMapiExtVersion('7')) ? MAPIUtils::GetEmailRestriction(Utils::GetCutOffDate($filtertype)) : false; + break; + case "Calendar": + $this->restriction = ($filtertype || !Utils::CheckMapiExtVersion('7')) ? MAPIUtils::GetCalendarRestriction($this->store, Utils::GetCutOffDate($filtertype)) : false; + break; + default: + case "Contacts": + case "Tasks": + $this->restriction = false; + break; + } + + $this->contentParameters = $contentparameters; + } + + + /** + * Sets the importer the exporter will sent it's changes to + * and initializes the Exporter + * + * @param object &$importer Implementation of IImportChanges + * + * @access public + * @return boolean + * @throws StatusException + */ + public function InitializeExporter(&$importer) { + // Because we're using ICS, we need to wrap the given importer to make it suitable to pass + // to ICS. We do this in two steps: first, wrap the importer with our own PHP importer class + // which removes all MAPI dependency, and then wrap that class with a C++ wrapper so we can + // pass it to ICS + + // this should never happen! + if($this->exporter === false || !isset($this->statestream) || !isset($this->flags) || !isset($this->exporterflags) || + ($this->folderid && !isset($this->contentParameters)) ) + throw new StatusException("ExportChangesICS->InitializeExporter(): Error, exporter or essential data not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR); + + // PHP wrapper + $phpwrapper = new PHPWrapper($this->session, $this->store, $importer); + + // with a folderid we are going to get content + if($this->folderid) { + $phpwrapper->ConfigContentParameters($this->contentParameters); + + // ICS c++ wrapper + $mapiimporter = mapi_wrap_importcontentschanges($phpwrapper); + $includeprops = false; + } + else { + $mapiimporter = mapi_wrap_importhierarchychanges($phpwrapper); + $includeprops = array(PR_SOURCE_KEY, PR_DISPLAY_NAME); + } + + if (!$mapiimporter) + throw new StatusException(sprintf("ExportChangesICS->InitializeExporter(): Error, mapi_wrap_import_*_changes() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN); + + $ret = mapi_exportchanges_config($this->exporter, $this->statestream, $this->exporterflags, $mapiimporter, $this->restriction, $includeprops, false, 1); + if(!$ret) + throw new StatusException(sprintf("ExportChangesICS->InitializeExporter(): Error, mapi_exportchanges_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN); + + $changes = mapi_exportchanges_getchangecount($this->exporter); + if($changes || !($this->flags & BACKEND_DISCARD_DATA)) + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExportChangesICS->InitializeExporter() successfully. %d changes ready to sync.", $changes)); + + return $ret; + } + + + /** + * Reads the current state from the Exporter + * + * @access public + * @return string + * @throws StatusException + */ + public function GetState() { + $error = false; + if(!isset($this->statestream) || $this->exporter === false) + $error = true; + + if($error === true || mapi_exportchanges_updatestate($this->exporter, $this->statestream) != true ) + throw new StatusException(sprintf("ExportChangesICS->GetState(): Error, state not available or unable to update: 0x%X", mapi_last_hresult()), (($this->folderid)?SYNC_STATUS_FOLDERHIERARCHYCHANGED:SYNC_FSSTATUS_CODEUNKNOWN), null, LOGLEVEL_WARN); + + mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET); + + $state = ""; + while(true) { + $data = mapi_stream_read($this->statestream, 4096); + if(strlen($data)) + $state .= $data; + else + break; + } + + return $state; + } + + /** + * Returns the amount of changes to be exported + * + * @access public + * @return int + */ + public function GetChangeCount() { + if ($this->exporter) + return mapi_exportchanges_getchangecount($this->exporter); + else + return 0; + } + + /** + * Synchronizes a change + * + * @access public + * @return array + */ + public function Synchronize() { + if ($this->exporter) { + return mapi_exportchanges_synchronize($this->exporter); + } + return false; + } +} +?> \ No newline at end of file diff --git a/sources/backend/zarafa/icalparser.php b/sources/backend/zarafa/icalparser.php new file mode 100644 index 0000000..09d2949 --- /dev/null +++ b/sources/backend/zarafa/icalparser.php @@ -0,0 +1,201 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class ICalParser{ + private $props; + + /** + * Constructor + * + * @param mapistore $store + * @param array &$props properties to be set + * + * @access public + */ + public function ICalParser(&$store, &$props){ + $this->props = $props; + } + + /** + * Function reads calendar part and puts mapi properties into an array. + * + * @param string $ical the ical data + * @param array &$mapiprops mapi properties + * + * @access public + */ + public function ExtractProps($ical, &$mapiprops) { + //mapping between partstat in ical and MAPI Meeting Response classes as well as icons + $aClassMap = array( + "ACCEPTED" => array("class" => "IPM.Schedule.Meeting.Resp.Pos", "icon" => 0x405), + "DECLINED" => array("class" => "IPM.Schedule.Meeting.Resp.Neg", "icon" => 0x406), + "TENTATIVE" => array("class" => "IPM.Schedule.Meeting.Resp.Tent", "icon" => 0x407), + "NEEDS-ACTION" => array("class" => "IPM.Schedule.Meeting.Request", "icon" => 0x404), //iphone + "REQ-PARTICIPANT" => array("class" => "IPM.Schedule.Meeting.Request", "icon" => 0x404), //nokia + ); + + $aical = preg_split("/[\n]/", $ical); + $elemcount = count($aical); + $i=0; + $nextline = $aical[0]; + + //last element is empty + while ($i < $elemcount - 1) { + $line = $nextline; + $nextline = $aical[$i+1]; + + //if a line starts with a space or a tab it belongs to the previous line + while (strlen($nextline) > 0 && ($nextline{0} == " " || $nextline{0} == "\t")) { + $line = rtrim($line) . substr($nextline, 1); + $nextline = $aical[++$i + 1]; + } + $line = rtrim($line); + + switch (strtoupper($line)) { + case "BEGIN:VCALENDAR": + case "BEGIN:VEVENT": + case "END:VEVENT": + case "END:VCALENDAR": + break; + default: + unset ($field, $data, $prop_pos, $property); + if (preg_match ("/([^:]+):(.*)/", $line, $line)){ + $field = $line[1]; + $data = $line[2]; + $property = $field; + $prop_pos = strpos($property,';'); + if ($prop_pos !== false) $property = substr($property, 0, $prop_pos); + $property = strtoupper($property); + + switch ($property) { + case 'DTSTART': + $data = $this->getTimestampFromStreamerDate($data); + $mapiprops[$this->props["starttime"]] = $mapiprops[$this->props["commonstart"]] = $mapiprops[$this->props["clipstart"]] = $mapiprops[PR_START_DATE] = $data; + break; + + case 'DTEND': + $data = $this->getTimestampFromStreamerDate($data); + $mapiprops[$this->props["endtime"]] = $mapiprops[$this->props["commonend"]] = $mapiprops[$this->props["recurrenceend"]] = $mapiprops[PR_END_DATE] = $data; + break; + + case 'UID': + $mapiprops[$this->props["goidtag"]] = $mapiprops[$this->props["goid2tag"]] = Utils::GetOLUidFromICalUid($data); + break; + + case 'ATTENDEE': + $fields = explode(";", $field); + foreach ($fields as $field) { + $prop_pos = strpos($field, '='); + if ($prop_pos !== false) { + switch (substr($field, 0, $prop_pos)) { + case 'PARTSTAT' : $partstat = substr($field, $prop_pos+1); break; + case 'CN' : $cn = substr($field, $prop_pos+1); break; + case 'ROLE' : $role = substr($field, $prop_pos+1); break; + case 'RSVP' : $rsvp = substr($field, $prop_pos+1); break; + } + } + } + if (isset($partstat) && isset($aClassMap[$partstat]) && + + (!isset($mapiprops[PR_MESSAGE_CLASS]) || $mapiprops[PR_MESSAGE_CLASS] == "IPM.Schedule.Meeting.Request")) { + $mapiprops[PR_MESSAGE_CLASS] = $aClassMap[$partstat]['class']; + $mapiprops[PR_ICON_INDEX] = $aClassMap[$partstat]['icon']; + } + // START ADDED dw2412 to support meeting requests on HTC Android Mail App + elseif (isset($role) && isset($aClassMap[$role]) && + (!isset($mapiprops[PR_MESSAGE_CLASS]) || $mapiprops[PR_MESSAGE_CLASS] == "IPM.Schedule.Meeting.Request")) { + $mapiprops[PR_MESSAGE_CLASS] = $aClassMap[$role]['class']; + $mapiprops[PR_ICON_INDEX] = $aClassMap[$role]['icon']; + } + // END ADDED dw2412 to support meeting requests on HTC Android Mail App + if (!isset($cn)) $cn = ""; + $data = str_replace ("MAILTO:", "", $data); + $attendee[] = array ('name' => stripslashes($cn), 'email' => stripslashes($data)); + break; + + case 'ORGANIZER': + $field = str_replace("ORGANIZER;CN=", "", $field); + $data = str_replace ("MAILTO:", "", $data); + $organizer[] = array ('name' => stripslashes($field), 'email' => stripslashes($data)); + break; + + case 'LOCATION': + $data = str_replace("\\n", "
", $data); + $data = str_replace("\\t", " ", $data); + $data = str_replace("\\r", "
", $data); + $data = stripslashes($data); + $mapiprops[$this->props["tneflocation"]] = $mapiprops[$this->props["location"]] = $data; + break; + } + } + break; + } + $i++; + + } + $mapiprops[$this->props["usetnef"]] = true; + } + + /** + * Converts an YYYYMMDDTHHMMSSZ kind of string into an unixtimestamp + * + * @param string $data + * + * @access private + * @return long + */ + private function getTimestampFromStreamerDate ($data) { + $data = str_replace('Z', '', $data); + $data = str_replace('T', '', $data); + + preg_match ('/([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{0,2})([0-9]{0,2})([0-9]{0,2})/', $data, $regs); + if ($regs[1] < 1970) { + $regs[1] = '1971'; + } + return gmmktime($regs[4], $regs[5], $regs[6], $regs[2], $regs[3], $regs[1]); + } +} + +?> \ No newline at end of file diff --git a/sources/backend/zarafa/importer.php b/sources/backend/zarafa/importer.php new file mode 100644 index 0000000..ac9579a --- /dev/null +++ b/sources/backend/zarafa/importer.php @@ -0,0 +1,691 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + + +/** + * This is our local importer. Tt receives data from the PDA, for contents and hierarchy changes. + * It must therefore receive the incoming data and convert it into MAPI objects, and then send + * them to the ICS importer to do the actual writing of the object. + * The creation of folders is fairly trivial, because folders that are created on + * the PDA are always e-mail folders. + */ + +class ImportChangesICS implements IImportChanges { + private $folderid; + private $store; + private $session; + private $flags; + private $statestream; + private $importer; + private $memChanges; + private $mapiprovider; + private $conflictsLoaded; + private $conflictsContentParameters; + private $conflictsState; + private $cutoffdate; + private $contentClass; + + /** + * Constructor + * + * @param mapisession $session + * @param mapistore $store + * @param string $folderid (opt) + * + * @access public + * @throws StatusException + */ + public function ImportChangesICS($session, $store, $folderid = false) { + $this->session = $session; + $this->store = $store; + $this->folderid = $folderid; + $this->conflictsLoaded = false; + $this->cutoffdate = false; + $this->contentClass = false; + + if ($folderid) { + $entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid); + } + else { + $storeprops = mapi_getprops($store, array(PR_IPM_SUBTREE_ENTRYID)); + $entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID]; + } + + $folder = false; + if ($entryid) + $folder = mapi_msgstore_openentry($store, $entryid); + + if(!$folder) { + $this->importer = false; + + // We throw an general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12) + // if this happened while doing content sync, the mobile will try to resync the folderhierarchy + throw new StatusException(sprintf("ImportChangesICS('%s','%s','%s'): Error, unable to open folder: 0x%X", $session, $store, Utils::PrintAsString($folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN); + } + + $this->mapiprovider = new MAPIProvider($this->session, $this->store); + + if ($folderid) + $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportContentsChanges, 0 , 0); + else + $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportHierarchyChanges, 0 , 0); + } + + /** + * Initializes the importer + * + * @param string $state + * @param int $flags + * + * @access public + * @return boolean + * @throws StatusException + */ + public function Config($state, $flags = 0) { + $this->flags = $flags; + + // this should never happen + if ($this->importer === false) + throw new StatusException("ImportChangesICS->Config(): Error, importer not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR); + + // Put the state information in a stream that can be used by ICS + $stream = mapi_stream_create(); + if(strlen($state) == 0) + $state = hex2bin("0000000000000000"); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->Config(): initializing importer with state: 0x%s", bin2hex($state))); + + mapi_stream_write($stream, $state); + $this->statestream = $stream; + + if ($this->folderid !== false) { + // possible conflicting messages will be cached here + $this->memChanges = new ChangesMemoryWrapper(); + $stat = mapi_importcontentschanges_config($this->importer, $stream, $flags); + } + else + $stat = mapi_importhierarchychanges_config($this->importer, $stream, $flags); + + if (!$stat) + throw new StatusException(sprintf("ImportChangesICS->Config(): Error, mapi_import_*_changes_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN); + return $stat; + } + + /** + * Configures additional parameters for content selection + * + * @param ContentParameters $contentparameters + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ConfigContentParameters($contentparameters) { + $filtertype = $contentparameters->GetFilterType(); + switch($contentparameters->GetContentClass()) { + case "Email": + $this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false; + break; + case "Calendar": + $this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false; + break; + default: + case "Contacts": + case "Tasks": + $this->cutoffdate = false; + break; + } + $this->contentClass = $contentparameters->GetContentClass(); + } + + /** + * Reads state from the Importer + * + * @access public + * @return string + * @throws StatusException + */ + public function GetState() { + $error = false; + if(!isset($this->statestream) || $this->importer === false) + $error = true; + + if ($error === false && $this->folderid !== false && function_exists("mapi_importcontentschanges_updatestate")) + if(mapi_importcontentschanges_updatestate($this->importer, $this->statestream) != true) + $error = true; + + if ($error == true) + throw new StatusException(sprintf("ImportChangesICS->GetState(): Error, state not available or unable to update: 0x%X", mapi_last_hresult()), (($this->folderid)?SYNC_STATUS_FOLDERHIERARCHYCHANGED:SYNC_FSSTATUS_CODEUNKNOWN), null, LOGLEVEL_WARN); + + mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET); + + $state = ""; + while(true) { + $data = mapi_stream_read($this->statestream, 4096); + if(strlen($data)) + $state .= $data; + else + break; + } + + return $state; + } + + /** + * Checks if a message is in the synchronization interval (window) + * if a filter (e.g. Sync items two weeks back) or limits this synchronization. + * These checks only apply to Emails and Appointments only, Contacts, Tasks and Notes do not have time restrictions. + * + * @param string $messageid the message id to be checked + * + * @access private + * @return boolean + */ + private function isMessageInSyncInterval($messageid) { + // if there is no restriciton we do not need to check + if ($this->cutoffdate === false) + return true; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->isMessageInSyncInterval('%s'): cut off date is: %s", $messageid, $this->cutoffdate)); + + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($messageid)); + if(!$entryid) { + ZLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isMessageInSyncInterval('%s'): Error, unable to resolve message id: 0x%X", $messageid, mapi_last_hresult())); + return false; + } + + $mapimessage = mapi_msgstore_openentry($this->store, $entryid); + if(!$mapimessage) { + ZLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isMessageInSyncInterval('%s'): Error, unable to open entry id: 0x%X", $messageid, mapi_last_hresult())); + return false; + } + + if ($this->contentClass == "Email") + return MAPIUtils::IsInEmailSyncInterval($this->store, $mapimessage, $this->cutoffdate); + elseif ($this->contentClass == "Calendar") + return MAPIUtils::IsInCalendarSyncInterval($this->store, $mapimessage, $this->cutoffdate); + + return true; + } + + /**---------------------------------------------------------------------------------------------------------- + * Methods for ContentsExporter + */ + + /** + * Loads objects which are expected to be exported with the state + * Before importing/saving the actual message from the mobile, a conflict detection should be done + * + * @param ContentParameters $contentparameters class of objects + * @param string $state + * + * @access public + * @return boolean + * @throws StatusException + */ + public function LoadConflicts($contentparameters, $state) { + if (!isset($this->session) || !isset($this->store) || !isset($this->folderid)) + throw new StatusException("ImportChangesICS->LoadConflicts(): Error, can not load changes for conflict detection. Session, store or folder information not available", SYNC_STATUS_SERVERERROR); + + // save data to load changes later if necessary + $this->conflictsLoaded = false; + $this->conflictsContentParameters = $contentparameters; + $this->conflictsState = $state; + + ZLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->LoadConflicts(): will be loaded later if necessary"); + return true; + } + + /** + * Potential conflicts are only loaded when really necessary, + * e.g. on ADD or MODIFY + * + * @access private + * @return + */ + private function lazyLoadConflicts() { + if (!isset($this->session) || !isset($this->store) || !isset($this->folderid) || + !isset($this->conflictsContentParameters) || $this->conflictsState === false) { + ZLog::Write(LOGLEVEL_WARN, "ImportChangesICS->lazyLoadConflicts(): can not load potential conflicting changes in lazymode for conflict detection. Missing information"); + return false; + } + + if (!$this->conflictsLoaded) { + ZLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->lazyLoadConflicts(): loading.."); + + // configure an exporter so we can detect conflicts + $exporter = new ExportChangesICS($this->session, $this->store, $this->folderid); + $exporter->Config($this->conflictsState); + $exporter->ConfigContentParameters($this->conflictsContentParameters); + $exporter->InitializeExporter($this->memChanges); + + // monitor how long it takes to export potential conflicts + // if this takes "too long" we cancel this operation! + $potConflicts = $exporter->GetChangeCount(); + $started = time(); + $exported = 0; + while(is_array($exporter->Synchronize())) { + $exported++; + + // stop if this takes more than 15 seconds and there are more than 5 changes still to be exported + // within 20 seconds this should be finished or it will not be performed + if ((time() - $started) > 15 && ($potConflicts - $exported) > 5 ) { + ZLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): conflict detection cancelled as operation is too slow. In %d seconds only %d from %d changes were processed.",(time() - $started), $exported, $potConflicts)); + $this->conflictsLoaded = true; + return; + } + } + $this->conflictsLoaded = true; + } + } + + /** + * Imports a single message + * + * @param string $id + * @param SyncObject $message + * + * @access public + * @return boolean/string - failure / id of message + * @throws StatusException + */ + public function ImportMessageChange($id, $message) { + $parentsourcekey = $this->folderid; + if($id) + $sourcekey = hex2bin($id); + + $flags = 0; + $props = array(); + $props[PR_PARENT_SOURCE_KEY] = $parentsourcekey; + + // set the PR_SOURCE_KEY if available or mark it as new message + if($id) { + $props[PR_SOURCE_KEY] = $sourcekey; + + // on editing an existing message, check if it is in the synchronization interval + if (!$this->isMessageInSyncInterval($id)) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Message is outside the sync interval. Data not saved.", $id, get_class($message)), SYNC_STATUS_SYNCCANNOTBECOMPLETED); + + // check for conflicts + $this->lazyLoadConflicts(); + if($this->memChanges->IsChanged($id)) { + if ($this->flags & SYNC_CONFLICT_OVERWRITE_PIM) { + // in these cases the status SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT should be returned, so the mobile client can inform the end user + throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from PIM will be dropped! Server overwrites PIM. User is informed.", $id, get_class($message)), SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT, null, LOGLEVEL_INFO); + return false; + } + else + ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from Server will be dropped! PIM overwrites server.", $id, get_class($message))); + } + if($this->memChanges->IsDeleted($id)) { + ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from PIM will be dropped! Object was deleted on server.", $id, get_class($message))); + return false; + } + } + else + $flags = SYNC_NEW_MESSAGE; + + if(mapi_importcontentschanges_importmessagechange($this->importer, $props, $flags, $mapimessage)) { + $this->mapiprovider->SetMessage($mapimessage, $message); + mapi_message_savechanges($mapimessage); + + if (mapi_last_hresult()) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error, mapi_message_savechanges() failed: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_SYNCCANNOTBECOMPLETED); + + $sourcekeyprops = mapi_getprops($mapimessage, array (PR_SOURCE_KEY)); + return bin2hex($sourcekeyprops[PR_SOURCE_KEY]); + } + else + throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error updating object: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); + } + + /** + * Imports a deletion. This may conflict if the local object has been modified + * + * @param string $id + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ImportMessageDeletion($id) { + // check if the message is in the current syncinterval + if (!$this->isMessageInSyncInterval($id)) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Message is outside the sync interval and so far not deleted.", $id), SYNC_STATUS_OBJECTNOTFOUND); + + // check for conflicts + $this->lazyLoadConflicts(); + if($this->memChanges->IsChanged($id)) { + ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data from Server will be dropped! PIM deleted object.", $id)); + } + elseif($this->memChanges->IsDeleted($id)) { + ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id)); + return true; + } + + // do a 'soft' delete so people can un-delete if necessary + if(mapi_importcontentschanges_importmessagedeletion($this->importer, 1, array(hex2bin($id)))) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Error updating object: 0x%X", $id, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); + + return true; + } + + /** + * Imports a change in 'read' flag + * This can never conflict + * + * @param string $id + * @param int $flags - read/unread + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ImportMessageReadFlag($id, $flags) { + // check if the message is in the current syncinterval + if (!$this->isMessageInSyncInterval($id)) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Message is outside the sync interval. Flags not updated.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND); + + // check for conflicts + /* + * Checking for conflicts is correct at this point, but is a very expensive operation. + * If the message was deleted, only an error will be shown. + * + $this->lazyLoadConflicts(); + if($this->memChanges->IsDeleted($id)) { + ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageReadFlag('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id)); + return true; + } + */ + + $readstate = array ( "sourcekey" => hex2bin($id), "flags" => $flags); + + if(!mapi_importcontentschanges_importperuserreadstatechange($this->importer, array($readstate) )) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state: 0x%X", $id, $flags, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); + + return true; + } + + /** + * Imports a move of a message. This occurs when a user moves an item to another folder + * + * Normally, we would implement this via the 'offical' importmessagemove() function on the ICS importer, + * but the Zarafa importer does not support this. Therefore we currently implement it via a standard mapi + * call. This causes a mirror 'add/delete' to be sent to the PDA at the next sync. + * Manfred, 2010-10-21. For some mobiles import was causing duplicate messages in the destination folder + * (Mantis #202). Therefore we will create a new message in the destination folder, copy properties + * of the source message to the new one and then delete the source message. + * + * @param string $id + * @param string $newfolder destination folder + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ImportMessageMove($id, $newfolder) { + if (strtolower($newfolder) == strtolower(bin2hex($this->folderid)) ) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, source and destination are equal", $id, $newfolder), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST); + + // check if the source message is in the current syncinterval + if (!$this->isMessageInSyncInterval($id)) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Source message is outside the sync interval. Move not performed.", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); + + // Get the entryid of the message we're moving + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($id)); + if(!$entryid) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source message id", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); + + //open the source message + $srcmessage = mapi_msgstore_openentry($this->store, $entryid); + if (!$srcmessage) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source message: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); + + // get correct mapi store for the destination folder + $dststore = ZPush::GetBackend()->GetMAPIStoreForFolderId(ZPush::GetAdditionalSyncFolderStore($newfolder), $newfolder); + if ($dststore === false) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open store of destination folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); + + $dstentryid = mapi_msgstore_entryidfromsourcekey($dststore, hex2bin($newfolder)); + if(!$dstentryid) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve destination folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); + + $dstfolder = mapi_msgstore_openentry($dststore, $dstentryid); + if(!$dstfolder) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open destination folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); + + $newmessage = mapi_folder_createmessage($dstfolder); + if (!$newmessage) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to create message in destination folder: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); + + // Copy message + mapi_copyto($srcmessage, array(), array(), $newmessage); + if (mapi_last_hresult()) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, copy to destination message failed: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); + + $srcfolderentryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid); + if(!$srcfolderentryid) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); + + $srcfolder = mapi_msgstore_openentry($this->store, $srcfolderentryid); + if (!$srcfolder) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source folder: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); + + // Save changes + mapi_savechanges($newmessage); + if (mapi_last_hresult()) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); + + // Delete the old message + if (!mapi_folder_deletemessages($srcfolder, array($entryid))) + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, delete of source message failed: 0x%X. Possible duplicates.", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_SOURCEORDESTLOCKED); + + $sourcekeyprops = mapi_getprops($newmessage, array (PR_SOURCE_KEY)); + if (isset($sourcekeyprops[PR_SOURCE_KEY]) && $sourcekeyprops[PR_SOURCE_KEY]) + return bin2hex($sourcekeyprops[PR_SOURCE_KEY]); + + return false; + } + + + /**---------------------------------------------------------------------------------------------------------- + * Methods for HierarchyExporter + */ + + /** + * Imports a change on a folder + * + * @param object $folder SyncFolder + * + * @access public + * @return string id of the folder + * @throws StatusException + */ + public function ImportFolderChange($folder) { + $id = isset($folder->serverid)?$folder->serverid:false; + $parent = $folder->parentid; + $displayname = u2wi($folder->displayname); + $type = $folder->type; + + if (Utils::IsSystemFolder($type)) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, system folder can not be created/modified", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname), SYNC_FSSTATUS_SYSTEMFOLDER); + + // create a new folder if $id is not set + if (!$id) { + // the root folder is "0" - get IPM_SUBTREE + if ($parent == "0") { + $parentprops = mapi_getprops($this->store, array(PR_IPM_SUBTREE_ENTRYID)); + if (isset($parentprops[PR_IPM_SUBTREE_ENTRYID])) + $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID]; + } + else + $parentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent)); + + if (!$parentfentryid) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent folder (no entry id)", Utils::PrintAsString(false), $folder->parentid, $displayname), SYNC_FSSTATUS_PARENTNOTFOUND); + + $parentfolder = mapi_msgstore_openentry($this->store, $parentfentryid); + if (!$parentfolder) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent folder (open entry)", Utils::PrintAsString(false), $folder->parentid, $displayname), SYNC_FSSTATUS_PARENTNOTFOUND); + + // mapi_folder_createfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION + $newfolder = mapi_folder_createfolder($parentfolder, $displayname, ""); + if (mapi_last_hresult()) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, mapi_folder_createfolder() failed: 0x%X", Utils::PrintAsString(false), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_FOLDEREXISTS); + + mapi_setprops($newfolder, array(PR_CONTAINER_CLASS => MAPIUtils::GetContainerClassFromFolderType($type))); + + $props = mapi_getprops($newfolder, array(PR_SOURCE_KEY)); + if (isset($props[PR_SOURCE_KEY])) { + $sourcekey = bin2hex($props[PR_SOURCE_KEY]); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Created folder '%s' with id: '%s'", $displayname, $sourcekey)); + return $sourcekey; + } + else + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, folder created but PR_SOURCE_KEY not available: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); + return false; + } + + // open folder for update + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id)); + if (!$entryid) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND); + + // check if this is a MAPI default folder + if ($this->mapiprovider->IsMAPIDefaultFolder($entryid)) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, MAPI default folder can not be created/modified", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname), SYNC_FSSTATUS_SYSTEMFOLDER); + + $mfolder = mapi_msgstore_openentry($this->store, $entryid); + if (!$mfolder) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND); + + $props = mapi_getprops($mfolder, array(PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_DISPLAY_NAME, PR_CONTAINER_CLASS)); + if (!isset($props[PR_SOURCE_KEY]) || !isset($props[PR_PARENT_SOURCE_KEY]) || !isset($props[PR_DISPLAY_NAME]) || !isset($props[PR_CONTAINER_CLASS])) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, folder data not available: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); + + // get the real parent source key from mapi + if ($parent == "0") { + $parentprops = mapi_getprops($this->store, array(PR_IPM_SUBTREE_ENTRYID)); + $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID]; + $mapifolder = mapi_msgstore_openentry($this->store, $parentfentryid); + + $rootfolderprops = mapi_getprops($mapifolder, array(PR_SOURCE_KEY)); + $parent = bin2hex($rootfolderprops[PR_SOURCE_KEY]); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): resolved AS parent '0' to sourcekey '%s'", $parent)); + } + + // a changed parent id means that the folder should be moved + if (bin2hex($props[PR_PARENT_SOURCE_KEY]) !== $parent) { + $sourceparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, $props[PR_PARENT_SOURCE_KEY]); + if(!$sourceparentfentryid) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent source folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND); + + $sourceparentfolder = mapi_msgstore_openentry($this->store, $sourceparentfentryid); + if(!$sourceparentfolder) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent source folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND); + + $destparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent)); + if(!$sourceparentfentryid) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open destination folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); + + $destfolder = mapi_msgstore_openentry($this->store, $destparentfentryid); + if(!$destfolder) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open destination folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); + + // mapi_folder_copyfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION + if(! mapi_folder_copyfolder($sourceparentfolder, $entryid, $destfolder, $displayname, FOLDER_MOVE)) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to move folder: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_FOLDEREXISTS); + + $folderProps = mapi_getprops($mfolder, array(PR_SOURCE_KEY)); + return $folderProps[PR_SOURCE_KEY]; + } + + // update the display name + $props = array(PR_DISPLAY_NAME => $displayname); + mapi_setprops($mfolder, $props); + mapi_savechanges($mfolder); + if (mapi_last_hresult()) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, mapi_savechanges() failed: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); + + ZLog::Write(LOGLEVEL_DEBUG, "Imported changes for folder: $id"); + return $id; + } + + /** + * Imports a folder deletion + * + * @param string $id + * @param string $parent id is ignored in ICS + * + * @access public + * @return int SYNC_FOLDERHIERARCHY_STATUS + * @throws StatusException + */ + public function ImportFolderDeletion($id, $parent = false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): importing folder deletetion", $id, $parent)); + + $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id)); + if(!$folderentryid) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error, unable to resolve folder", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_FOLDERDOESNOTEXIST); + + // get the folder type from the MAPIProvider + $type = $this->mapiprovider->GetFolderType($folderentryid); + + if (Utils::IsSystemFolder($type) || $this->mapiprovider->IsMAPIDefaultFolder($folderentryid)) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting system/default folder", $id, $parent), SYNC_FSSTATUS_SYSTEMFOLDER); + + $ret = mapi_importhierarchychanges_importfolderdeletion ($this->importer, 0, array(PR_SOURCE_KEY => hex2bin($id))); + if (!$ret) + throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting folder: 0x%X", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); + + return $ret; + } +} +?> diff --git a/sources/backend/zarafa/listfolders.php b/sources/backend/zarafa/listfolders.php new file mode 100755 index 0000000..1b95b7d --- /dev/null +++ b/sources/backend/zarafa/listfolders.php @@ -0,0 +1,184 @@ +#!/usr/bin/php +. +* +* Consult LICENSE file for details +************************************************/ + +define("PHP_MAPI_PATH", "/usr/share/php/mapi/"); +define('MAPI_SERVER', 'file:///var/run/zarafa'); + +$supported_classes = array ( + "IPF.Note" => "SYNC_FOLDER_TYPE_USER_MAIL", + "IPF.Task" => "SYNC_FOLDER_TYPE_USER_TASK", + "IPF.Appointment" => "SYNC_FOLDER_TYPE_USER_APPOINTMENT", + "IPF.Contact" => "SYNC_FOLDER_TYPE_USER_CONTACT", + "IPF.StickyNote" => "SYNC_FOLDER_TYPE_USER_NOTE" +); + +main(); + +function main() { + listfolders_configure(); + listfolders_handle(); +} + +function listfolders_configure() { + + if (!isset($_SERVER["TERM"]) || !isset($_SERVER["LOGNAME"])) { + echo "This script should not be called in a browser.\n"; + exit(1); + } + + if (!function_exists("getopt")) { + echo "PHP Function 'getopt()' not found. Please check your PHP version and settings.\n"; + exit(1); + } + + require(PHP_MAPI_PATH.'mapi.util.php'); + require(PHP_MAPI_PATH.'mapidefs.php'); + require(PHP_MAPI_PATH.'mapicode.php'); + require(PHP_MAPI_PATH.'mapitags.php'); + require(PHP_MAPI_PATH.'mapiguid.php'); +} + +function listfolders_handle() { + $shortoptions = "l:h:u:p:"; + $options = getopt($shortoptions); + + $mapi = MAPI_SERVER; + $user = "SYSTEM"; + $pass = ""; + + if (isset($options['h'])) + $mapi = $options['h']; + + if (isset($options['u']) && isset($options['p'])) { + $user = $options['u']; + $pass = $options['p']; + } + + $zarafaAdmin = listfolders_zarafa_admin_setup($mapi, $user, $pass); + if (isset($zarafaAdmin['adminStore']) && isset($options['l'])) { + listfolders_getlist($zarafaAdmin['adminStore'], $zarafaAdmin['session'], trim($options['l'])); + } + else { + echo "Usage:\nlistfolders.php [actions] [options]\n\nActions: [-l username]\n\t-l username\tlist folders of user, for public folder use 'SYSTEM'\n\nGlobal options: [-h path] [[-u remoteuser] [-p password]]\n\t-h path\t\tconnect through , e.g. file:///var/run/socket\n\t-u authuser\tlogin as authenticated administration user\n\t-p authpassword\tpassword of the remoteuser\n\n"; + } +} + +function listfolders_zarafa_admin_setup ($mapi, $user, $pass) { + $session = @mapi_logon_zarafa($user, $pass, $mapi); + + if (!$session) { + echo "User '$user' could not login. The script will exit. Errorcode: 0x". sprintf("%x", mapi_last_hresult()) . "\n"; + exit(1); + } + + $stores = @mapi_getmsgstorestable($session); + $storeslist = @mapi_table_queryallrows($stores); + $adminStore = @mapi_openmsgstore($session, $storeslist[0][PR_ENTRYID]); + + $zarafauserinfo = @mapi_zarafa_getuser_by_name($adminStore, $user); + $admin = (isset($zarafauserinfo['admin']) && $zarafauserinfo['admin'])?true:false; + + if (!$stores || !$storeslist || !$adminStore || !$admin) { + echo "There was error trying to log in as admin or retrieving admin info. The script will exit.\n"; + exit(1); + } + + return array("session" => $session, "adminStore" => $adminStore); +} + + +function listfolders_getlist ($adminStore, $session, $user) { + global $supported_classes; + + if (strtoupper($user) == 'SYSTEM') { + // Find the public store store + $storestables = @mapi_getmsgstorestable($session); + $result = @mapi_last_hresult(); + + if ($result == NOERROR){ + $rows = @mapi_table_queryallrows($storestables, array(PR_ENTRYID, PR_MDB_PROVIDER)); + + foreach($rows as $row) { + if (isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) { + if (!isset($row[PR_ENTRYID])) { + echo "Public folder are not available.\nIf this is a multi-tenancy system, use -u and -p and login with an admin user of the company.\nThe script will exit.\n"; + exit (1); + } + $entryid = $row[PR_ENTRYID]; + break; + } + } + } + } + else + $entryid = @mapi_msgstore_createentryid($adminStore, $user); + + $userStore = @mapi_openmsgstore($session, $entryid); + $hresult = mapi_last_hresult(); + + // Cache the store for later use + if($hresult != NOERROR) { + echo "Could not open store for '$user'. The script will exit.\n"; + exit (1); + } + + $folder = @mapi_msgstore_openentry($userStore); + $h_table = @mapi_folder_gethierarchytable($folder, CONVENIENT_DEPTH); + $subfolders = @mapi_table_queryallrows($h_table, array(PR_ENTRYID, PR_DISPLAY_NAME, PR_CONTAINER_CLASS, PR_SOURCE_KEY)); + + echo "Available folders in store '$user':\n" . str_repeat("-", 50) . "\n"; + foreach($subfolders as $folder) { + if (isset($folder[PR_CONTAINER_CLASS]) && array_key_exists($folder[PR_CONTAINER_CLASS], $supported_classes)) { + echo "Folder name:\t". $folder[PR_DISPLAY_NAME] . "\n"; + echo "Folder ID:\t". bin2hex($folder[PR_SOURCE_KEY]) . "\n"; + echo "Type:\t\t". $supported_classes[$folder[PR_CONTAINER_CLASS]] . "\n"; + echo "\n"; + } + } +} + +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapi/class.baseexception.php b/sources/backend/zarafa/mapi/class.baseexception.php new file mode 100644 index 0000000..6ac22fc --- /dev/null +++ b/sources/backend/zarafa/mapi/class.baseexception.php @@ -0,0 +1,226 @@ +. + * + */ + + +/** + * Defines a base exception class for all custom exceptions, so every exceptions that + * is thrown/caught by this application should extend this base class and make use of it. + * it removes some peculiarities between different versions of PHP and exception handling. + * + * Some basic function of Exception class + * getMessage()- message of exception + * getCode() - code of exception + * getFile() - source filename + * getLine() - source line + * getTrace() - n array of the backtrace() + * getTraceAsString() - formated string of trace + */ +class BaseException extends Exception +{ + /** + * Reference of previous exception, only used for PHP < 5.3 + * can't use $previous here as its a private variable of parent class + */ + private $_previous = null; + + /** + * Base name of the file, so we don't have to use static path of the file + */ + private $baseFile = null; + + /** + * Flag to check if exception is already handled or not + */ + public $isHandled = false; + + /** + * The exception message to show at client side. + */ + public $displayMessage = null; + + /** + * Construct the exception + * + * @param string $errorMessage + * @param int $code + * @param Exception $previous + * @param string $displayMessage + * @return void + */ + public function __construct($errorMessage, $code = 0, Exception $previous = null, $displayMessage = null) { + // assign display message + $this->displayMessage = $displayMessage; + + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + parent::__construct($errorMessage, (int) $code); + + // set previous exception + if(!is_null($previous)) { + $this->_previous = $previous; + } + } else { + parent::__construct($errorMessage, (int) $code, $previous); + } + } + + /** + * Overloading of final methods to get rid of incompatibilities between different PHP versions. + * + * @param string $method + * @param array $args + * @return mixed + */ + public function __call($method, array $args) + { + if ('getprevious' == strtolower($method)) { + return $this->_getPrevious(); + } + + return null; + } + + /** + * @return string returns file name and line number combined where exception occured. + */ + public function getFileLine() + { + return $this->getBaseFile() . ':' . $this->getLine(); + } + + /** + * @return string returns message that should be sent to client to display + */ + public function getDisplayMessage() + { + if(!is_null($this->displayMessage)) { + return $this->displayMessage; + } + + return $this->getMessage(); + } + + /** + * Function sets display message of an exception that will be sent to the client side + * to show it to user. + * @param string $message display message. + */ + public function setDisplayMessage($message) + { + $this->displayMessage = $message; + } + + /** + * Function sets a flag in exception class to indicate that exception is already handled + * so if it is caught again in the top level of function stack then we have to silently + * ignore it. + */ + public function setHandled() + { + $this->isHandled = true; + } + + /** + * @return string returns base path of the file where exception occured. + */ + public function getBaseFile() + { + if(is_null($this->baseFile)) { + $this->baseFile = basename(parent::getFile()); + } + + return $this->baseFile; + } + + /** + * Function will check for PHP version if it is greater than 5.3 then we can use its default implementation + * otherwise we have to use our own implementation of chanining functionality. + * + * @return Exception returns previous exception + */ + public function _getPrevious() + { + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + return $this->_previous; + } else { + return parent::getPrevious(); + } + } + + /** + * String representation of the exception, also handles previous exception. + * + * @return string + */ + public function __toString() + { + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + if (($e = $this->getPrevious()) !== null) { + return $e->__toString() + . "\n\nNext " + . parent::__toString(); + } + } + + return parent::__toString(); + } + + /** + * Name of the class of exception. + * + * @return string + */ + public function getName() + { + return get_class($this); + } + + // @TODO getTrace and getTraceAsString +} +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapi/class.baserecurrence.php b/sources/backend/zarafa/mapi/class.baserecurrence.php new file mode 100644 index 0000000..e4b4cf4 --- /dev/null +++ b/sources/backend/zarafa/mapi/class.baserecurrence.php @@ -0,0 +1,1945 @@ +. + * + */ + + /** + * BaseRecurrence + * this class is superclass for recurrence for appointments and tasks. This class provides all + * basic features of recurrence. + */ + class BaseRecurrence + { + /** + * @var object Mapi Message Store (may be null if readonly) + */ + var $store; + + /** + * @var object Mapi Message (may be null if readonly) + */ + var $message; + + /** + * @var array Message Properties + */ + var $messageprops; + + /** + * @var array list of property tags + */ + var $proptags; + + /** + * @var recurrence data of this calendar item + */ + var $recur; + + /** + * @var Timezone data of this calendar item + */ + var $tz; + + /** + * Constructor + * @param resource $store MAPI Message Store Object + * @param resource $message the MAPI (appointment) message + * @param array $properties the list of MAPI properties the message has. + */ + function BaseRecurrence($store, $message) + { + $this->store = $store; + + if(is_array($message)) { + $this->messageprops = $message; + } else { + $this->message = $message; + $this->messageprops = mapi_getprops($this->message, $this->proptags); + } + + if(isset($this->messageprops[$this->proptags["recurring_data"]])) { + // There is a possibility that recurr blob can be more than 255 bytes so get full blob through stream interface + if (strlen($this->messageprops[$this->proptags["recurring_data"]]) >= 255) { + $this->getFullRecurrenceBlob(); + } + + $this->recur = $this->parseRecurrence($this->messageprops[$this->proptags["recurring_data"]]); + } + if(isset($this->proptags["timezone_data"]) && isset($this->messageprops[$this->proptags["timezone_data"]])) { + $this->tz = $this->parseTimezone($this->messageprops[$this->proptags["timezone_data"]]); + } + } + + function getRecurrence() + { + return $this->recur; + } + + function getFullRecurrenceBlob() + { + $message = mapi_msgstore_openentry($this->store, $this->messageprops[PR_ENTRYID]); + + $recurrBlob = ''; + $stream = mapi_openproperty($message, $this->proptags["recurring_data"], IID_IStream, 0, 0); + $stat = mapi_stream_stat($stream); + + for ($i = 0; $i < $stat['cb']; $i += 1024) { + $recurrBlob .= mapi_stream_read($stream, 1024); + } + + if (!empty($recurrBlob)) { + $this->messageprops[$this->proptags["recurring_data"]] = $recurrBlob; + } + } + + /** + * Function for parsing the Recurrence value of a Calendar item. + * + * Retrieve it from Named Property 0x8216 as a PT_BINARY and pass the + * data to this function + * + * Returns a structure containing the data: + * + * type - type of recurrence: day=10, week=11, month=12, year=13 + * subtype - type of day recurrence: 2=monthday (ie 21st day of month), 3=nday'th weekdays (ie. 2nd Tuesday and Wednesday) + * start - unix timestamp of first occurrence + * end - unix timestamp of last occurrence (up to and including), so when start == end -> occurrences = 1 + * numoccur - occurrences (may be very large when there is no end data) + * + * then, for each type: + * + * Daily: + * everyn - every [everyn] days in minutes + * regen - regenerating event (like tasks) + * + * Weekly: + * everyn - every [everyn] weeks in weeks + * regen - regenerating event (like tasks) + * weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc) + * + * Monthly: + * everyn - every [everyn] months + * regen - regenerating event (like tasks) + * + * subtype 2: + * monthday - on day [monthday] of the month + * + * subtype 3: + * weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc) + * nday - on [nday]'th [weekdays] of the month + * + * Yearly: + * everyn - every [everyn] months (12, 24, 36, ...) + * month - in month [month] (although the month is encoded in minutes since the startning of the year ........) + * regen - regenerating event (like tasks) + * + * subtype 2: + * monthday - on day [monthday] of the month + * + * subtype 3: + * weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc) + * nday - on [nday]'th [weekdays] of the month [month] + * @param string $rdata Binary string + * @return array recurrence data. + */ + function parseRecurrence($rdata) + { + if (strlen($rdata) < 10) { + return; + } + + $ret["changed_occurences"] = array(); + $ret["deleted_occurences"] = array(); + + $data = unpack("Vconst1/Crtype/Cconst2/Vrtype2", $rdata); + + $ret["type"] = $data["rtype"]; + $ret["subtype"] = $data["rtype2"]; + $rdata = substr($rdata, 10); + + switch ($data["rtype"]) + { + case 0x0a: + // Daily + if (strlen($rdata) < 12) { + return $ret; + } + + $data = unpack("Vunknown/Veveryn/Vregen", $rdata); + $ret["everyn"] = $data["everyn"]; + $ret["regen"] = $data["regen"]; + + switch($ret["subtype"]) + { + case 0: + $rdata = substr($rdata, 12); + break; + case 1: + $rdata = substr($rdata, 16); + break; + } + + break; + + case 0x0b: + // Weekly + if (strlen($rdata) < 16) { + return $ret; + } + + $data = unpack("Vconst1/Veveryn/Vregen", $rdata); + $rdata = substr($rdata, 12); + + $ret["everyn"] = $data["everyn"]; + $ret["regen"] = $data["regen"]; + $ret["weekdays"] = 0; + + if ($data["regen"] == 0) { + $data = unpack("Vweekdays", $rdata); + $rdata = substr($rdata, 4); + + $ret["weekdays"] = $data["weekdays"]; + } + break; + + case 0x0c: + // Monthly + if (strlen($rdata) < 16) { + return $ret; + } + + $data = unpack("Vconst1/Veveryn/Vregen/Vmonthday", $rdata); + + $ret["everyn"] = $data["everyn"]; + $ret["regen"] = $data["regen"]; + + if ($ret["subtype"] == 3) { + $ret["weekdays"] = $data["monthday"]; + } else { + $ret["monthday"] = $data["monthday"]; + } + + $rdata = substr($rdata, 16); + + if ($ret["subtype"] == 3) { + $data = unpack("Vnday", $rdata); + $ret["nday"] = $data["nday"]; + $rdata = substr($rdata, 4); + } + break; + + case 0x0d: + // Yearly + if (strlen($rdata) < 16) + return $ret; + + $data = unpack("Vmonth/Veveryn/Vregen/Vmonthday", $rdata); + + $ret["month"] = $data["month"]; + $ret["everyn"] = $data["everyn"]; + $ret["regen"] = $data["regen"]; + + if ($ret["subtype"] == 3) { + $ret["weekdays"] = $data["monthday"]; + } else { + $ret["monthday"] = $data["monthday"]; + } + + $rdata = substr($rdata, 16); + + if ($ret["subtype"] == 3) { + $data = unpack("Vnday", $rdata); + $ret["nday"] = $data["nday"]; + $rdata = substr($rdata, 4); + } + break; + } + + if (strlen($rdata) < 16) { + return $ret; + } + + $data = unpack("Cterm/C3const1/Vnumoccur/Vconst2/Vnumexcept", $rdata); + + $rdata = substr($rdata, 16); + + $ret["term"] = $data["term"]; + $ret["numoccur"] = $data["numoccur"]; + $ret["numexcept"] = $data["numexcept"]; + + // exc_base_dates are *all* the base dates that have been either deleted or modified + $exc_base_dates = array(); + for($i = 0; $i < $ret["numexcept"]; $i++) + { + if (strlen($rdata) < 4) { + // We shouldn't arrive here, because that implies + // numexcept does not match the amount of data + // which is available for the exceptions. + return $ret; + } + $data = unpack("Vbasedate", $rdata); + $rdata = substr($rdata, 4); + $exc_base_dates[] = $this->recurDataToUnixData($data["basedate"]); + } + + if (strlen($rdata) < 4) { + return $ret; + } + + $data = unpack("Vnumexceptmod", $rdata); + $rdata = substr($rdata, 4); + + $ret["numexceptmod"] = $data["numexceptmod"]; + + // exc_changed are the base dates of *modified* occurrences. exactly what is modified + // is in the attachments *and* in the data further down this function. + $exc_changed = array(); + for($i = 0; $i < $ret["numexceptmod"]; $i++) + { + if (strlen($rdata) < 4) { + // We shouldn't arrive here, because that implies + // numexceptmod does not match the amount of data + // which is available for the exceptions. + return $ret; + } + $data = unpack("Vstartdate", $rdata); + $rdata = substr($rdata, 4); + $exc_changed[] = $this->recurDataToUnixData($data["startdate"]); + } + + if (strlen($rdata) < 8) { + return $ret; + } + + $data = unpack("Vstart/Vend", $rdata); + $rdata = substr($rdata, 8); + + $ret["start"] = $this->recurDataToUnixData($data["start"]); + $ret["end"] = $this->recurDataToUnixData($data["end"]); + + // this is where task recurrence stop + if (strlen($rdata) < 16) { + return $ret; + } + + $data = unpack("Vreaderversion/Vwriterversion/Vstartmin/Vendmin", $rdata); + $rdata = substr($rdata, 16); + + $ret["startocc"] = $data["startmin"]; + $ret["endocc"] = $data["endmin"]; + $readerversion = $data["readerversion"]; + $writerversion = $data["writerversion"]; + + $data = unpack("vnumber", $rdata); + $rdata = substr($rdata, 2); + + $nexceptions = $data["number"]; + $exc_changed_details = array(); + + // Parse n modified exceptions + for($i=0;$i<$nexceptions;$i++) + { + $item = array(); + + // Get exception startdate, enddate and basedate (the date at which the occurrence would have started) + $data = unpack("Vstartdate/Venddate/Vbasedate", $rdata); + $rdata = substr($rdata, 12); + + // Convert recurtimestamp to unix timestamp + $startdate = $this->recurDataToUnixData($data["startdate"]); + $enddate = $this->recurDataToUnixData($data["enddate"]); + $basedate = $this->recurDataToUnixData($data["basedate"]); + + // Set the right properties + $item["basedate"] = $this->dayStartOf($basedate); + $item["start"] = $startdate; + $item["end"] = $enddate; + + $data = unpack("vbitmask", $rdata); + $rdata = substr($rdata, 2); + $item["bitmask"] = $data["bitmask"]; // save bitmask for extended exceptions + + // Bitmask to verify what properties are changed + $bitmask = $data["bitmask"]; + + // ARO_SUBJECT: 0x0001 + // Look for field: SubjectLength (2b), SubjectLength2 (2b) and Subject + if(($bitmask &(1 << 0))) { + $data = unpack("vnull_length/vlength", $rdata); + $rdata = substr($rdata, 4); + + $length = $data["length"]; + $item["subject"] = ""; // Normalized subject + for($j = 0; $j < $length && strlen($rdata); $j++) + { + $data = unpack("Cchar", $rdata); + $rdata = substr($rdata, 1); + + $item["subject"] .= chr($data["char"]); + } + } + + // ARO_MEETINGTYPE: 0x0002 + if(($bitmask &(1 << 1))) { + $rdata = substr($rdata, 4); + // Attendees modified: no data here (only in attachment) + } + + // ARO_REMINDERDELTA: 0x0004 + // Look for field: ReminderDelta (4b) + if(($bitmask &(1 << 2))) { + $data = unpack("Vremind_before", $rdata); + $rdata = substr($rdata, 4); + + $item["remind_before"] = $data["remind_before"]; + } + + // ARO_REMINDER: 0x0008 + // Look field: ReminderSet (4b) + if(($bitmask &(1 << 3))) { + $data = unpack("Vreminder_set", $rdata); + $rdata = substr($rdata, 4); + + $item["reminder_set"] = $data["reminder_set"]; + } + + // ARO_LOCATION: 0x0010 + // Look for fields: LocationLength (2b), LocationLength2 (2b) and Location + // Similar to ARO_SUBJECT above. + if(($bitmask &(1 << 4))) { + $data = unpack("vnull_length/vlength", $rdata); + $rdata = substr($rdata, 4); + + $item["location"] = ""; + + $length = $data["length"]; + $data = substr($rdata, 0, $length); + $rdata = substr($rdata, $length); + + $item["location"] .= $data; + } + + // ARO_BUSYSTATUS: 0x0020 + // Look for field: BusyStatus (4b) + if(($bitmask &(1 << 5))) { + $data = unpack("Vbusystatus", $rdata); + $rdata = substr($rdata, 4); + + $item["busystatus"] = $data["busystatus"]; + } + + // ARO_ATTACHMENT: 0x0040 + if(($bitmask &(1 << 6))) { + // no data: RESERVED + $rdata = substr($rdata, 4); + } + + // ARO_SUBTYPE: 0x0080 + // Look for field: SubType (4b). Determines whether it is an allday event. + if(($bitmask &(1 << 7))) { + $data = unpack("Vallday", $rdata); + $rdata = substr($rdata, 4); + + $item["alldayevent"] = $data["allday"]; + } + + // ARO_APPTCOLOR: 0x0100 + // Look for field: AppointmentColor (4b) + if(($bitmask &(1 << 8))) { + $data = unpack("Vlabel", $rdata); + $rdata = substr($rdata, 4); + + $item["label"] = $data["label"]; + } + + // ARO_EXCEPTIONAL_BODY: 0x0200 + if(($bitmask &(1 << 9))) { + // Notes or Attachments modified: no data here (only in attachment) + } + + array_push($exc_changed_details, $item); + } + + /** + * We now have $exc_changed, $exc_base_dates and $exc_changed_details + * We will ignore $exc_changed, as this information is available in $exc_changed_details + * also. If an item is in $exc_base_dates and NOT in $exc_changed_details, then the item + * has been deleted. + */ + + // Find deleted occurrences + $deleted_occurences = array(); + + foreach($exc_base_dates as $base_date) { + $found = false; + + foreach($exc_changed_details as $details) { + if($details["basedate"] == $base_date) { + $found = true; + break; + } + } + if(! $found) { + // item was not in exc_changed_details, so it must be deleted + $deleted_occurences[] = $base_date; + } + } + + $ret["deleted_occurences"] = $deleted_occurences; + $ret["changed_occurences"] = $exc_changed_details; + + // enough data for normal exception (no extended data) + if (strlen($rdata) < 16) { + return $ret; + } + + $data = unpack("Vreservedsize", $rdata); + $rdata = substr($rdata, 4 + $data["reservedsize"]); + + for($i=0;$i<$nexceptions;$i++) + { + // subject and location in ucs-2 to utf-8 + if ($writerversion >= 0x3009) { + $data = unpack("Vsize/Vvalue", $rdata); // size includes sizeof(value)==4 + $rdata = substr($rdata, 4 + $data["size"]); + } + + $data = unpack("Vreservedsize", $rdata); + $rdata = substr($rdata, 4 + $data["reservedsize"]); + + // ARO_SUBJECT(0x01) | ARO_LOCATION(0x10) + if ($exc_changed_details[$i]["bitmask"] & 0x11) { + $data = unpack("Vstart/Vend/Vorig", $rdata); + $rdata = substr($rdata, 4 * 3); + + $exc_changed_details[$i]["ex_start_datetime"] = $data["start"]; + $exc_changed_details[$i]["ex_end_datetime"] = $data["end"]; + $exc_changed_details[$i]["ex_orig_date"] = $data["orig"]; + } + + // ARO_SUBJECT + if ($exc_changed_details[$i]["bitmask"] & 0x01) { + // decode ucs2 string to utf-8 + $data = unpack("vlength", $rdata); + $rdata = substr($rdata, 2); + $length = $data["length"]; + $data = substr($rdata, 0, $length * 2); + $rdata = substr($rdata, $length * 2); + $subject = iconv("UCS-2LE", "UTF-8", $data); + // replace subject with unicode subject + $exc_changed_details[$i]["subject"] = $subject; + } + + // ARO_LOCATION + if ($exc_changed_details[$i]["bitmask"] & 0x10) { + // decode ucs2 string to utf-8 + $data = unpack("vlength", $rdata); + $rdata = substr($rdata, 2); + $length = $data["length"]; + $data = substr($rdata, 0, $length * 2); + $rdata = substr($rdata, $length * 2); + $location = iconv("UCS-2LE", "UTF-8", $data); + // replace subject with unicode subject + $exc_changed_details[$i]["location"] = $location; + } + + // ARO_SUBJECT(0x01) | ARO_LOCATION(0x10) + if ($exc_changed_details[$i]["bitmask"] & 0x11) { + $data = unpack("Vreservedsize", $rdata); + $rdata = substr($rdata, 4 + $data["reservedsize"]); + } + } + + // update with extended data + $ret["changed_occurences"] = $exc_changed_details; + + return $ret; + } + + /** + * Saves the recurrence data to the recurrence property + * @param array $properties the recurrence data. + * @return string binary string + */ + function saveRecurrence() + { + // Only save if a message was passed + if(!isset($this->message)) + return; + + // Abort if no recurrence was set + if(!isset($this->recur["type"]) && !isset($this->recur["subtype"])) { + return; + } + + if(!isset($this->recur["start"]) && !isset($this->recur["end"])) { + return; + } + + if(!isset($this->recur["startocc"]) && !isset($this->recur["endocc"])) { + return; + } + + $rdata = pack("CCCCCCV", 0x04, 0x30, 0x04, 0x30, (int) $this->recur["type"], 0x20, (int) $this->recur["subtype"]); + + $weekstart = 1; //monday + $forwardcount = 0; + $restocc = 0; + $dayofweek = (int) gmdate("w", (int) $this->recur["start"]); //0 (for Sunday) through 6 (for Saturday) + + $term = (int) $this->recur["type"]; + switch($term) + { + case 0x0A: + // Daily + if(!isset($this->recur["everyn"])) { + return; + } + + if($this->recur["subtype"] == 1) { + + // Daily every workday + $rdata .= pack("VVVV", (6 * 24 * 60), 1, 0, 0x3E); + } else { + // Daily every N days (everyN in minutes) + + $everyn = ((int) $this->recur["everyn"]) / 1440; + + // Calc first occ + $firstocc = $this->unixDataToRecurData($this->recur["start"]) % ((int) $this->recur["everyn"]); + + $rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], $this->recur["regen"] ? 1 : 0); + } + break; + case 0x0B: + // Weekly + if(!isset($this->recur["everyn"])) { + return; + } + + if (!$this->recur["regen"] && !isset($this->recur["weekdays"])) { + return; + } + + // No need to calculate startdate if sliding flag was set. + if (!$this->recur['regen']) { + // Calculate start date of recurrence + + // Find the first day that matches one of the weekdays selected + $daycount = 0; + $dayskip = -1; + for($j = 0; $j < 7; $j++) { + if(((int) $this->recur["weekdays"]) & (1<<( ($dayofweek+$j)%7)) ) { + if($dayskip == -1) + $dayskip = $j; + + $daycount++; + } + } + + // $dayskip is the number of days to skip from the startdate until the first occurrence + // $daycount is the number of days per week that an occurrence occurs + + $weekskip = 0; + if(($dayofweek < $weekstart && $dayskip > 0) || ($dayofweek+$dayskip) > 6) + $weekskip = 1; + + // Check if the recurrence ends after a number of occurences, in that case we must calculate the + // remaining occurences based on the start of the recurrence. + if (((int) $this->recur["term"]) == 0x22) { + // $weekskip is the amount of weeks to skip from the startdate before the first occurence + // $forwardcount is the maximum number of week occurrences we can go ahead after the first occurrence that + // is still inside the recurrence. We subtract one to make sure that the last week is never forwarded over + // (eg when numoccur = 2, and daycount = 1) + $forwardcount = floor( (int) ($this->recur["numoccur"] -1 ) / $daycount); + + // $restocc is the number of occurrences left after $forwardcount whole weeks of occurrences, minus one + // for the occurrence on the first day + $restocc = ((int) $this->recur["numoccur"]) - ($forwardcount*$daycount) - 1; + + // $forwardcount is now the number of weeks we can go forward and still be inside the recurrence + $forwardcount *= (int) $this->recur["everyn"]; + } + + // The real start is start + dayskip + weekskip-1 (since dayskip will already bring us into the next week) + $this->recur["start"] = ((int) $this->recur["start"]) + ($dayskip * 24*60*60)+ ($weekskip *(((int) $this->recur["everyn"]) - 1) * 7 * 24*60*60); + } + + // Calc first occ + $firstocc = ($this->unixDataToRecurData($this->recur["start"]) ) % ( ((int) $this->recur["everyn"]) * 7 * 24 * 60); + + $firstocc -= (((int) gmdate("w", (int) $this->recur["start"])) - 1) * 24 * 60; + + if ($this->recur["regen"]) + $rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], 1); + else + $rdata .= pack("VVVV", $firstocc, (int) $this->recur["everyn"], 0, (int) $this->recur["weekdays"]); + break; + case 0x0C: + // Monthly + case 0x0D: + // Yearly + if(!isset($this->recur["everyn"])) { + return; + } + if($term == 0x0D /*yearly*/ && !isset($this->recur["month"])) { + return; + } + + if($term == 0x0C /*monthly*/) { + $everyn = (int) $this->recur["everyn"]; + }else { + $everyn = $this->recur["regen"] ? ((int) $this->recur["everyn"]) * 12 : 12; + } + + // Get montday/month/year of original start + $curmonthday = gmdate("j", (int) $this->recur["start"] ); + $curyear = gmdate("Y", (int) $this->recur["start"] ); + $curmonth = gmdate("n", (int) $this->recur["start"] ); + + // Check if the recurrence ends after a number of occurences, in that case we must calculate the + // remaining occurences based on the start of the recurrence. + if (((int) $this->recur["term"]) == 0x22) { + // $forwardcount is the number of occurrences we can skip and still be inside the recurrence range (minus + // one to make sure there are always at least one occurrence left) + $forwardcount = ((((int) $this->recur["numoccur"])-1) * $everyn ); + } + + // Get month for yearly on D'th day of month M + if($term == 0x0D /*yearly*/) { + $selmonth = floor(((int) $this->recur["month"]) / (24 * 60 *29)) + 1; // 1=jan, 2=feb, eg + } + + switch((int) $this->recur["subtype"]) + { + // on D day of every M month + case 2: + if(!isset($this->recur["monthday"])) { + return; + } + // Recalc startdate + + // Set on the right begin day + + // Go the beginning of the month + $this->recur["start"] -= ($curmonthday-1) * 24*60*60; + // Go the the correct month day + $this->recur["start"] += (((int) $this->recur["monthday"])-1) * 24*60*60; + + // If the previous calculation gave us a start date *before* the original start date, then we need to skip to the next occurrence + if ( ($term == 0x0C /*monthly*/ && ((int) $this->recur["monthday"]) < $curmonthday) || + ($term == 0x0D /*yearly*/ &&( $selmonth < $curmonth || ($selmonth == $curmonth && ((int) $this->recur["monthday"]) < $curmonthday)) )) + { + if($term == 0x0D /*yearly*/) + $count = ($everyn - ($curmonth - $selmonth)); // Yearly, go to next occurrence in 'everyn' months minus difference in first occurence and original date + else + $count = $everyn; // Monthly, go to next occurrence in 'everyn' months + + // Forward by $count months. This is done by getting the number of days in that month and forwarding that many days + for($i=0; $i < $count; $i++) { + $this->recur["start"] += $this->getMonthInSeconds($curyear, $curmonth); + + if($curmonth == 12) { + $curyear++; + $curmonth = 0; + } + $curmonth++; + } + } + + // "start" is now pointing to the first occurrence, except that it will overshoot if the + // month in which it occurs has less days than specified as the day of the month. So 31st + // of each month will overshoot in february (29 days). We compensate for that by checking + // if the day of the month we got is wrong, and then back up to the last day of the previous + // month. + if(((int) $this->recur["monthday"]) >=28 && ((int) $this->recur["monthday"]) <= 31 && + gmdate("j", ((int) $this->recur["start"])) < ((int) $this->recur["monthday"])) + { + $this->recur["start"] -= gmdate("j", ((int) $this->recur["start"])) * 24 * 60 *60; + } + + // "start" is now the first occurrence + + if($term == 0x0C /*monthly*/) { + // Calc first occ + $monthIndex = ((((12%$everyn) * ((((int) gmdate("Y", $this->recur["start"])) - 1601)%$everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1))%$everyn; + + $firstocc = 0; + for($i=0; $i < $monthIndex; $i++) { + $firstocc+= $this->getMonthInSeconds(1601 + floor($i/12), ($i%12)+1) / 60; + } + + $rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]); + } else{ + // Calc first occ + $firstocc = 0; + $monthIndex = (int) gmdate("n", $this->recur["start"]); + for($i=1; $i < $monthIndex; $i++) { + $firstocc+= $this->getMonthInSeconds(1601 + floor($i/12), $i) / 60; + } + + $rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]); + } + break; + + case 3: + // monthly: on Nth weekday of every M month + // yearly: on Nth weekday of M month + if(!isset($this->recur["weekdays"]) && !isset($this->recur["nday"])) { + return; + } + + $weekdays = (int) $this->recur["weekdays"]; + $nday = (int) $this->recur["nday"]; + + // Calc startdate + $monthbegindow = (int) $this->recur["start"]; + + if($nday == 5) { + // Set date on the last day of the last month + $monthbegindow += (gmdate("t", $monthbegindow ) - gmdate("j", $monthbegindow )) * 24 * 60 * 60; + }else { + // Set on the first day of the month + $monthbegindow -= ((gmdate("j", $monthbegindow )-1) * 24 * 60 * 60); + } + + if($term == 0x0D /*yearly*/) { + // Set on right month + if($selmonth < $curmonth) + $tmp = 12 - $curmonth + $selmonth; + else + $tmp = ($selmonth - $curmonth); + + for($i=0; $i < $tmp; $i++) { + $monthbegindow += $this->getMonthInSeconds($curyear, $curmonth); + + if($curmonth == 12) { + $curyear++; + $curmonth = 0; + } + $curmonth++; + } + + }else { + // Check or you exist in the right month + + for($i = 0; $i < 7; $i++) { + if($nday == 5 && (1<<( (gmdate("w", $monthbegindow)-$i)%7) ) & $weekdays) { + $day = gmdate("j", $monthbegindow) - $i; + break; + }else if($nday != 5 && (1<<( (gmdate("w", $monthbegindow )+$i)%7) ) & $weekdays) { + $day = (($nday-1)*7) + ($i+1); + break; + } + } + + // Goto the next X month + if(isset($day) && ($day < gmdate("j", (int) $this->recur["start"])) ) { + if($nday == 5) { + $monthbegindow += 24 * 60 * 60; + if($curmonth == 12) { + $curyear++; + $curmonth = 0; + } + $curmonth++; + } + + for($i=0; $i < $everyn; $i++) { + $monthbegindow += $this->getMonthInSeconds($curyear, $curmonth); + + if($curmonth == 12) { + $curyear++; + $curmonth = 0; + } + $curmonth++; + } + + if($nday == 5) { + $monthbegindow -= 24 * 60 * 60; + } + } + } + + //FIXME: weekstart? + + $day = 0; + // Set start on the right day + for($i = 0; $i < 7; $i++) { + if($nday == 5 && (1<<( (gmdate("w", $monthbegindow )-$i)%7) ) & $weekdays) { + $day = $i; + break; + }else if($nday != 5 && (1<<( (gmdate("w", $monthbegindow )+$i)%7) ) & $weekdays) { + $day = ($nday - 1) * 7 + ($i+1); + break; + } + } + if($nday == 5) + $monthbegindow -= $day * 24 * 60 *60; + else + $monthbegindow += ($day-1) * 24 * 60 *60; + + $firstocc = 0; + + if($term == 0x0C /*monthly*/) { + // Calc first occ + $monthIndex = ((((12%$everyn) * (((int) gmdate("Y", $this->recur["start"]) - 1601)%$everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1))%$everyn; + + for($i=0; $i < $monthIndex; $i++) { + $firstocc+= $this->getMonthInSeconds(1601 + floor($i/12), ($i%12)+1) / 60; + } + + $rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday); + } else { + // Calc first occ + $monthIndex = (int) gmdate("n", $this->recur["start"]); + + for($i=1; $i < $monthIndex; $i++) { + $firstocc+= $this->getMonthInSeconds(1601 + floor($i/12), $i) / 60; + } + + $rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday); + } + break; + } + break; + + + + } + + if(!isset($this->recur["term"])) { + return; + } + + // Terminate + $term = (int) $this->recur["term"]; + $rdata .= pack("CCCC", $term, 0x20, 0x00, 0x00); + + switch($term) + { + // After the given enddate + case 0x21: + $rdata .= pack("V", 10); + break; + // After a number of times + case 0x22: + if(!isset($this->recur["numoccur"])) { + return; + } + + $rdata .= pack("V", (int) $this->recur["numoccur"]); + break; + // Never ends + case 0x23: + $rdata .= pack("V", 0); + break; + } + + // Strange little thing for the recurrence type "every workday" + if(((int) $this->recur["type"]) == 0x0B && ((int) $this->recur["subtype"]) == 1) { + $rdata .= pack("V", 1); + } else { // Other recurrences + $rdata .= pack("V", 0); + } + + // Exception data + + // Get all exceptions + $deleted_items = $this->recur["deleted_occurences"]; + $changed_items = $this->recur["changed_occurences"]; + + // Merge deleted and changed items into one list + $items = $deleted_items; + + foreach($changed_items as $changed_item) + array_push($items, $changed_item["basedate"]); + + sort($items); + + // Add the merged list in to the rdata + $rdata .= pack("V", count($items)); + foreach($items as $item) + $rdata .= pack("V", $this->unixDataToRecurData($item)); + + // Loop through the changed exceptions (not deleted) + $rdata .= pack("V", count($changed_items)); + $items = array(); + + foreach($changed_items as $changed_item) + { + $items[] = $this->dayStartOf($changed_item["start"]); + } + + sort($items); + + // Add the changed items list int the rdata + foreach($items as $item) + $rdata .= pack("V", $this->unixDataToRecurData($item)); + + // Set start date + $rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["start"])); + + // Set enddate + switch($term) + { + // After the given enddate + case 0x21: + $rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"])); + break; + // After a number of times + case 0x22: + // @todo: calculate enddate with intval($this->recur["startocc"]) + intval($this->recur["duration"]) > 24 hour + $occenddate = (int) $this->recur["start"]; + + switch((int) $this->recur["type"]) { + case 0x0A: //daily + + if($this->recur["subtype"] == 1) { + // Daily every workday + $restocc = (int) $this->recur["numoccur"]; + + // Get starting weekday + $nowtime = $this->gmtime($occenddate); + $j = $nowtime["tm_wday"]; + + while(1) + { + if(($j%7) > 0 && ($j%7)<6 ) { + $restocc--; + } + + $j++; + + if($restocc <= 0) + break; + + $occenddate += 24*60*60; + } + + } else { + // -1 because the first day already counts (from 1-1-1980 to 1-1-1980 is 1 occurrence) + $occenddate += (((int) $this->recur["everyn"]) * 60 * (((int) $this->recur["numoccur"]-1))); + } + break; + case 0x0B: //weekly + // Needed values + // $forwardcount - number of weeks we can skip forward + // $restocc - number of remaning occurrences after the week skip + + // Add the weeks till the last item + $occenddate+=($forwardcount*7*24*60*60); + + $dayofweek = gmdate("w", $occenddate); + + // Loop through the last occurrences until we have had them all + for($j = 1; $restocc>0; $j++) + { + // Jump to the next week (which may be N weeks away) when going over the week boundary + if((($dayofweek+$j)%7) == $weekstart) + $occenddate += (((int) $this->recur["everyn"])-1) * 7 * 24*60*60; + + // If this is a matching day, once less occurrence to process + if(((int) $this->recur["weekdays"]) & (1<<(($dayofweek+$j)%7)) ) { + $restocc--; + } + + // Next day + $occenddate += 24*60*60; + } + + break; + case 0x0C: //monthly + case 0x0D: //yearly + + $curyear = gmdate("Y", (int) $this->recur["start"] ); + $curmonth = gmdate("n", (int) $this->recur["start"] ); + // $forwardcount = months + + switch((int) $this->recur["subtype"]) + { + case 2: // on D day of every M month + while($forwardcount > 0) + { + $occenddate += $this->getMonthInSeconds($curyear, $curmonth); + + if($curmonth >=12) { + $curmonth = 1; + $curyear++; + } else { + $curmonth++; + } + $forwardcount--; + } + + // compensation between 28 and 31 + if(((int) $this->recur["monthday"]) >=28 && ((int) $this->recur["monthday"]) <= 31 && + gmdate("j", $occenddate) < ((int) $this->recur["monthday"])) + { + if(gmdate("j", $occenddate) < 28) + $occenddate -= gmdate("j", $occenddate) * 24 * 60 *60; + else + $occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 *60; + } + + + break; + case 3: // on Nth weekday of every M month + $nday = (int) $this->recur["nday"]; //1 tot 5 + $weekdays = (int) $this->recur["weekdays"]; + + + while($forwardcount > 0) + { + $occenddate += $this->getMonthInSeconds($curyear, $curmonth); + if($curmonth >=12) { + $curmonth = 1; + $curyear++; + } else { + $curmonth++; + } + + $forwardcount--; + } + + if($nday == 5) { + // Set date on the last day of the last month + $occenddate += (gmdate("t", $occenddate ) - gmdate("j", $occenddate )) * 24 * 60 * 60; + }else { + // Set date on the first day of the last month + $occenddate -= (gmdate("j", $occenddate )-1) * 24 * 60 * 60; + } + + for($i = 0; $i < 7; $i++) { + if( $nday == 5 && (1<<( (gmdate("w", $occenddate)-$i)%7) ) & $weekdays) { + $occenddate -= $i * 24 * 60 * 60; + break; + }else if($nday != 5 && (1<<( (gmdate("w", $occenddate)+$i)%7) ) & $weekdays) { + $occenddate += ($i + (($nday-1) *7)) * 24 * 60 * 60; + break; + } + } + + break; //case 3: + } + + break; + + } + + if (defined("PHP_INT_MAX") && $occenddate > PHP_INT_MAX) + $occenddate = PHP_INT_MAX; + + $this->recur["end"] = $occenddate; + + $rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"]) ); + break; + // Never ends + case 0x23: + default: + $this->recur["end"] = 0x7fffffff; // max date -> 2038 + $rdata .= pack("V", 0x5AE980DF); + break; + } + + // UTC date + $utcstart = $this->toGMT($this->tz, (int) $this->recur["start"]); + $utcend = $this->toGMT($this->tz, (int) $this->recur["end"]); + + //utc date+time + $utcfirstoccstartdatetime = (isset($this->recur["startocc"])) ? $utcstart + (((int) $this->recur["startocc"])*60) : $utcstart; + $utcfirstoccenddatetime = (isset($this->recur["endocc"])) ? $utcstart + (((int) $this->recur["endocc"]) * 60) : $utcstart; + + // update reminder time + mapi_setprops($this->message, Array($this->proptags["reminder_time"] => $utcfirstoccstartdatetime )); + + // update first occurrence date + mapi_setprops($this->message, Array($this->proptags["startdate"] => $utcfirstoccstartdatetime )); + mapi_setprops($this->message, Array($this->proptags["duedate"] => $utcfirstoccenddatetime )); + mapi_setprops($this->message, Array($this->proptags["commonstart"] => $utcfirstoccstartdatetime )); + mapi_setprops($this->message, Array($this->proptags["commonend"] => $utcfirstoccenddatetime )); + + // Set Outlook properties, if it is an appointment + if (isset($this->recur["message_class"]) && $this->recur["message_class"] == "IPM.Appointment") { + // update real begin and real end date + mapi_setprops($this->message, Array($this->proptags["startdate_recurring"] => $utcstart)); + mapi_setprops($this->message, Array($this->proptags["enddate_recurring"] => $utcend)); + + // recurrencetype + // Strange enough is the property recurrencetype, (type-0x9) and not the CDO recurrencetype + mapi_setprops($this->message, Array($this->proptags["recurrencetype"] => ((int) $this->recur["type"]) - 0x9)); + + // set named prop 'side_effects' to 369, needed for Outlook to ask for single or total recurrence when deleting + mapi_setprops($this->message, Array($this->proptags["side_effects"] => 369)); + } else { + mapi_setprops($this->message, Array($this->proptags["side_effects"] => 3441)); + } + + // FlagDueBy is datetime of the first reminder occurrence. Outlook gives on this time a reminder popup dialog + // Any change of the recurrence (including changing and deleting exceptions) causes the flagdueby to be reset + // to the 'next' occurrence; this makes sure that deleting the next ocurrence will correctly set the reminder to + // the occurrence after that. The 'next' occurrence is defined as being the first occurrence that starts at moment X (server time) + // with the reminder flag set. + $reminderprops = mapi_getprops($this->message, array($this->proptags["reminder_minutes"]) ); + if(isset($reminderprops[$this->proptags["reminder_minutes"]]) ) { + $occ = false; + $occurrences = $this->getItems(time(), 0x7ff00000, 3, true); + + for($i = 0, $len = count($occurrences) ; $i < $len; $i++) { + // This will actually also give us appointments that have already started, but not yet ended. Since we want the next + // reminder that occurs after time(), we may have to skip the first few entries. We get 3 entries since that is the maximum + // number that would be needed (assuming reminder for item X cannot be before the previous occurrence starts). Worst case: + // time() is currently after start but before end of item, but reminder of next item has already passed (reminder for next item + // can be DURING the previous item, eg daily allday events). In that case, the first and second items must be skipped. + + if(($occurrences[$i][$this->proptags["startdate"]] - $reminderprops[$this->proptags["reminder_minutes"]] * 60) > time()) { + $occ = $occurrences[$i]; + break; + } + } + + if($occ) { + mapi_setprops($this->message, Array($this->proptags["flagdueby"] => $occ[$this->proptags["startdate"]] - ($reminderprops[$this->proptags["reminder_minutes"]] * 60) )); + } else { + // Last reminder passed, no reminders any more. + mapi_setprops($this->message, Array($this->proptags["reminder"] => false, $this->proptags["flagdueby"] => 0x7ff00000)); + } + } + + // Default data + // Second item (0x08) indicates the Outlook version (see documentation at the bottom of this file for more information) + $rdata .= pack("VCCCC", 0x00003006, 0x08, 0x30, 0x00, 0x00); + + if(isset($this->recur["startocc"]) && isset($this->recur["endocc"])) { + // Set start and endtime in minutes + $rdata .= pack("VV", (int) $this->recur["startocc"], (int) $this->recur["endocc"]); + } + + // Detailed exception data + + $changed_items = $this->recur["changed_occurences"]; + + $rdata .= pack("v", count($changed_items)); + + foreach($changed_items as $changed_item) + { + // Set start and end time of exception + $rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"])); + $rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"])); + $rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"])); + + //Bitmask + $bitmask = 0; + + // Check for changed strings + if(isset($changed_item["subject"])) { + $bitmask |= 1 << 0; + } + + if(isset($changed_item["remind_before"])) { + $bitmask |= 1 << 2; + } + + if(isset($changed_item["reminder_set"])) { + $bitmask |= 1 << 3; + } + + if(isset($changed_item["location"])) { + $bitmask |= 1 << 4; + } + + if(isset($changed_item["busystatus"])) { + $bitmask |= 1 << 5; + } + + if(isset($changed_item["alldayevent"])) { + $bitmask |= 1 << 7; + } + + if(isset($changed_item["label"])) { + $bitmask |= 1 << 8; + } + + $rdata .= pack("v", $bitmask); + + // Set "subject" + if(isset($changed_item["subject"])) { + // convert utf-8 to non-unicode blob string (us-ascii?) + $subject = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["subject"]); + $length = strlen($subject); + $rdata .= pack("vv", $length + 1, $length); + $rdata .= pack("a".$length, $subject); + } + + if(isset($changed_item["remind_before"])) { + $rdata .= pack("V", $changed_item["remind_before"]); + } + + if(isset($changed_item["reminder_set"])) { + $rdata .= pack("V", $changed_item["reminder_set"]); + } + + if(isset($changed_item["location"])) { + $location = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["location"]); + $length = strlen($location); + $rdata .= pack("vv", $length + 1, $length); + $rdata .= pack("a".$length, $location); + } + + if(isset($changed_item["busystatus"])) { + $rdata .= pack("V", $changed_item["busystatus"]); + } + + if(isset($changed_item["alldayevent"])) { + $rdata .= pack("V", $changed_item["alldayevent"]); + } + + if(isset($changed_item["label"])) { + $rdata .= pack("V", $changed_item["label"]); + } + } + + $rdata .= pack("V", 0); + + // write extended data + foreach($changed_items as $changed_item) + { + $rdata .= pack("V", 0); + if(isset($changed_item["subject"]) || isset($changed_item["location"])) { + $rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"])); + $rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"])); + $rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"])); + } + + if(isset($changed_item["subject"])) { + $subject = iconv("UTF-8", "UCS-2LE", $changed_item["subject"]); + $length = iconv_strlen($subject, "UCS-2LE"); + $rdata .= pack("v", $length); + $rdata .= pack("a".$length*2, $subject); + } + + if(isset($changed_item["location"])) { + $location = iconv("UTF-8", "UCS-2LE", $changed_item["location"]); + $length = iconv_strlen($location, "UCS-2LE"); + $rdata .= pack("v", $length); + $rdata .= pack("a".$length*2, $location); + } + + if(isset($changed_item["subject"]) || isset($changed_item["location"])) { + $rdata .= pack("V", 0); + } + } + + $rdata .= pack("V", 0); + + // Set props + mapi_setprops($this->message, Array($this->proptags["recurring_data"] => $rdata, $this->proptags["recurring"] => true)); + if(isset($this->tz) && $this->tz){ + $timezone = "GMT"; + if ($this->tz["timezone"]!=0){ + // Create user readable timezone information + $timezone = sprintf("(GMT %s%02d:%02d)", (-$this->tz["timezone"]>0 ? "+" : "-"), + abs($this->tz["timezone"]/60), + abs($this->tz["timezone"]%60)); + } + mapi_setprops($this->message, Array($this->proptags["timezone_data"] => $this->getTimezoneData($this->tz), + $this->proptags["timezone"] => $timezone)); + } + } + + /** + * Function which converts a recurrence date timestamp to an unix date timestamp. + * @author Steve Hardy + * @param Int $rdate the date which will be converted + * @return Int the converted date + */ + function recurDataToUnixData($rdate) + { + return ($rdate - 194074560) * 60 ; + } + + /** + * Function which converts an unix date timestamp to recurrence date timestamp. + * @author Johnny Biemans + * @param Date $date the date which will be converted + * @return Int the converted date in minutes + */ + function unixDataToRecurData($date) + { + return ($date / 60) + 194074560; + } + + /** + * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves + * @author Steve Hardy + */ + function GetTZOffset($ts) + { + $Offset = date("O", $ts); + + $Parity = $Offset < 0 ? -1 : 1; + $Offset = $Parity * $Offset; + $Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100; + + return $Parity * $Offset; + } + + /** + * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves + * @author Steve Hardy + * @param Date $time + * @return Date GMT Time + */ + function gmtime($time) + { + $TZOffset = $this->GetTZOffset($time); + + $t_time = $time - $TZOffset * 60; #Counter adjust for localtime() + $t_arr = localtime($t_time, 1); + + return $t_arr; + } + + function isLeapYear($year) { + return ( $year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0) ); + } + + function getMonthInSeconds($year, $month) + { + if( in_array($month, array(1,3,5,7,8,10,12) ) ) { + $day = 31; + } else if( in_array($month, array(4,6,9,11) ) ) { + $day = 30; + } else { + $day = 28; + if( $this->isLeapYear($year) == 1 ) + $day++; + } + return $day * 24 * 60 * 60; + } + + /** + * Function to get a date by Year Nr, Month Nr, Week Nr, Day Nr, and hour + * @param int $year + * @param int $month + * @param int $week + * @param int $day + * @param int $hour + * @return returns the timestamp of the given date, timezone-independant + */ + function getDateByYearMonthWeekDayHour($year, $month, $week, $day, $hour) + { + // get first day of month + $date = gmmktime(0,0,0,$month,0,$year + 1900); + + // get wday info + $gmdate = $this->gmtime($date); + + $date -= $gmdate["tm_wday"] * 24 * 60 * 60; // back up to start of week + + $date += $week * 7 * 24 * 60 * 60; // go to correct week nr + $date += $day * 24 * 60 * 60; + $date += $hour * 60 * 60; + + $gmdate = $this->gmtime($date); + + // if we are in the next month, then back up a week, because week '5' means + // 'last week of month' + + if($gmdate["tm_mon"]+1 != $month) + $date -= 7 * 24 * 60 * 60; + + return $date; + } + + /** + * getTimezone gives the timezone offset (in minutes) of the given + * local date/time according to the given TZ info + */ + function getTimezone($tz, $date) + { + // No timezone -> GMT (+0) + if(!isset($tz["timezone"])) + return 0; + + $dst = false; + $gmdate = $this->gmtime($date); + + $dststart = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dststartmonth"], $tz["dststartweek"], 0, $tz["dststarthour"]); + $dstend = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dstendmonth"], $tz["dstendweek"], 0, $tz["dstendhour"]); + + if($dststart <= $dstend) { + // Northern hemisphere, eg DST is during Mar-Oct + if($date > $dststart && $date < $dstend) { + $dst = true; + } + } else { + // Southern hemisphere, eg DST is during Oct-Mar + if($date < $dstend || $date > $dststart) { + $dst = true; + } + } + + if($dst) { + return $tz["timezone"] + $tz["timezonedst"]; + } else { + return $tz["timezone"]; + } + } + + /** + * getWeekNr() returns the week nr of the month (ie first week of february is 1) + */ + function getWeekNr($date) + { + $gmdate = gmtime($date); + $gmdate["tm_mday"] = 0; + return strftime("%W", $date) - strftime("%W", gmmktime($gmdate)) + 1; + } + + /** + * parseTimezone parses the timezone as specified in named property 0x8233 + * in Outlook calendar messages. Returns the timezone in minutes negative + * offset (GMT +2:00 -> -120) + */ + function parseTimezone($data) + { + if(strlen($data) < 48) + return; + + $tz = unpack("ltimezone/lunk/ltimezonedst/lunk/ldstendmonth/vdstendweek/vdstendhour/lunk/lunk/vunk/ldststartmonth/vdststartweek/vdststarthour/lunk/vunk", $data); + return $tz; + } + + function getTimezoneData($tz) + { + $data = pack("lllllvvllvlvvlv", $tz["timezone"], 0, $tz["timezonedst"], 0, $tz["dstendmonth"], $tz["dstendweek"], $tz["dstendhour"], 0, 0, 0, $tz["dststartmonth"], $tz["dststartweek"], $tz["dststarthour"], 0 ,0); + + return $data; + } + + /** + * createTimezone creates the timezone as specified in the named property 0x8233 + * see also parseTimezone() + * $tz is an array with the timezone data + */ + function createTimezone($tz) + { + $data = pack("lxxxxlxxxxlvvxxxxxxxxxxlvvxxxxxx", + $tz["timezone"], + array_key_exists("timezonedst",$tz)?$tz["timezonedst"]:0, + array_key_exists("dstendmonth",$tz)?$tz["dstendmonth"]:0, + array_key_exists("dstendweek",$tz)?$tz["dstendweek"]:0, + array_key_exists("dstendhour",$tz)?$tz["dstendhour"]:0, + array_key_exists("dststartmonth",$tz)?$tz["dststartmonth"]:0, + array_key_exists("dststartweek",$tz)?$tz["dststartweek"]:0, + array_key_exists("dststarthour",$tz)?$tz["dststarthour"]:0 + ); + + return $data; + } + + /** + * toGMT returns a timestamp in GMT time for the time and timezone given + */ + function toGMT($tz, $date) { + if(!isset($tz['timezone'])) + return $date; + $offset = $this->getTimezone($tz, $date); + + return $date + $offset * 60; + } + + /** + * fromGMT returns a timestamp in the local timezone given from the GMT time given + */ + function fromGMT($tz, $date) { + $offset = $this->getTimezone($tz, $date); + + return $date - $offset * 60; + } + + /** + * Function to get timestamp of the beginning of the day of the timestamp given + * @param date $date + * @return date timestamp referring to same day but at 00:00:00 + */ + function dayStartOf($date) + { + $time1 = $this->gmtime($date); + + return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, $time1["tm_mday"], $time1["tm_year"] + 1900); + } + + /** + * Function to get timestamp of the beginning of the month of the timestamp given + * @param date $date + * @return date Timestamp referring to same month but on the first day, and at 00:00:00 + */ + function monthStartOf($date) + { + $time1 = $this->gmtime($date); + + return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, 1, $time1["tm_year"] + 1900); + } + + /** + * Function to get timestamp of the beginning of the year of the timestamp given + * @param date $date + * @return date Timestamp referring to the same year but on Jan 01, at 00:00:00 + */ + function yearStartOf($date) + { + $time1 = $this->gmtime($date); + + return gmmktime(0, 0, 0, 1, 1, $time1["tm_year"] + 1900); + } + + + /** + * Function which returns the items in a given interval. This included expansion of the recurrence and + * processing of exceptions (modified and deleted). + * + * @param string $entryid the entryid of the message + * @param array $props the properties of the message + * @param date $start start time of the interval (GMT) + * @param date $end end time of the interval (GMT) + */ + function getItems($start, $end, $limit = 0, $remindersonly = false) + { + $items = array(); + + if(isset($this->recur)) { + + // Optimization: remindersonly and default reminder is off; since only exceptions with reminder set will match, just look which + // exceptions are in range and have a reminder set + if($remindersonly && (!isset($this->messageprops[$this->proptags["reminder"]]) || $this->messageprops[$this->proptags["reminder"]] == false)) { + // Sort exceptions by start time + uasort($this->recur["changed_occurences"], array($this, "sortExceptionStart")); + + // Loop through all changed exceptions + foreach($this->recur["changed_occurences"] as $exception) { + // Check reminder set + if(!isset($exception["reminder"]) || $exception["reminder"] == false) + continue; + + // Convert to GMT + $occstart = $this->toGMT($this->tz, $exception["start"]); // seb changed $tz to $this->tz + $occend = $this->toGMT($this->tz, $exception["end"]); // seb changed $tz to $this->tz + + // Check range criterium + if($occstart > $end || $occend < $start) + continue; + + // OK, add to items. + array_push($items, $this->getExceptionProperties($exception)); + if($limit && (count($items) == $limit)) + break; + } + + uasort($items, array($this, "sortStarttime")); + + return $items; + } + + // From here on, the dates of the occurrences are calculated in local time, so the days we're looking + // at are calculated from the local time dates of $start and $end + + if ($this->recur['regen'] && isset($this->action['datecompleted'])) { + $daystart = $this->dayStartOf($this->action['datecompleted']); + } else { + $daystart = $this->dayStartOf($this->recur["start"]); // start on first day of occurrence + } + + // Calculate the last day on which we want to be looking at a recurrence; this is either the end of the view + // or the end of the recurrence, whichever comes first + if($end > $this->toGMT($this->tz, $this->recur["end"])) { + $rangeend = $this->toGMT($this->tz, $this->recur["end"]); + } else { + $rangeend = $end; + } + + $dayend = $this->dayStartOf($this->fromGMT($this->tz, $rangeend)); + + // Loop through the entire recurrence range of dates, and check for each occurrence whether it is in the view range. + + switch($this->recur["type"]) + { + case 10: + // Daily + if($this->recur["everyn"] <= 0) + $this->recur["everyn"] = 1440; + + if($this->recur["subtype"] == 0) { + // Every Nth day + for($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * $this->recur["everyn"]) { + $this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); + } + } else { + // Every workday + for($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * 1440) + { + $nowtime = $this->gmtime($now); + if ($nowtime["tm_wday"] > 0 && $nowtime["tm_wday"] < 6) { // only add items in the given timespace + $this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); + } + } + } + break; + case 11: + // Weekly + if($this->recur["everyn"] <= 0) + $this->recur["everyn"] = 1; + + // If sliding flag is set then move to 'n' weeks + if ($this->recur['regen']) $daystart += (60 * 60 * 24 * 7 * $this->recur["everyn"]); + + for($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += (60 * 60 * 24 * 7 * $this->recur["everyn"])) + { + if ($this->recur['regen']) { + $this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); + } else { + // Loop through the whole following week to the first occurrence of the week, add each day that is specified + for($wday = 0; $wday < 7; $wday++) + { + $daynow = $now + $wday * 60 * 60 * 24; + //checks weather the next coming day in recurring pattern is less than or equal to end day of the recurring item + if ($daynow <= $dayend){ + $nowtime = $this->gmtime($daynow); // Get the weekday of the current day + if(($this->recur["weekdays"] &(1 << $nowtime["tm_wday"]))) { // Selected ? + $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); + } + } + } + } + } + break; + case 12: + // Monthly + if($this->recur["everyn"] <= 0) + $this->recur["everyn"] = 1; + + // Loop through all months from start to end of occurrence, starting at beginning of first month + for($now = $this->monthStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60 ) + { + if(isset($this->recur["monthday"]) &&($this->recur['monthday'] != "undefined") && !$this->recur['regen']) { // Day M of every N months + $difference = 1; + if ($this->daysInMonth($now, $this->recur["everyn"]) < $this->recur["monthday"]) { + $difference = $this->recur["monthday"] - $this->daysInMonth($now, $this->recur["everyn"]) + 1; + } + $daynow = $now + (($this->recur["monthday"] - $difference) * 24 * 60 * 60); + //checks weather the next coming day in recurrence pattern is less than or equal to end day of the recurring item + if ($daynow <= $dayend){ + $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); + } + } + else if(isset($this->recur["nday"]) && isset($this->recur["weekdays"])) { // Nth [weekday] of every N months + // Sanitize input + if($this->recur["weekdays"] == 0) + $this->recur["weekdays"] = 1; + + // If nday is not set to the last day in the month + if ($this->recur["nday"] < 5) { + // keep the track of no. of time correct selection pattern(like 2nd weekday, 4th fiday, etc.)is matched + $ndaycounter = 0; + // Find matching weekday in this month + for($day = 0; $day < $this->daysInMonth($now, 1); $day++) + { + $daynow = $now + $day * 60 * 60 * 24; + $nowtime = $this->gmtime($daynow); // Get the weekday of the current day + + if($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ? + $ndaycounter ++; + } + // check the selected pattern is same as asked Nth weekday,If so set the firstday + if($this->recur["nday"] == $ndaycounter){ + $firstday = $day; + break; + } + } + // $firstday is the day of the month on which the asked pattern of nth weekday matches + $daynow = $now + $firstday * 60 * 60 * 24; + }else{ + // Find last day in the month ($now is the firstday of the month) + $NumDaysInMonth = $this->daysInMonth($now, 1); + $daynow = $now + (($NumDaysInMonth-1) * 24*60*60); + + $nowtime = $this->gmtime($daynow); + while (($this->recur["weekdays"] & (1 << $nowtime["tm_wday"]))==0){ + $daynow -= 86400; + $nowtime = $this->gmtime($daynow); + } + } + + /** + * checks weather the next coming day in recurrence pattern is less than or equal to end day of the * recurring item.Also check weather the coming day in recurrence pattern is greater than or equal to start * of recurring pattern, so that appointment that fall under the recurrence range are only displayed. + */ + if ($daynow <= $dayend && $daynow >= $daystart){ + $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz , $remindersonly); + } + } else if ($this->recur['regen']) { + $next_month_start = $now + ($this->daysInMonth($now, 1) * 24 * 60 * 60); + $now = $daystart +($this->daysInMonth($next_month_start, $this->recur['everyn']) * 24 * 60 * 60); + + if ($now <= $dayend) { + $this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); + } + } + } + break; + case 13: + // Yearly + if($this->recur["everyn"] <= 0) + $this->recur["everyn"] = 12; + + for($now = $this->yearStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60 ) + { + if(isset($this->recur["monthday"]) && !$this->recur['regen']) { // same as monthly, but in a specific month + // recur["month"] is in minutes since the beginning of the year + $month = $this->monthOfYear($this->recur["month"]); // $month is now month of year [0..11] + $monthday = $this->recur["monthday"]; // $monthday is day of the month [1..31] + $monthstart = $now + $this->daysInMonth($now, $month) * 24 * 60 * 60; // $monthstart is the timestamp of the beginning of the month + if($monthday > $this->daysInMonth($monthstart, 1)) + $monthday = $this->daysInMonth($monthstart, 1); // Cap $monthday on month length (eg 28 feb instead of 29 feb) + $daynow = $monthstart + ($monthday-1) * 24 * 60 * 60; + $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); + } + else if(isset($this->recur["nday"]) && isset($this->recur["weekdays"])) { // Nth [weekday] in month X of every N years + + // Go the correct month + $monthnow = $now + $this->daysInMonth($now, $this->monthOfYear($this->recur["month"])) * 24 * 60 * 60; + + // Find first matching weekday in this month + for($wday = 0; $wday < 7; $wday++) + { + $daynow = $monthnow + $wday * 60 * 60 * 24; + $nowtime = $this->gmtime($daynow); // Get the weekday of the current day + + if($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ? + $firstday = $wday; + break; + } + } + + // Same as above (monthly) + $daynow = $monthnow + ($firstday + ($this->recur["nday"]-1)*7) * 60 * 60 * 24; + + while($this->monthStartOf($daynow) != $this->monthStartOf($monthnow)) { + $daynow -= 7 * 60 * 60 * 24; + } + + $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); + } else if ($this->recur['regen']) { + $year_starttime = $this->gmtime($now); + $is_next_leapyear = $this->isLeapYear($year_starttime['tm_year'] + 1900 + 1); // +1 next year + $now = $daystart + ($is_next_leapyear ? 31622400 /* Leap year in seconds */ : 31536000 /*year in seconds*/); + + if ($now <= $dayend) { + $this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); + } + } + } + } + //to get all exception items + if (!empty($this->recur['changed_occurences'])) + $this->processExceptionItems($items, $start, $end); + } + + // sort items on starttime + usort($items, array($this, "sortStarttime")); + + // Return the MAPI-compatible list of items for this object + return $items; + } + + function sortStarttime($a, $b) + { + $aTime = $a[$this->proptags["startdate"]]; + $bTime = $b[$this->proptags["startdate"]]; + + return $aTime==$bTime?0:($aTime>$bTime?1:-1); + } + + /** + * daysInMonth + * + * Returns the number of days in the upcoming number of months. If you specify 1 month as + * $months it will give you the number of days in the month of $date. If you specify more it + * will also count the days in the upcomming months and add that to the number of days. So + * if you have a date in march and you specify $months as 2 it will return 61. + * @param Integer $date Specified date as timestamp from which you want to know the number + * of days in the month. + * @param Integer $months Number of months you want to know the number of days in. + * @returns Integer Number of days in the specified amount of months. + */ + function daysInMonth($date, $months) { + $days = 0; + + for($i=0;$i<$months;$i++) { + $days += date("t", $date + $days * 24 * 60 * 60); + } + + return $days; + } + + // Converts MAPI-style 'minutes' into the month of the year [0..11] + function monthOfYear($minutes) { + $d = gmmktime(0,0,0,1,1,2001); // The year 2001 was a non-leap year, and the minutes provided are always in non-leap-year-minutes + + $d += $minutes*60; + + $dtime = $this->gmtime($d); + + return $dtime["tm_mon"]; + } + + function sortExceptionStart($a, $b) + { + return $a["start"] == $b["start"] ? 0 : ($a["start"] > $b["start"] ? 1 : -1 ); + } + } +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapi/class.freebusypublish.php b/sources/backend/zarafa/mapi/class.freebusypublish.php new file mode 100644 index 0000000..47d62bf --- /dev/null +++ b/sources/backend/zarafa/mapi/class.freebusypublish.php @@ -0,0 +1,397 @@ +. + * + */ + + +include_once('backend/zarafa/mapi/class.recurrence.php'); + +class FreeBusyPublish { + + var $session; + var $calendar; + var $entryid; + var $starttime; + var $length; + var $store; + var $proptags; + + /** + * Constuctor + * + * @param mapi_session $session MAPI Session + * @param mapi_folder $calendar Calendar to publish + * @param string $entryid AddressBook Entry ID for the user we're publishing for + */ + + + function FreeBusyPublish($session, $store, $calendar, $entryid) + { + $properties["entryid"] = PR_ENTRYID; + $properties["parent_entryid"] = PR_PARENT_ENTRYID; + $properties["message_class"] = PR_MESSAGE_CLASS; + $properties["icon_index"] = PR_ICON_INDEX; + $properties["subject"] = PR_SUBJECT; + $properties["display_to"] = PR_DISPLAY_TO; + $properties["importance"] = PR_IMPORTANCE; + $properties["sensitivity"] = PR_SENSITIVITY; + $properties["startdate"] = "PT_SYSTIME:PSETID_Appointment:0x820d"; + $properties["duedate"] = "PT_SYSTIME:PSETID_Appointment:0x820e"; + $properties["recurring"] = "PT_BOOLEAN:PSETID_Appointment:0x8223"; + $properties["recurring_data"] = "PT_BINARY:PSETID_Appointment:0x8216"; + $properties["busystatus"] = "PT_LONG:PSETID_Appointment:0x8205"; + $properties["label"] = "PT_LONG:PSETID_Appointment:0x8214"; + $properties["alldayevent"] = "PT_BOOLEAN:PSETID_Appointment:0x8215"; + $properties["private"] = "PT_BOOLEAN:PSETID_Common:0x8506"; + $properties["meeting"] = "PT_LONG:PSETID_Appointment:0x8217"; + $properties["startdate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8235"; + $properties["enddate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8236"; + $properties["location"] = "PT_STRING8:PSETID_Appointment:0x8208"; + $properties["duration"] = "PT_LONG:PSETID_Appointment:0x8213"; + $properties["responsestatus"] = "PT_LONG:PSETID_Appointment:0x8218"; + $properties["reminder"] = "PT_BOOLEAN:PSETID_Common:0x8503"; + $properties["reminder_minutes"] = "PT_LONG:PSETID_Common:0x8501"; + $properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a"; + $properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586"; + $properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords"; + $properties["reminder_time"] = "PT_SYSTIME:PSETID_Common:0x8502"; + $properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516"; + $properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517"; + $properties["basedate"] = "PT_SYSTIME:PSETID_Appointment:0x8228"; + $properties["timezone_data"] = "PT_BINARY:PSETID_Appointment:0x8233"; + $this->proptags = getPropIdsFromStrings($store, $properties); + + $this->session = $session; + $this->calendar = $calendar; + $this->entryid = $entryid; + $this->store = $store; + } + + /** + * Publishes the infomation + * @paam timestamp $starttime Time from which to publish data (usually now) + * @paam integer $length Amount of seconds from $starttime we should publish + */ + function publishFB($starttime, $length) { + $start = $starttime; + $end = $starttime + $length; + + // Get all the items in the calendar that we need + + $calendaritems = Array(); + + $restrict = Array(RES_OR, + Array( + // OR + // (item[start] >= start && item[start] <= end) + Array(RES_AND, + Array( + Array(RES_PROPERTY, + Array(RELOP => RELOP_GE, + ULPROPTAG => $this->proptags["startdate"], + VALUE => $start + ) + ), + Array(RES_PROPERTY, + Array(RELOP => RELOP_LE, + ULPROPTAG => $this->proptags["startdate"], + VALUE => $end + ) + ) + ) + ), + // OR + // (item[end] >= start && item[end] <= end) + Array(RES_AND, + Array( + Array(RES_PROPERTY, + Array(RELOP => RELOP_GE, + ULPROPTAG => $this->proptags["duedate"], + VALUE => $start + ) + ), + Array(RES_PROPERTY, + Array(RELOP => RELOP_LE, + ULPROPTAG => $this->proptags["duedate"], + VALUE => $end + ) + ) + ) + ), + // OR + // (item[start] < start && item[end] > end) + Array(RES_AND, + Array( + Array(RES_PROPERTY, + Array(RELOP => RELOP_LT, + ULPROPTAG => $this->proptags["startdate"], + VALUE => $start + ) + ), + Array(RES_PROPERTY, + Array(RELOP => RELOP_GT, + ULPROPTAG => $this->proptags["duedate"], + VALUE => $end + ) + ) + ) + ), + // OR + Array(RES_OR, + Array( + // OR + // (EXIST(ecurrence_enddate_property) && item[isRecurring] == true && item[end] >= start) + Array(RES_AND, + Array( + Array(RES_EXIST, + Array(ULPROPTAG => $this->proptags["enddate_recurring"], + ) + ), + Array(RES_PROPERTY, + Array(RELOP => RELOP_EQ, + ULPROPTAG => $this->proptags["recurring"], + VALUE => true + ) + ), + Array(RES_PROPERTY, + Array(RELOP => RELOP_GE, + ULPROPTAG => $this->proptags["enddate_recurring"], + VALUE => $start + ) + ) + ) + ), + // OR + // (!EXIST(ecurrence_enddate_property) && item[isRecurring] == true && item[start] <= end) + Array(RES_AND, + Array( + Array(RES_NOT, + Array( + Array(RES_EXIST, + Array(ULPROPTAG => $this->proptags["enddate_recurring"] + ) + ) + ) + ), + Array(RES_PROPERTY, + Array(RELOP => RELOP_LE, + ULPROPTAG => $this->proptags["startdate"], + VALUE => $end + ) + ), + Array(RES_PROPERTY, + Array(RELOP => RELOP_EQ, + ULPROPTAG => $this->proptags["recurring"], + VALUE => true + ) + ) + ) + ) + ) + ) // EXISTS OR + ) + ); // global OR + + $contents = mapi_folder_getcontentstable($this->calendar); + mapi_table_restrict($contents, $restrict); + + while(1) { + $rows = mapi_table_queryrows($contents, array_values($this->proptags), 0, 50); + + if(!is_array($rows)) + break; + + if(empty($rows)) + break; + + foreach ($rows as $row) { + $occurrences = Array(); + if(isset($row[$this->proptags['recurring']]) && $row[$this->proptags['recurring']]) { + $recur = new Recurrence($this->store, $row); + + $occurrences = $recur->getItems($starttime, $starttime + $length); + } else { + $occurrences[] = $row; + } + + $calendaritems = array_merge($calendaritems, $occurrences); + } + } + + // $calendaritems now contains all the calendar items in the specified time + // frame. We now need to merge these into a flat array of begin/end/status + // objects. This also filters out all the 'free' items (status 0) + + $freebusy = $this->mergeItemsFB($calendaritems); + + // $freebusy now contains the start, end and status of all items, merged. + + // Get the FB interface + try { + $fbsupport = mapi_freebusysupport_open($this->session, $this->store); + } catch (MAPIException $e) { + if($e->getCode() == MAPI_E_NOT_FOUND) { + $e->setHandled(); + if(function_exists("dump")) { + dump("Error in opening freebusysupport object."); + } + } + } + + // Open updater for this user + if(isset($fbsupport) && $fbsupport) { + $updaters = mapi_freebusysupport_loadupdate($fbsupport, Array($this->entryid)); + + $updater = $updaters[0]; + + // Send the data + mapi_freebusyupdate_reset($updater); + mapi_freebusyupdate_publish($updater, $freebusy); + mapi_freebusyupdate_savechanges($updater, $start-24*60*60, $end); + + // We're finished + mapi_freebusysupport_close($fbsupport); + } + else + ZLog::Write(LOGLEVEL_WARN, "FreeBusyPublish is not available"); + } + + /** + * Sorts by timestamp, if equal, then end before start + */ + function cmp($a, $b) + { + if ($a["time"] == $b["time"]) { + if($a["type"] < $b["type"]) + return 1; + if($a["type"] > $b["type"]) + return -1; + return 0; + } + return ($a["time"] > $b["time"] ? 1 : -1); + } + + /** + * Function mergeItems + * @author Steve Hardy + */ + function mergeItemsFB($items) + { + $merged = Array(); + $timestamps = Array(); + $csubj = Array(); + $cbusy = Array(); + $level = 0; + $laststart = null; + + foreach($items as $item) + { + $ts["type"] = 0; + $ts["time"] = $item[$this->proptags["startdate"]]; + $ts["subject"] = $item[PR_SUBJECT]; + $ts["status"] = (isset($item[$this->proptags["busystatus"]])) ? $item[$this->proptags["busystatus"]] : 0; //ZP-197 + $timestamps[] = $ts; + + $ts["type"] = 1; + $ts["time"] = $item[$this->proptags["duedate"]]; + $ts["subject"] = $item[PR_SUBJECT]; + $ts["status"] = (isset($item[$this->proptags["busystatus"]])) ? $item[$this->proptags["busystatus"]] : 0; //ZP-197 + $timestamps[] = $ts; + } + + usort($timestamps, Array($this, "cmp")); + $laststart = 0; // seb added + + foreach($timestamps as $ts) + { + switch ($ts["type"]) + { + case 0: // Start + if ($level != 0 && $laststart != $ts["time"]) + { + $newitem["start"] = $laststart; + $newitem["end"] = $ts["time"]; + $newitem["subject"] = join(",", $csubj); + $newitem["status"] = !empty($cbusy) ? max($cbusy) : 0; + if($newitem["status"] > 0) + $merged[] = $newitem; + } + + $level++; + + $csubj[] = $ts["subject"]; + $cbusy[] = $ts["status"]; + + $laststart = $ts["time"]; + break; + case 1: // End + if ($laststart != $ts["time"]) + { + $newitem["start"] = $laststart; + $newitem["end"] = $ts["time"]; + $newitem["subject"] = join(",", $csubj); + $newitem["status"] = !empty($cbusy) ? max($cbusy) : 0; + if($newitem["status"] > 0) + $merged[] = $newitem; + } + + $level--; + + array_splice($csubj, array_search($ts["subject"], $csubj, 1), 1); + array_splice($cbusy, array_search($ts["status"], $cbusy, 1), 1); + + $laststart = $ts["time"]; + break; + } + } + + return $merged; + } + +} +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapi/class.mapiexception.php b/sources/backend/zarafa/mapi/class.mapiexception.php new file mode 100644 index 0000000..c15e8fe --- /dev/null +++ b/sources/backend/zarafa/mapi/class.mapiexception.php @@ -0,0 +1,107 @@ +. + * + */ + + + /** + * MAPIException + * if enabled using mapi_enable_exceptions then php-ext can throw exceptions when + * any error occurs in mapi calls. this exception will only be thrown when severity bit is set in + * error code that means it will be thrown only for mapi errors not for mapi warnings. + */ + // FatalException will trigger a HTTP return code 500 to the mobile + class MAPIException extends FatalException + { + /** + * Function will return display message of exception if its set by the calle. + * if it is not set then we are generating some default display messages based + * on mapi error code. + * @return string returns error-message that should be sent to client to display. + */ + public function getDisplayMessage() + { + if(!empty($this->displayMessage)) + return $this->displayMessage; + + switch($this->getCode()) + { + case MAPI_E_NO_ACCESS: + return _("You have insufficient privileges to open this object."); + case MAPI_E_LOGON_FAILED: + case MAPI_E_UNCONFIGURED: + return _("Logon Failed. Please check your username/password."); + case MAPI_E_NETWORK_ERROR: + return _("Can not connect to Zarafa server."); + case MAPI_E_UNKNOWN_ENTRYID: + return _("Can not open object with provided id."); + case MAPI_E_NO_RECIPIENTS: + return _("There are no recipients in the message."); + case MAPI_E_NOT_FOUND: + return _("Can not find object."); + case MAPI_E_INTERFACE_NOT_SUPPORTED: + case MAPI_E_INVALID_PARAMETER: + case MAPI_E_INVALID_ENTRYID: + case MAPI_E_INVALID_OBJECT: + case MAPI_E_TOO_COMPLEX: + case MAPI_E_CORRUPT_DATA: + case MAPI_E_END_OF_SESSION: + case MAPI_E_AMBIGUOUS_RECIP: + case MAPI_E_COLLISION: + case MAPI_E_UNCONFIGURED: + default : + return sprintf(_("Unknown MAPI Error: %s"), get_mapi_error_name($this->getCode())); + } + } + } + + // Tell the PHP extension which exception class to instantiate + if (function_exists('mapi_enable_exceptions')) { + //mapi_enable_exceptions("mapiexception"); + } +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapi/class.meetingrequest.php b/sources/backend/zarafa/mapi/class.meetingrequest.php new file mode 100644 index 0000000..6cfe0dd --- /dev/null +++ b/sources/backend/zarafa/mapi/class.meetingrequest.php @@ -0,0 +1,3198 @@ +. + * + */ + +class Meetingrequest { + /* + * NOTE + * + * This class is designed to modify and update meeting request properties + * and to search for linked appointments in the calendar. It does not + * - set standard properties like subject or location + * - commit property changes through savechanges() (except in accept() and decline()) + * + * To set all the other properties, just handle the item as any other appointment + * item. You aren't even required to set those properties before or after using + * this class. If you update properties before REsending a meeting request (ie with + * a time change) you MUST first call updateMeetingRequest() so the internal counters + * can be updated. You can then submit the message any way you like. + * + */ + + /* + * How to use + * ---------- + * + * Sending a meeting request: + * - Create appointment item as normal, but as 'tentative' + * (this is the state of the item when the receiving user has received but + * not accepted the item) + * - Set recipients as normally in e-mails + * - Create Meetingrequest class instance + * - Call setMeetingRequest(), this turns on all the meeting request properties in the + * calendar item + * - Call sendMeetingRequest(), this sends a copy of the item with some extra properties + * + * Updating a meeting request: + * - Create Meetingrequest class instance + * - Call updateMeetingRequest(), this updates the counters + * - Call sendMeetingRequest() + * + * Clicking on a an e-mail: + * - Create Meetingrequest class instance + * - Check isMeetingRequest(), if true: + * - Check isLocalOrganiser(), if true then ignore the message + * - Check isInCalendar(), if not call doAccept(true, false, false). This adds the item in your + * calendar as tentative without sending a response + * - Show Accept, Tentative, Decline buttons + * - When the user presses Accept, Tentative or Decline, call doAccept(false, true, true), + * doAccept(true, true, true) or doDecline(true) respectively to really accept or decline and + * send the response. This will remove the request from your inbox. + * - Check isMeetingRequestResponse, if true: + * - Check isLocalOrganiser(), if not true then ignore the message + * - Call processMeetingRequestResponse() + * This will update the trackstatus of all recipients, and set the item to 'busy' + * when all the recipients have accepted. + * - Check isMeetingCancellation(), if true: + * - Check isLocalOrganiser(), if true then ignore the message + * - Check isInCalendar(), if not, then ignore + * Call processMeetingCancellation() + * - Show 'Remove item' button to user + * - When userpresses button, call doCancel(), which removes the item from your + * calendar and deletes the message + */ + + // All properties for a recipient that are interesting + var $recipprops = Array(PR_ENTRYID, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_RECIPIENT_ENTRYID, PR_RECIPIENT_TYPE, PR_SEND_INTERNET_ENCODING, PR_SEND_RICH_INFO, PR_RECIPIENT_DISPLAY_NAME, PR_ADDRTYPE, PR_DISPLAY_TYPE, PR_RECIPIENT_TRACKSTATUS, PR_RECIPIENT_TRACKSTATUS_TIME, PR_RECIPIENT_FLAGS, PR_ROWID, PR_OBJECT_TYPE, PR_SEARCH_KEY); + + /** + * Indication whether the setting of resources in a Meeting Request is success (false) or if it + * has failed (integer). + */ + var $errorSetResource; + + /** + * Constructor + * + * Takes a store and a message. The message is an appointment item + * that should be converted into a meeting request or an incoming + * e-mail message that is a meeting request. + * + * The $session variable is optional, but required if the following features + * are to be used: + * + * - Sending meeting requests for meetings that are not in your own store + * - Sending meeting requests to resources, resource availability checking and resource freebusy updates + */ + + function Meetingrequest($store, $message, $session = false, $enableDirectBooking = true) + { + $this->store = $store; + $this->message = $message; + $this->session = $session; + // This variable string saves time information for the MR. + $this->meetingTimeInfo = false; + $this->enableDirectBooking = $enableDirectBooking; + + $properties["goid"] = "PT_BINARY:PSETID_Meeting:0x3"; + $properties["goid2"] = "PT_BINARY:PSETID_Meeting:0x23"; + $properties["type"] = "PT_STRING8:PSETID_Meeting:0x24"; + $properties["meetingrecurring"] = "PT_BOOLEAN:PSETID_Meeting:0x5"; + $properties["unknown2"] = "PT_BOOLEAN:PSETID_Meeting:0xa"; + $properties["attendee_critical_change"] = "PT_SYSTIME:PSETID_Meeting:0x1"; + $properties["owner_critical_change"] = "PT_SYSTIME:PSETID_Meeting:0x1a"; + $properties["meetingstatus"] = "PT_LONG:PSETID_Appointment:0x8217"; + $properties["responsestatus"] = "PT_LONG:PSETID_Appointment:0x8218"; + $properties["unknown6"] = "PT_LONG:PSETID_Meeting:0x4"; + $properties["replytime"] = "PT_SYSTIME:PSETID_Appointment:0x8220"; + $properties["usetnef"] = "PT_BOOLEAN:PSETID_Common:0x8582"; + $properties["recurrence_data"] = "PT_BINARY:PSETID_Appointment:0x8216"; + $properties["reminderminutes"] = "PT_LONG:PSETID_Common:0x8501"; + $properties["reminderset"] = "PT_BOOLEAN:PSETID_Common:0x8503"; + $properties["sendasical"] = "PT_BOOLEAN:PSETID_Appointment:0x8200"; + $properties["updatecounter"] = "PT_LONG:PSETID_Appointment:0x8201"; // AppointmentSequenceNumber + $properties["last_updatecounter"] = "PT_LONG:PSETID_Appointment:0x8203"; // AppointmentLastSequence + $properties["unknown7"] = "PT_LONG:PSETID_Appointment:0x8202"; + $properties["busystatus"] = "PT_LONG:PSETID_Appointment:0x8205"; + $properties["intendedbusystatus"] = "PT_LONG:PSETID_Appointment:0x8224"; + $properties["start"] = "PT_SYSTIME:PSETID_Appointment:0x820d"; + $properties["responselocation"] = "PT_STRING8:PSETID_Meeting:0x2"; + $properties["location"] = "PT_STRING8:PSETID_Appointment:0x8208"; + $properties["requestsent"] = "PT_BOOLEAN:PSETID_Appointment:0x8229"; // PidLidFInvited, MeetingRequestWasSent + $properties["startdate"] = "PT_SYSTIME:PSETID_Appointment:0x820d"; + $properties["duedate"] = "PT_SYSTIME:PSETID_Appointment:0x820e"; + $properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516"; + $properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517"; + $properties["recurring"] = "PT_BOOLEAN:PSETID_Appointment:0x8223"; + $properties["clipstart"] = "PT_SYSTIME:PSETID_Appointment:0x8235"; + $properties["clipend"] = "PT_SYSTIME:PSETID_Appointment:0x8236"; + $properties["start_recur_date"] = "PT_LONG:PSETID_Meeting:0xD"; // StartRecurTime + $properties["start_recur_time"] = "PT_LONG:PSETID_Meeting:0xE"; // StartRecurTime + $properties["end_recur_date"] = "PT_LONG:PSETID_Meeting:0xF"; // EndRecurDate + $properties["end_recur_time"] = "PT_LONG:PSETID_Meeting:0x10"; // EndRecurTime + $properties["is_exception"] = "PT_BOOLEAN:PSETID_Meeting:0xA"; // LID_IS_EXCEPTION + $properties["apptreplyname"] = "PT_STRING8:PSETID_Appointment:0x8230"; + // Propose new time properties + $properties["proposed_start_whole"] = "PT_SYSTIME:PSETID_Appointment:0x8250"; + $properties["proposed_end_whole"] = "PT_SYSTIME:PSETID_Appointment:0x8251"; + $properties["proposed_duration"] = "PT_LONG:PSETID_Appointment:0x8256"; + $properties["counter_proposal"] = "PT_BOOLEAN:PSETID_Appointment:0x8257"; + $properties["recurring_pattern"] = "PT_STRING8:PSETID_Appointment:0x8232"; + $properties["basedate"] = "PT_SYSTIME:PSETID_Appointment:0x8228"; + $properties["meetingtype"] = "PT_LONG:PSETID_Meeting:0x26"; + $properties["timezone_data"] = "PT_BINARY:PSETID_Appointment:0x8233"; + $properties["timezone"] = "PT_STRING8:PSETID_Appointment:0x8234"; + $properties["toattendeesstring"] = "PT_STRING8:PSETID_Appointment:0x823B"; + $properties["ccattendeesstring"] = "PT_STRING8:PSETID_Appointment:0x823C"; + $this->proptags = getPropIdsFromStrings($store, $properties); + } + + /** + * Sets the direct booking property. This is an alternative to the setting of the direct booking + * property through the constructor. However, setting it in the constructor is prefered. + * @param Boolean $directBookingSetting + * + */ + function setDirectBooking($directBookingSetting) + { + $this->enableDirectBooking = $directBookingSetting; + } + + /** + * Returns TRUE if the message pointed to is an incoming meeting request and should + * therefore be replied to with doAccept or doDecline() + */ + function isMeetingRequest() + { + $props = mapi_getprops($this->message, Array(PR_MESSAGE_CLASS)); + + if(isset($props[PR_MESSAGE_CLASS]) && $props[PR_MESSAGE_CLASS] == "IPM.Schedule.Meeting.Request") + return true; + } + + /** + * Returns TRUE if the message pointed to is a returning meeting request response + */ + function isMeetingRequestResponse() + { + $props = mapi_getprops($this->message, Array(PR_MESSAGE_CLASS)); + + if(isset($props[PR_MESSAGE_CLASS]) && strpos($props[PR_MESSAGE_CLASS], "IPM.Schedule.Meeting.Resp") === 0) + return true; + } + + /** + * Returns TRUE if the message pointed to is a cancellation request + */ + function isMeetingCancellation() + { + $props = mapi_getprops($this->message, Array(PR_MESSAGE_CLASS)); + + if(isset($props[PR_MESSAGE_CLASS]) && $props[PR_MESSAGE_CLASS] == "IPM.Schedule.Meeting.Canceled") + return true; + } + + + /** + * Process an incoming meeting request response as Delegate. This will updates the appointment + * in Organiser's calendar. + * @returns the entryids(storeid, parententryid, entryid, also basedate if response is occurrence) + * of corresponding meeting in Calendar + */ + function processMeetingRequestResponseAsDelegate() + { + if(!$this->isMeetingRequestResponse()) + return; + + $messageprops = mapi_getprops($this->message); + + $goid2 = $messageprops[$this->proptags['goid2']]; + + if(!isset($goid2) || !isset($messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS])) + return; + + // Find basedate in GlobalID(0x3), this can be a response for an occurrence + $basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]); + + if (isset($messageprops[PR_RCVD_REPRESENTING_NAME])) { + $delegatorStore = $this->getDelegatorStore($messageprops); + $userStore = $delegatorStore['store']; + $calFolder = $delegatorStore['calFolder']; + + if($calFolder){ + $calendaritems = $this->findCalendarItems($goid2, $calFolder); + + // $calendaritems now contains the ENTRYID's of all the calendar items to which + // this meeting request points. + + // Open the calendar items, and update all the recipients of the calendar item that match + // the email address of the response. + if (!empty($calendaritems)) { + return $this->processResponse($userStore, $calendaritems[0], $basedate, $messageprops); + }else{ + return false; + } + } + } + } + + + /** + * Process an incoming meeting request response. This updates the appointment + * in your calendar to show whether the user has accepted or declined. + * @returns the entryids(storeid, parententryid, entryid, also basedate if response is occurrence) + * of corresponding meeting in Calendar + */ + function processMeetingRequestResponse() + { + if(!$this->isLocalOrganiser()) + return; + + if(!$this->isMeetingRequestResponse()) + return; + + // Get information we need from the response message + $messageprops = mapi_getprops($this->message, Array( + $this->proptags['goid'], + $this->proptags['goid2'], + PR_OWNER_APPT_ID, + PR_SENT_REPRESENTING_EMAIL_ADDRESS, + PR_SENT_REPRESENTING_NAME, + PR_SENT_REPRESENTING_ADDRTYPE, + PR_SENT_REPRESENTING_ENTRYID, + PR_MESSAGE_DELIVERY_TIME, + PR_MESSAGE_CLASS, + PR_PROCESSED, + $this->proptags['proposed_start_whole'], + $this->proptags['proposed_end_whole'], + $this->proptags['proposed_duration'], + $this->proptags['counter_proposal'], + $this->proptags['attendee_critical_change'])); + + $goid2 = $messageprops[$this->proptags['goid2']]; + + if(!isset($goid2) || !isset($messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS])) + return; + + // Find basedate in GlobalID(0x3), this can be a response for an occurrence + $basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]); + + $calendaritems = $this->findCalendarItems($goid2); + + // $calendaritems now contains the ENTRYID's of all the calendar items to which + // this meeting request points. + + // Open the calendar items, and update all the recipients of the calendar item that match + // the email address of the response. + if (!empty($calendaritems)) { + return $this->processResponse($this->store, $calendaritems[0], $basedate, $messageprops); + }else{ + return false; + } + } + + /** + * Process every incoming MeetingRequest response.This updates the appointment + * in your calendar to show whether the user has accepted or declined. + *@param resource $store contains the userStore in which the meeting is created + *@param $entryid contains the ENTRYID of the calendar items to which this meeting request points. + *@param boolean $basedate if present the create an exception + *@param array $messageprops contains m3/17/2010essage properties. + *@return entryids(storeid, parententryid, entryid, also basedate if response is occurrence) of corresponding meeting in Calendar + */ + function processResponse($store, $entryid, $basedate, $messageprops) + { + $data = array(); + $senderentryid = $messageprops[PR_SENT_REPRESENTING_ENTRYID]; + $messageclass = $messageprops[PR_MESSAGE_CLASS]; + $deliverytime = $messageprops[PR_MESSAGE_DELIVERY_TIME]; + + // Open the calendar item, find the sender in the recipient table and update all the recipients of the calendar item that match + // the email address of the response. + $calendaritem = mapi_msgstore_openentry($store, $entryid); + $calendaritemProps = mapi_getprops($calendaritem, array($this->proptags['recurring'], PR_STORE_ENTRYID, PR_PARENT_ENTRYID, PR_ENTRYID, $this->proptags['updatecounter'])); + + $data["storeid"] = bin2hex($calendaritemProps[PR_STORE_ENTRYID]); + $data["parententryid"] = bin2hex($calendaritemProps[PR_PARENT_ENTRYID]); + $data["entryid"] = bin2hex($calendaritemProps[PR_ENTRYID]); + $data["basedate"] = $basedate; + $data["updatecounter"] = isset($calendaritemProps[$this->proptags['updatecounter']]) ? $calendaritemProps[$this->proptags['updatecounter']] : 0; + + /** + * Check if meeting is updated or not in organizer's calendar + */ + $data["meeting_updated"] = $this->isMeetingUpdated(); + + if(isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] == true) { + // meeting is already processed + return $data; + } else { + mapi_setprops($this->message, Array(PR_PROCESSED => true)); + mapi_savechanges($this->message); + } + + // if meeting is updated in organizer's calendar then we don't need to process + // old response + if($data['meeting_updated'] === true) { + return $data; + } + + // If basedate is found, then create/modify exception msg and do processing + if ($basedate && $calendaritemProps[$this->proptags['recurring']]) { + $recurr = new Recurrence($store, $calendaritem); + + // Copy properties from meeting request + $exception_props = mapi_getprops($this->message, array(PR_OWNER_APPT_ID, + $this->proptags['proposed_start_whole'], + $this->proptags['proposed_end_whole'], + $this->proptags['proposed_duration'], + $this->proptags['counter_proposal'] + )); + + // Create/modify exception + if($recurr->isException($basedate)) { + $recurr->modifyException($exception_props, $basedate); + } else { + // When we are creating an exception we need copy recipients from main recurring item + $recipTable = mapi_message_getrecipienttable($calendaritem); + $recips = mapi_table_queryallrows($recipTable, $this->recipprops); + + // Retrieve actual start/due dates from calendar item. + $exception_props[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate); + $exception_props[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate); + + $recurr->createException($exception_props, $basedate, false, $recips); + } + + mapi_message_savechanges($calendaritem); + + $attach = $recurr->getExceptionAttachment($basedate); + if ($attach) { + $recurringItem = $calendaritem; + $calendaritem = mapi_attach_openobj($attach, MAPI_MODIFY); + } else { + return false; + } + } + + // Get the recipients of the calendar item + $reciptable = mapi_message_getrecipienttable($calendaritem); + $recipients = mapi_table_queryallrows($reciptable, $this->recipprops); + + // FIXME we should look at the updatecounter property and compare it + // to the counter in the recipient to see if this update is actually + // newer than the status in the calendar item + $found = false; + + $totalrecips = 0; + $acceptedrecips = 0; + foreach($recipients as $recipient) { + $totalrecips++; + if(isset($recipient[PR_ENTRYID]) && $this->compareABEntryIDs($recipient[PR_ENTRYID],$senderentryid)) { + $found = true; + + /** + * If value of attendee_critical_change on meeting response mail is less than PR_RECIPIENT_TRACKSTATUS_TIME + * on the corresponding recipientRow of meeting then we ignore this response mail. + */ + if (isset($recipient[PR_RECIPIENT_TRACKSTATUS_TIME]) && ($messageprops[$this->proptags['attendee_critical_change']] < $recipient[PR_RECIPIENT_TRACKSTATUS_TIME])) { + continue; + } + + // The email address matches, update the row + $recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass); + $recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $messageprops[$this->proptags['attendee_critical_change']]; + + // If this is a counter proposal, set the proposal properties in the recipient row + if(isset($messageprops[$this->proptags['counter_proposal']]) && $messageprops[$this->proptags['counter_proposal']]){ + $recipient[PR_PROPOSENEWTIME_START] = $messageprops[$this->proptags['proposed_start_whole']]; + $recipient[PR_PROPOSENEWTIME_END] = $messageprops[$this->proptags['proposed_end_whole']]; + $recipient[PR_PROPOSEDNEWTIME] = $messageprops[$this->proptags['counter_proposal']]; + } + + mapi_message_modifyrecipients($calendaritem, MODRECIP_MODIFY, Array($recipient)); + } + if(isset($recipient[PR_RECIPIENT_TRACKSTATUS]) && $recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) + $acceptedrecips++; + } + + // If the recipient was not found in the original calendar item, + // then add the recpient as a new optional recipient + if(!$found) { + $recipient = Array(); + $recipient[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID]; + $recipient[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS]; + $recipient[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME]; + $recipient[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE]; + $recipient[PR_RECIPIENT_TYPE] = MAPI_CC; + $recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass); + $recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $deliverytime; + + // If this is a counter proposal, set the proposal properties in the recipient row + if(isset($messageprops[$this->proptags['counter_proposal']])){ + $recipient[PR_PROPOSENEWTIME_START] = $messageprops[$this->proptags['proposed_start_whole']]; + $recipient[PR_PROPOSENEWTIME_END] = $messageprops[$this->proptags['proposed_end_whole']]; + $recipient[PR_PROPOSEDNEWTIME] = $messageprops[$this->proptags['counter_proposal']]; + } + + mapi_message_modifyrecipients($calendaritem, MODRECIP_ADD, Array($recipient)); + $totalrecips++; + if($recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) + $acceptedrecips++; + } + +//TODO: Upate counter proposal number property on message +/* +If it is the first time this attendee has proposed a new date/time, increment the value of the PidLidAppointmentProposalNumber property on the organizer�s meeting object, by 0x00000001. If this property did not previously exist on the organizer�s meeting object, it MUST be set with a value of 0x00000001. +*/ + // If this is a counter proposal, set the counter proposal indicator boolean + if(isset($messageprops[$this->proptags['counter_proposal']])){ + $props = Array(); + if($messageprops[$this->proptags['counter_proposal']]){ + $props[$this->proptags['counter_proposal']] = true; + }else{ + $props[$this->proptags['counter_proposal']] = false; + } + + mapi_message_setprops($calendaritem, $props); + } + + mapi_message_savechanges($calendaritem); + if (isset($attach)) { + mapi_message_savechanges($attach); + mapi_message_savechanges($recurringItem); + } + + return $data; + } + + + /** + * Process an incoming meeting request cancellation. This updates the + * appointment in your calendar to show that the meeting has been cancelled. + */ + function processMeetingCancellation() + { + if($this->isLocalOrganiser()) + return; + + if(!$this->isMeetingCancellation()) + return; + + if(!$this->isInCalendar()) + return; + + $listProperties = $this->proptags; + $listProperties['subject'] = PR_SUBJECT; + $listProperties['sent_representing_name'] = PR_SENT_REPRESENTING_NAME; + $listProperties['sent_representing_address_type'] = PR_SENT_REPRESENTING_ADDRTYPE; + $listProperties['sent_representing_email_address'] = PR_SENT_REPRESENTING_EMAIL_ADDRESS; + $listProperties['sent_representing_entryid'] = PR_SENT_REPRESENTING_ENTRYID; + $listProperties['sent_representing_search_key'] = PR_SENT_REPRESENTING_SEARCH_KEY; + $listProperties['rcvd_representing_name'] = PR_RCVD_REPRESENTING_NAME; + $messageprops = mapi_getprops($this->message, $listProperties); + $store = $this->store; + + $goid = $messageprops[$this->proptags['goid']]; //GlobalID (0x3) + if(!isset($goid)) + return; + + if (isset($messageprops[PR_RCVD_REPRESENTING_NAME])){ + $delegatorStore = $this->getDelegatorStore($messageprops); + $store = $delegatorStore['store']; + $calFolder = $delegatorStore['calFolder']; + } else { + $calFolder = $this->openDefaultCalendar(); + } + + // First, find the items in the calendar by GOID + $calendaritems = $this->findCalendarItems($goid, $calFolder); + $basedate = $this->getBasedateFromGlobalID($goid); + + if ($basedate) { + // Calendaritems with GlobalID were not found, so find main recurring item using CleanGlobalID(0x23) + if (empty($calendaritems)) { + // This meeting req is of an occurrance + $goid2 = $messageprops[$this->proptags['goid2']]; + + // First, find the items in the calendar by GOID + $calendaritems = $this->findCalendarItems($goid2); + foreach($calendaritems as $entryid) { + // Open each calendar item and set the properties of the cancellation object + $calendaritem = mapi_msgstore_openentry($store, $entryid); + + if ($calendaritem){ + $calendaritemProps = mapi_getprops($calendaritem, array($this->proptags['recurring'])); + if ($calendaritemProps[$this->proptags['recurring']]){ + $recurr = new Recurrence($store, $calendaritem); + + // Set message class + $messageprops[PR_MESSAGE_CLASS] = 'IPM.Appointment'; + + if($recurr->isException($basedate)) + $recurr->modifyException($messageprops, $basedate); + else + $recurr->createException($messageprops, $basedate); + } + mapi_savechanges($calendaritem); + } + } + } + } + + if (!isset($calendaritem)) { + foreach($calendaritems as $entryid) { + // Open each calendar item and set the properties of the cancellation object + $calendaritem = mapi_msgstore_openentry($store, $entryid); + mapi_message_setprops($calendaritem, $messageprops); + mapi_savechanges($calendaritem); + } + } + } + + /** + * Returns true if the item is already in the calendar + */ + function isInCalendar() { + $messageprops = mapi_getprops($this->message, Array($this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_NAME)); + $goid = $messageprops[$this->proptags['goid']]; + if (isset($messageprops[$this->proptags['goid2']])) + $goid2 = $messageprops[$this->proptags['goid2']]; + + $basedate = $this->getBasedateFromGlobalID($goid); + + if (isset($messageprops[PR_RCVD_REPRESENTING_NAME])){ + $delegatorStore = $this->getDelegatorStore($messageprops); + $calFolder = $delegatorStore['calFolder']; + } else { + $calFolder = $this->openDefaultCalendar(); + } + /** + * If basedate is found in globalID, then there are two possibilities. + * case 1) User has only this occurrence OR + * case 2) User has recurring item and has received an update for an occurrence + */ + if ($basedate) { + // First try with GlobalID(0x3) (case 1) + $entryid = $this->findCalendarItems($goid, $calFolder); + // If not found then try with CleanGlobalID(0x23) (case 2) + if (!is_array($entryid) && isset($goid2)) + $entryid = $this->findCalendarItems($goid2, $calFolder); + } else if (isset($goid2)) { + $entryid = $this->findCalendarItems($goid2, $calFolder); + } + else + return false; + + return is_array($entryid); + } + + /** + * Accepts the meeting request by moving the item to the calendar + * and sending a confirmation message back to the sender. If $tentative + * is TRUE, then the item is accepted tentatively. After accepting, you + * can't use this class instance any more. The message is closed. If you + * specify TRUE for 'move', then the item is actually moved (from your + * inbox probably) to the calendar. If you don't, it is copied into + * your calendar. + *@param boolean $tentative true if user as tentative accepted the meeting + *@param boolean $sendresponse true if a response has to be send to organizer + *@param boolean $move true if the meeting request should be moved to the deleted items after processing + *@param string $newProposedStartTime contains starttime if user has proposed other time + *@param string $newProposedEndTime contains endtime if user has proposed other time + *@param string $basedate start of day of occurrence for which user has accepted the recurrent meeting + *@return string $entryid entryid of item which created/updated in calendar + */ + function doAccept($tentative, $sendresponse, $move, $newProposedStartTime=false, $newProposedEndTime=false, $body=false, $userAction = false, $store=false, $basedate = false) + { + if($this->isLocalOrganiser()) + return false; + + // Remove any previous calendar items with this goid and appt id + $messageprops = mapi_getprops($this->message, Array(PR_ENTRYID, PR_MESSAGE_CLASS, $this->proptags['goid'], $this->proptags['goid2'], PR_OWNER_APPT_ID, $this->proptags['updatecounter'], PR_PROCESSED, $this->proptags['recurring'], $this->proptags['intendedbusystatus'], PR_RCVD_REPRESENTING_NAME)); + + /** + * if this function is called automatically with meeting request object then there will be + * two possibilitites + * 1) meeting request is opened first time, in this case make a tentative appointment in + recipient's calendar + * 2) after this every subsequest request to open meeting request will not do any processing + */ + if($messageprops[PR_MESSAGE_CLASS] == "IPM.Schedule.Meeting.Request" && $userAction == false) { + if(isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] == true) { + // if meeting request is already processed then don't do anything + return false; + } else { + mapi_setprops($this->message, Array(PR_PROCESSED => true)); + mapi_message_savechanges($this->message); + } + } + + // If this meeting request is received by a delegate then open delegator's store. + if (isset($messageprops[PR_RCVD_REPRESENTING_NAME])) { + $delegatorStore = $this->getDelegatorStore($messageprops); + + $store = $delegatorStore['store']; + $calFolder = $delegatorStore['calFolder']; + } else { + $calFolder = $this->openDefaultCalendar(); + $store = $this->store; + } + + return $this->accept($tentative, $sendresponse, $move, $newProposedStartTime, $newProposedEndTime, $body, $userAction, $store, $calFolder, $basedate); + } + + function accept($tentative, $sendresponse, $move, $newProposedStartTime=false, $newProposedEndTime=false, $body=false, $userAction = false, $store, $calFolder, $basedate = false) + { + $messageprops = mapi_getprops($this->message); + $isDelegate = false; + + if (isset($messageprops[PR_DELEGATED_BY_RULE])) + $isDelegate = true; + + $goid = $messageprops[$this->proptags['goid2']]; + + // Retrieve basedate from globalID, if it is not recieved as argument + if (!$basedate) + $basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]); + + if ($sendresponse) + $this->createResponse($tentative ? olResponseTentative : olResponseAccepted, $newProposedStartTime, $newProposedEndTime, $body, $store, $basedate, $calFolder); + + $entryids = $this->findCalendarItems($goid, $calFolder); + + if(is_array($entryids)) { + // Only check the first, there should only be one anyway... + $previtem = mapi_msgstore_openentry($store, $entryids[0]); + $prevcounterprops = mapi_getprops($previtem, array($this->proptags['updatecounter'])); + + // Check if the existing item has an updatecounter that is lower than the request we are processing. If not, then we ignore this call, since the + // meeting request is out of date. + /* + if(message_counter < appointment_counter) do_nothing + if(message_counter == appointment_counter) do_something_if_the_user_tells_us (userAction == true) + if(message_counter > appointment_counter) do_something_even_automatically + */ + if(isset($prevcounterprops[$this->proptags['updatecounter']]) && $messageprops[$this->proptags['updatecounter']] < $prevcounterprops[$this->proptags['updatecounter']]) { + return false; + } else if(isset($prevcounterprops[$this->proptags['updatecounter']]) && $messageprops[$this->proptags['updatecounter']] == $prevcounterprops[$this->proptags['updatecounter']]) { + if($userAction == false && !$basedate) { + return false; + } + } + } + + // set counter proposal properties in calendar item when proposing new time + // @FIXME this can be moved before call to createResponse function so that function doesn't need to recalculate duration + $proposeNewTimeProps = array(); + if($newProposedStartTime && $newProposedEndTime) { + $proposeNewTimeProps[$this->proptags['proposed_start_whole']] = $newProposedStartTime; + $proposeNewTimeProps[$this->proptags['proposed_end_whole']] = $newProposedEndTime; + $proposeNewTimeProps[$this->proptags['proposed_duration']] = round($newProposedEndTime - $newProposedStartTime) / 60; + $proposeNewTimeProps[$this->proptags['counter_proposal']] = true; + } + + /** + * Further processing depends on what user is receiving. User can receive recurring item, a single occurrence or a normal meeting. + * 1) If meeting req is of recurrence then we find all the occurrence in calendar because in past user might have recivied one or few occurrences. + * 2) If single occurrence then find occurrence itself using globalID and if item is not found then user cleanGlobalID to find main recurring item + * 3) Normal meeting req are handled normally has they were handled previously. + * + * Also user can respond(accept/decline) to item either from previewpane or from calendar by opening the item. If user is responding the meeting from previewpane + * and that item is not found in calendar then item is move else item is opened and all properties, attachments and recipient are copied from meeting request. + * If user is responding from calendar then item is opened and properties are set such as meetingstatus, responsestatus, busystatus etc. + */ + if ($messageprops[PR_MESSAGE_CLASS] == "IPM.Schedule.Meeting.Request") { + // While processing the item mark it as read. + mapi_message_setreadflag($this->message, SUPPRESS_RECEIPT); + + // This meeting request item is recurring, so find all occurrences and saves them all as exceptions to this meeting request item. + if ($messageprops[$this->proptags['recurring']] == true) { + $calendarItem = false; + + // Find main recurring item based on GlobalID (0x3) + $items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder); + if (is_array($items)) { + foreach($items as $key => $entryid) + $calendarItem = mapi_msgstore_openentry($store, $entryid); + } + + // Recurring item not found, so create new meeting in Calendar + if (!$calendarItem) + $calendarItem = mapi_folder_createmessage($calFolder); + + // Copy properties + $props = mapi_getprops($this->message); + $props[PR_MESSAGE_CLASS] = 'IPM.Appointment'; + $props[$this->proptags['meetingstatus']] = olMeetingReceived; + // when we are automatically processing the meeting request set responsestatus to olResponseNotResponded + $props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded; + + if (isset($props[$this->proptags['intendedbusystatus']])) { + if($tentative && $props[$this->proptags['intendedbusystatus']] !== fbFree) { + $props[$this->proptags['busystatus']] = $tentative; + } else { + $props[$this->proptags['busystatus']] = $props[$this->proptags['intendedbusystatus']]; + } + // we already have intendedbusystatus value in $props so no need to copy it + } else { + $props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy; + } + + if($userAction) { + // if user has responded then set replytime + $props[$this->proptags['replytime']] = time(); + } + + mapi_setprops($calendarItem, $props); + + // Copy attachments too + $this->replaceAttachments($this->message, $calendarItem); + // Copy recipients too + $this->replaceRecipients($this->message, $calendarItem, $isDelegate); + + // Find all occurrences based on CleanGlobalID (0x23) + $items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder, true); + if (is_array($items)) { + // Save all existing occurrence as exceptions + foreach($items as $entryid) { + // Open occurrence + $occurrenceItem = mapi_msgstore_openentry($store, $entryid); + + // Save occurrence into main recurring item as exception + if ($occurrenceItem) { + $occurrenceItemProps = mapi_getprops($occurrenceItem, array($this->proptags['goid'], $this->proptags['recurring'])); + + // Find basedate of occurrence item + $basedate = $this->getBasedateFromGlobalID($occurrenceItemProps[$this->proptags['goid']]); + if ($basedate && $occurrenceItemProps[$this->proptags['recurring']] != true) + $this->acceptException($calendarItem, $occurrenceItem, $basedate, true, $tentative, $userAction, $store, $isDelegate); + } + } + } + mapi_savechanges($calendarItem); + if ($move) { + $wastebasket = $this->openDefaultWastebasket(); + mapi_folder_copymessages($calFolder, Array($props[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE); + } + $entryid = $props[PR_ENTRYID]; + } else { + /** + * This meeting request is not recurring, so can be an exception or normal meeting. + * If exception then find main recurring item and update exception + * If main recurring item is not found then put exception into Calendar as normal meeting. + */ + $calendarItem = false; + + // We found basedate in GlobalID of this meeting request, so this meeting request if for an occurrence. + if ($basedate) { + // Find main recurring item from CleanGlobalID of this meeting request + $items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder); + if (is_array($items)) { + foreach($items as $key => $entryid) { + $calendarItem = mapi_msgstore_openentry($store, $entryid); + } + } + + // Main recurring item is found, so now update exception + if ($calendarItem) { + $this->acceptException($calendarItem, $this->message, $basedate, $move, $tentative, $userAction, $store, $isDelegate); + $calendarItemProps = mapi_getprops($calendarItem, array(PR_ENTRYID)); + $entryid = $calendarItemProps[PR_ENTRYID]; + } + } + + if (!$calendarItem) { + $items = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder); + + if (is_array($items)) + mapi_folder_deletemessages($calFolder, $items); + + if ($move) { + // All we have to do is open the default calendar, + // set the mesage class correctly to be an appointment item + // and move it to the calendar folder + $sourcefolder = $this->openParentFolder(); + + /* create a new calendar message, and copy the message to there, + since we want to delete (move to wastebasket) the original message */ + $old_entryid = mapi_getprops($this->message, Array(PR_ENTRYID)); + $calmsg = mapi_folder_createmessage($calFolder); + mapi_copyto($this->message, array(), array(), $calmsg); /* includes attachments and recipients */ + /* release old message */ + $message = null; + + $calItemProps = Array(); + $calItemProps[PR_MESSAGE_CLASS] = "IPM.Appointment"; + + if (isset($messageprops[$this->proptags['intendedbusystatus']])) { + if($tentative && $messageprops[$this->proptags['intendedbusystatus']] !== fbFree) { + $calItemProps[$this->proptags['busystatus']] = $tentative; + } else { + $calItemProps[$this->proptags['busystatus']] = $messageprops[$this->proptags['intendedbusystatus']]; + } + $calItemProps[$this->proptags['intendedbusystatus']] = $messageprops[$this->proptags['intendedbusystatus']]; + } else { + $calItemProps[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy; + } + + // when we are automatically processing the meeting request set responsestatus to olResponseNotResponded + $calItemProps[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded; + if($userAction) { + // if user has responded then set replytime + $calItemProps[$this->proptags['replytime']] = time(); + } + + mapi_setprops($calmsg, $proposeNewTimeProps + $calItemProps); + + // get properties which stores owner information in meeting request mails + $props = mapi_getprops($calmsg, array(PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_ADDRTYPE)); + + // add owner to recipient table + $recips = array(); + $this->addOrganizer($props, $recips); + + if($isDelegate) { + /** + * If user is delegate then remove that user from recipienttable of the MR. + * and delegate MR mail doesn't contain any of the attendees in recipient table. + * So, other required and optional attendees are added from + * toattendeesstring and ccattendeesstring properties. + */ + $this->setRecipsFromString($recips, $messageprops[$this->proptags['toattendeesstring']], MAPI_TO); + $this->setRecipsFromString($recips, $messageprops[$this->proptags['ccattendeesstring']], MAPI_CC); + mapi_message_modifyrecipients($calmsg, 0, $recips); + } else { + mapi_message_modifyrecipients($calmsg, MODRECIP_ADD, $recips); + } + + mapi_message_savechanges($calmsg); + + // Move the message to the wastebasket + $wastebasket = $this->openDefaultWastebasket(); + mapi_folder_copymessages($sourcefolder, array($old_entryid[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE); + + $messageprops = mapi_getprops($calmsg, array(PR_ENTRYID)); + $entryid = $messageprops[PR_ENTRYID]; + } else { + // Create a new appointment with duplicate properties and recipient, but as an IPM.Appointment + $new = mapi_folder_createmessage($calFolder); + $props = mapi_getprops($this->message); + + $props[PR_MESSAGE_CLASS] = "IPM.Appointment"; + // when we are automatically processing the meeting request set responsestatus to olResponseNotResponded + $props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded; + + if (isset($props[$this->proptags['intendedbusystatus']])) { + if($tentative && $props[$this->proptags['intendedbusystatus']] !== fbFree) { + $props[$this->proptags['busystatus']] = $tentative; + } else { + $props[$this->proptags['busystatus']] = $props[$this->proptags['intendedbusystatus']]; + } + // we already have intendedbusystatus value in $props so no need to copy it + } else { + $props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy; + } + + // ZP-341 - we need to copy as well the attachments + // Copy attachments too + $this->replaceAttachments($this->message, $new); + // ZP-341 - end + + if($userAction) { + // if user has responded then set replytime + $props[$this->proptags['replytime']] = time(); + } + + mapi_setprops($new, $proposeNewTimeProps + $props); + + $reciptable = mapi_message_getrecipienttable($this->message); + + $recips = array(); + if(!$isDelegate) + $recips = mapi_table_queryallrows($reciptable, $this->recipprops); + + $this->addOrganizer($props, $recips); + + if($isDelegate) { + /** + * If user is delegate then remove that user from recipienttable of the MR. + * and delegate MR mail doesn't contain any of the attendees in recipient table. + * So, other required and optional attendees are added from + * toattendeesstring and ccattendeesstring properties. + */ + $this->setRecipsFromString($recips, $messageprops[$this->proptags['toattendeesstring']], MAPI_TO); + $this->setRecipsFromString($recips, $messageprops[$this->proptags['ccattendeesstring']], MAPI_CC); + mapi_message_modifyrecipients($new, 0, $recips); + } else { + mapi_message_modifyrecipients($new, MODRECIP_ADD, $recips); + } + mapi_message_savechanges($new); + + $props = mapi_getprops($new, array(PR_ENTRYID)); + $entryid = $props[PR_ENTRYID]; + } + } + } + } else { + // Here only properties are set on calendaritem, because user is responding from calendar. + $props = array(); + $props[$this->proptags['responsestatus']] = $tentative ? olResponseTentative : olResponseAccepted; + + if (isset($messageprops[$this->proptags['intendedbusystatus']])) { + if($tentative && $messageprops[$this->proptags['intendedbusystatus']] !== fbFree) { + $props[$this->proptags['busystatus']] = $tentative; + } else { + $props[$this->proptags['busystatus']] = $messageprops[$this->proptags['intendedbusystatus']]; + } + $props[$this->proptags['intendedbusystatus']] = $messageprops[$this->proptags['intendedbusystatus']]; + } else { + $props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy; + } + + $props[$this->proptags['meetingstatus']] = olMeetingReceived; + $props[$this->proptags['replytime']] = time(); + + if ($basedate) { + $recurr = new Recurrence($store, $this->message); + + // Copy recipients list + $reciptable = mapi_message_getrecipienttable($this->message); + $recips = mapi_table_queryallrows($reciptable, $this->recipprops); + + if($recurr->isException($basedate)) { + $recurr->modifyException($proposeNewTimeProps + $props, $basedate, $recips); + } else { + $props[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate); + $props[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate); + + $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS]; + $props[PR_SENT_REPRESENTING_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME]; + $props[PR_SENT_REPRESENTING_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE]; + $props[PR_SENT_REPRESENTING_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID]; + + $recurr->createException($proposeNewTimeProps + $props, $basedate, false, $recips); + } + } else { + mapi_setprops($this->message, $proposeNewTimeProps + $props); + } + mapi_savechanges($this->message); + + $entryid = $messageprops[PR_ENTRYID]; + } + + return $entryid; + } + + /** + * Declines the meeting request by moving the item to the deleted + * items folder and sending a decline message. After declining, you + * can't use this class instance any more. The message is closed. + * When an occurrence is decline then false is returned because that + * occurrence is deleted not the recurring item. + * + *@param boolean $sendresponse true if a response has to be sent to organizer + *@param resource $store MAPI_store of user + *@param string $basedate if specified contains starttime of day of an occurrence + *@return boolean true if item is deleted from Calendar else false + */ + function doDecline($sendresponse, $store=false, $basedate = false, $body = false) + { + $result = true; + $calendaritem = false; + if($this->isLocalOrganiser()) + return; + + // Remove any previous calendar items with this goid and appt id + $messageprops = mapi_getprops($this->message, Array($this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_NAME)); + + // If this meeting request is received by a delegate then open delegator's store. + if (isset($messageprops[PR_RCVD_REPRESENTING_NAME])) { + $delegatorStore = $this->getDelegatorStore($messageprops); + + $store = $delegatorStore['store']; + $calFolder = $delegatorStore['calFolder']; + } else { + $calFolder = $this->openDefaultCalendar(); + $store = $this->store; + } + + $goid = $messageprops[$this->proptags['goid']]; + + // First, find the items in the calendar by GlobalObjid (0x3) + $entryids = $this->findCalendarItems($goid, $calFolder); + + if (!$basedate) + $basedate = $this->getBasedateFromGlobalID($goid); + + if($sendresponse) + $this->createResponse(olResponseDeclined, false, false, $body, $store, $basedate, $calFolder); + + if ($basedate) { + // use CleanGlobalObjid (0x23) + $calendaritems = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder); + + foreach($calendaritems as $entryid) { + // Open each calendar item and set the properties of the cancellation object + $calendaritem = mapi_msgstore_openentry($store, $entryid); + + // Recurring item is found, now delete exception + if ($calendaritem) + $this->doRemoveExceptionFromCalendar($basedate, $calendaritem, $store); + } + + if ($this->isMeetingRequest()) + $calendaritem = false; + else + $result = false; + } + + if (!$calendaritem) { + $calendar = $this->openDefaultCalendar(); + + if(!empty($entryids)) { + mapi_folder_deletemessages($calendar, $entryids); + } + + // All we have to do to decline, is to move the item to the waste basket + $wastebasket = $this->openDefaultWastebasket(); + $sourcefolder = $this->openParentFolder(); + + $messageprops = mapi_getprops($this->message, Array(PR_ENTRYID)); + + // Release the message + $this->message = null; + + // Move the message to the waste basket + mapi_folder_copymessages($sourcefolder, Array($messageprops[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE); + } + return $result; + } + + /** + * Removes a meeting request from the calendar when the user presses the + * 'remove from calendar' button in response to a meeting cancellation. + * @param string $basedate if specified contains starttime of day of an occurrence + */ + function doRemoveFromCalendar($basedate) + { + if($this->isLocalOrganiser()) + return false; + + $store = $this->store; + $messageprops = mapi_getprops($this->message, Array(PR_ENTRYID, $this->proptags['goid'], PR_RCVD_REPRESENTING_NAME, PR_MESSAGE_CLASS)); + $goid = $messageprops[$this->proptags['goid']]; + + if (isset($messageprops[PR_RCVD_REPRESENTING_NAME])) { + $delegatorStore = $this->getDelegatorStore($messageprops); + $store = $delegatorStore['store']; + $calFolder = $delegatorStore['calFolder']; + } else { + $calFolder = $this->openDefaultCalendar(); + } + + $wastebasket = $this->openDefaultWastebasket(); + $sourcefolder = $this->openParentFolder(); + + // Check if the message is a meeting request in the inbox or a calendaritem by checking the message class + if (strpos($messageprops[PR_MESSAGE_CLASS], 'IPM.Schedule.Meeting') === 0) { + /** + * 'Remove from calendar' option from previewpane then we have to check GlobalID of this meeting request. + * If basedate found then open meeting from calendar and delete that occurence. + */ + $basedate = false; + if ($goid) { + // Retrieve GlobalID and find basedate in it. + $basedate = $this->getBasedateFromGlobalID($goid); + + // Basedate found, Now find item. + if ($basedate) { + $guid = $this->setBasedateInGlobalID($goid); + + // First, find the items in the calendar by GOID + $calendaritems = $this->findCalendarItems($guid, $calFolder); + if(is_array($calendaritems)) { + foreach($calendaritems as $entryid) { + // Open each calendar item and set the properties of the cancellation object + $calendaritem = mapi_msgstore_openentry($store, $entryid); + + if ($calendaritem){ + $this->doRemoveExceptionFromCalendar($basedate, $calendaritem, $store); + } + } + } + } + } + + // It is normal/recurring meeting item. + if (!$basedate) { + if (!isset($calFolder)) $calFolder = $this->openDefaultCalendar(); + + $entryids = $this->findCalendarItems($goid, $calFolder); + + if(is_array($entryids)){ + // Move the calendaritem to the waste basket + mapi_folder_copymessages($sourcefolder, $entryids, $wastebasket, MESSAGE_MOVE); + } + } + + // Release the message + $this->message = null; + + // Move the message to the waste basket + mapi_folder_copymessages($sourcefolder, Array($messageprops[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE); + + } else { + // Here only properties are set on calendaritem, because user is responding from calendar. + if ($basedate) { //remove the occurence + $this->doRemoveExceptionFromCalendar($basedate, $this->message, $store); + } else { //remove normal/recurring meeting item. + // Move the message to the waste basket + mapi_folder_copymessages($sourcefolder, Array($messageprops[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE); + } + } + } + + /** + * Removes the meeting request by moving the item to the deleted + * items folder. After canceling, youcan't use this class instance + * any more. The message is closed. + */ + function doCancel() + { + if($this->isLocalOrganiser()) + return; + if(!$this->isMeetingCancellation()) + return; + + // Remove any previous calendar items with this goid and appt id + $messageprops = mapi_getprops($this->message, Array($this->proptags['goid'])); + $goid = $messageprops[$this->proptags['goid']]; + + $entryids = $this->findCalendarItems($goid); + $calendar = $this->openDefaultCalendar(); + + mapi_folder_deletemessages($calendar, $entryids); + + // All we have to do to decline, is to move the item to the waste basket + + $wastebasket = $this->openDefaultWastebasket(); + $sourcefolder = $this->openParentFolder(); + + $messageprops = mapi_getprops($this->message, Array(PR_ENTRYID)); + + // Release the message + $this->message = null; + + // Move the message to the waste basket + mapi_folder_copymessages($sourcefolder, Array($messageprops[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE); + } + + + /** + * Sets the properties in the message so that is can be sent + * as a meeting request. The caller has to submit the message. This + * is only used for new MeetingRequests. Pass the appointment item as $message + * in the constructor to do this. + */ + function setMeetingRequest($basedate = false) + { + $props = mapi_getprops($this->message, Array($this->proptags['updatecounter'])); + + // Create a new global id for this item + $goid = pack("H*", "040000008200E00074C5B7101A82E00800000000"); + for ($i=0; $i<36; $i++) + $goid .= chr(rand(0, 255)); + + // Create a new appointment id for this item + $apptid = rand(); + + $props[PR_OWNER_APPT_ID] = $apptid; + $props[PR_ICON_INDEX] = 1026; + $props[$this->proptags['goid']] = $goid; + $props[$this->proptags['goid2']] = $goid; + + if (!isset($props[$this->proptags['updatecounter']])) { + $props[$this->proptags['updatecounter']] = 0; // OL also starts sequence no with zero. + $props[$this->proptags['last_updatecounter']] = 0; + } + + mapi_setprops($this->message, $props); + } + + /** + * Sends a meeting request by copying it to the outbox, converting + * the message class, adding some properties that are required only + * for sending the message and submitting the message. Set cancel to + * true if you wish to completely cancel the meeting request. You can + * specify an optional 'prefix' to prefix the sent message, which is normally + * 'Canceled: ' + */ + function sendMeetingRequest($cancel, $prefix = false, $basedate = false, $deletedRecips = false) + { + $this->includesResources = false; + $this->nonAcceptingResources = Array(); + + // Get the properties of the message + $messageprops = mapi_getprops($this->message, Array($this->proptags['recurring'])); + + /***************************************************************************************** + * Submit message to non-resource recipients + */ + // Set BusyStatus to olTentative (1) + // Set MeetingStatus to olMeetingReceived + // Set ResponseStatus to olResponseNotResponded + + /** + * While sending recurrence meeting exceptions are not send as attachments + * because first all exceptions are send and then recurrence meeting is sent. + */ + if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']] && !$basedate) { + // Book resource + $resourceRecipData = $this->bookResources($this->message, $cancel, $prefix); + + if (!$this->errorSetResource) { + $recurr = new Recurrence($this->openDefaultStore(), $this->message); + + // First send meetingrequest for recurring item + $this->submitMeetingRequest($this->message, $cancel, $prefix, false, $recurr, false, $deletedRecips); + + // Then send all meeting request for all exceptions + $exceptions = $recurr->getAllExceptions(); + if ($exceptions) { + foreach($exceptions as $exceptionBasedate) { + $attach = $recurr->getExceptionAttachment($exceptionBasedate); + + if ($attach) { + $occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY); + $this->submitMeetingRequest($occurrenceItem, $cancel, false, $exceptionBasedate, $recurr, false, $deletedRecips); + mapi_savechanges($attach); + } + } + } + } + } else { + // Basedate found, an exception is to be send + if ($basedate) { + $recurr = new Recurrence($this->openDefaultStore(), $this->message); + + if ($cancel) { + //@TODO: remove occurrence from Resource's Calendar if resource was booked for whole series + $this->submitMeetingRequest($this->message, $cancel, $prefix, $basedate, $recurr, false); + } else { + $attach = $recurr->getExceptionAttachment($basedate); + + if ($attach) { + $occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY); + + // Book resource for this occurrence + $resourceRecipData = $this->bookResources($occurrenceItem, $cancel, $prefix, $basedate); + + if (!$this->errorSetResource) { + // Save all previous changes + mapi_savechanges($this->message); + + $this->submitMeetingRequest($occurrenceItem, $cancel, $prefix, $basedate, $recurr, true, $deletedRecips); + mapi_savechanges($occurrenceItem); + mapi_savechanges($attach); + } + } + } + } else { + // This is normal meeting + $resourceRecipData = $this->bookResources($this->message, $cancel, $prefix); + + if (!$this->errorSetResource) { + $this->submitMeetingRequest($this->message, $cancel, $prefix, false, false, false, $deletedRecips); + } + } + } + + if(isset($this->errorSetResource) && $this->errorSetResource){ + return Array( + 'error' => $this->errorSetResource, + 'displayname' => $this->recipientDisplayname + ); + }else{ + return true; + } + } + + + function getFreeBusyInfo($entryID,$start,$end) + { + $result = array(); + $fbsupport = mapi_freebusysupport_open($this->session); + + if(mapi_last_hresult() != NOERROR) { + if(function_exists("dump")) { + dump("Error in opening freebusysupport object."); + } + return $result; + } + + $fbDataArray = mapi_freebusysupport_loaddata($fbsupport, array($entryID)); + + if($fbDataArray[0] != NULL){ + foreach($fbDataArray as $fbDataUser){ + $rangeuser1 = mapi_freebusydata_getpublishrange($fbDataUser); + if($rangeuser1 == NULL){ + return $result; + } + + $enumblock = mapi_freebusydata_enumblocks($fbDataUser, $start, $end); + mapi_freebusyenumblock_reset($enumblock); + + while(true){ + $blocks = mapi_freebusyenumblock_next($enumblock, 100); + if(!$blocks){ + break; + } + foreach($blocks as $blockItem){ + $result[] = $blockItem; + } + } + } + } + + mapi_freebusysupport_close($fbsupport); + return $result; + } + + /** + * Updates the message after an update has been performed (for example, + * changing the time of the meeting). This must be called before re-sending + * the meeting request. You can also call this function instead of 'setMeetingRequest()' + * as it will automatically call setMeetingRequest on this object if it is the first + * call to this function. + */ + function updateMeetingRequest($basedate = false) + { + $messageprops = mapi_getprops($this->message, Array($this->proptags['last_updatecounter'], $this->proptags['goid'])); + + if(!isset($messageprops[$this->proptags['last_updatecounter']]) || !isset($messageprops[$this->proptags['goid']])) { + $this->setMeetingRequest($basedate); + } else { + $counter = $messageprops[$this->proptags['last_updatecounter']] + 1; + + // increment value of last_updatecounter, last_updatecounter will be common for recurring series + // so even if you sending an exception only you need to update the last_updatecounter in the recurring series message + // this way we can make sure that everytime we will be using a uniwue number for every operation + mapi_setprops($this->message, Array($this->proptags['last_updatecounter'] => $counter)); + } + } + + /** + * Returns TRUE if we are the organiser of the meeting. + */ + function isLocalOrganiser() + { + if($this->isMeetingRequest() || $this->isMeetingRequestResponse()) { + $messageid = $this->getAppointmentEntryID(); + + if(!isset($messageid)) + return false; + + $message = mapi_msgstore_openentry($this->store, $messageid); + + $messageprops = mapi_getprops($this->message, Array($this->proptags['goid'])); + $basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]); + if ($basedate) { + $recurr = new Recurrence($this->store, $message); + $attach = $recurr->getExceptionAttachment($basedate); + if ($attach) { + $occurItem = mapi_attach_openobj($attach); + $occurItemProps = mapi_getprops($occurItem, Array($this->proptags['responsestatus'])); + } + } + + $messageprops = mapi_getprops($message, Array($this->proptags['responsestatus'])); + } + + /** + * User can send recurring meeting or any occurrences from a recurring appointment so + * to be organizer 'responseStatus' property should be 'olResponseOrganized' on either + * of the recurring item or occurrence item. + */ + if ((isset($messageprops[$this->proptags['responsestatus']]) && $messageprops[$this->proptags['responsestatus']] == olResponseOrganized) + || (isset($occurItemProps[$this->proptags['responsestatus']]) && $occurItemProps[$this->proptags['responsestatus']] == olResponseOrganized)) + return true; + else + return false; + } + + /** + * Returns the entryid of the appointment that this message points at. This is + * only used on messages that are not in the calendar. + */ + function getAppointmentEntryID() + { + $messageprops = mapi_getprops($this->message, Array($this->proptags['goid2'])); + + $goid2 = $messageprops[$this->proptags['goid2']]; + + $items = $this->findCalendarItems($goid2); + + if(empty($items)) + return; + + // There should be just one item. If there are more, we just take the first one + return $items[0]; + } + + /*************************************************************************************************** + * Support functions - INTERNAL ONLY + *************************************************************************************************** + */ + + /** + * Return the tracking status of a recipient based on the IPM class (passed) + */ + function getTrackStatus($class) { + $status = olRecipientTrackStatusNone; + switch($class) + { + case "IPM.Schedule.Meeting.Resp.Pos": + $status = olRecipientTrackStatusAccepted; + break; + + case "IPM.Schedule.Meeting.Resp.Tent": + $status = olRecipientTrackStatusTentative; + break; + + case "IPM.Schedule.Meeting.Resp.Neg": + $status = olRecipientTrackStatusDeclined; + break; + } + return $status; + } + + function openParentFolder() { + $messageprops = mapi_getprops($this->message, Array(PR_PARENT_ENTRYID)); + + $parentfolder = mapi_msgstore_openentry($this->store, $messageprops[PR_PARENT_ENTRYID]); + return $parentfolder; + } + + function openDefaultCalendar() { + return $this->openDefaultFolder(PR_IPM_APPOINTMENT_ENTRYID); + } + + function openDefaultOutbox($store=false) { + return $this->openBaseFolder(PR_IPM_OUTBOX_ENTRYID, $store); + } + + function openDefaultWastebasket() { + return $this->openBaseFolder(PR_IPM_WASTEBASKET_ENTRYID); + } + + function getDefaultWastebasketEntryID() { + return $this->getBaseEntryID(PR_IPM_WASTEBASKET_ENTRYID); + } + + function getDefaultSentmailEntryID($store=false) { + return $this->getBaseEntryID(PR_IPM_SENTMAIL_ENTRYID, $store); + } + + function getDefaultFolderEntryID($prop) { + try { + $inbox = mapi_msgstore_getreceivefolder($this->store); + } catch (MAPIException $e) { + // public store doesn't support this method + if($e->getCode() == MAPI_E_NO_SUPPORT) { + // don't propogate this error to parent handlers, if store doesn't support it + $e->setHandled(); + return; + } + } + + $inboxprops = mapi_getprops($inbox, Array($prop)); + if(!isset($inboxprops[$prop])) + return; + + return $inboxprops[$prop]; + } + + function openDefaultFolder($prop) { + $entryid = $this->getDefaultFolderEntryID($prop); + $folder = mapi_msgstore_openentry($this->store, $entryid); + + return $folder; + } + + function getBaseEntryID($prop, $store=false) { + $storeprops = mapi_getprops( (($store)?$store:$this->store) , Array($prop)); + if(!isset($storeprops[$prop])) + return; + + return $storeprops[$prop]; + } + + function openBaseFolder($prop, $store=false) { + $entryid = $this->getBaseEntryID($prop, $store); + $folder = mapi_msgstore_openentry( (($store)?$store:$this->store) , $entryid); + + return $folder; + } + /** + * Function which sends response to organizer when attendee accepts, declines or proposes new time to a received meeting request. + *@param integer $status response status of attendee + *@param integer $proposalStartTime proposed starttime by attendee + *@param integer $proposalEndTime proposed endtime by attendee + *@param integer $basedate date of occurrence which attendee has responded + */ + function createResponse($status, $proposalStartTime=false, $proposalEndTime=false, $body=false, $store, $basedate = false, $calFolder) { + $messageprops = mapi_getprops($this->message, Array(PR_SENT_REPRESENTING_ENTRYID, + PR_SENT_REPRESENTING_EMAIL_ADDRESS, + PR_SENT_REPRESENTING_ADDRTYPE, + PR_SENT_REPRESENTING_NAME, + $this->proptags['goid'], + $this->proptags['goid2'], + $this->proptags['location'], + $this->proptags['startdate'], + $this->proptags['duedate'], + $this->proptags['recurring'], + $this->proptags['recurring_pattern'], + $this->proptags['recurrence_data'], + $this->proptags['timezone_data'], + $this->proptags['timezone'], + $this->proptags['updatecounter'], + PR_SUBJECT, + PR_MESSAGE_CLASS, + PR_OWNER_APPT_ID, + $this->proptags['is_exception'] + )); + + if ($basedate && $messageprops[PR_MESSAGE_CLASS] != "IPM.Schedule.Meeting.Request" ){ + // we are creating response from a recurring calendar item object + // We found basedate,so opened occurrence and get properties. + $recurr = new Recurrence($store, $this->message); + $exception = $recurr->getExceptionAttachment($basedate); + + if ($exception) { + // Exception found, Now retrieve properties + $imessage = mapi_attach_openobj($exception, 0); + $imsgprops = mapi_getprops($imessage); + + // If location is provided, copy it to the response + if (isset($imsgprops[$this->proptags['location']])) { + $messageprops[$this->proptags['location']] = $imsgprops[$this->proptags['location']]; + } + + // Update $messageprops with timings of occurrence + $messageprops[$this->proptags['startdate']] = $imsgprops[$this->proptags['startdate']]; + $messageprops[$this->proptags['duedate']] = $imsgprops[$this->proptags['duedate']]; + + // Meeting related properties + $props[$this->proptags['meetingstatus']] = $imsgprops[$this->proptags['meetingstatus']]; + $props[$this->proptags['responsestatus']] = $imsgprops[$this->proptags['responsestatus']]; + $props[PR_SUBJECT] = $imsgprops[PR_SUBJECT]; + } else { + // Exceptions is deleted. + // Update $messageprops with timings of occurrence + $messageprops[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate); + $messageprops[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate); + + $props[$this->proptags['meetingstatus']] = olNonMeeting; + $props[$this->proptags['responsestatus']] = olResponseNone; + } + + $props[$this->proptags['recurring']] = false; + $props[$this->proptags['is_exception']] = true; + } else { + // we are creating a response from meeting request mail (it could be recurring or non-recurring) + // Send all recurrence info in response, if this is a recurrence meeting. + $isRecurring = isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']]; + $isException = isset($messageprops[$this->proptags['is_exception']]) && $messageprops[$this->proptags['is_exception']]; + if ($isRecurring || $isException) { + if($isRecurring) { + $props[$this->proptags['recurring']] = $messageprops[$this->proptags['recurring']]; + } + if($isException) { + $props[$this->proptags['is_exception']] = $messageprops[$this->proptags['is_exception']]; + } + $calendaritems = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder); + + $calendaritem = mapi_msgstore_openentry($this->store, $calendaritems[0]); + $recurr = new Recurrence($store, $calendaritem); + } + } + + // we are sending a response for recurring meeting request (or exception), so set some required properties + if(isset($recurr) && $recurr) { + if(!empty($messageprops[$this->proptags['recurring_pattern']])) { + $props[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']]; + } + + if(!empty($messageprops[$this->proptags['recurrence_data']])) { + $props[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']]; + } + + $props[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']]; + $props[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']]; + + $this->generateRecurDates($recurr, $messageprops, $props); + } + + // Create a response message + $recip = Array(); + $recip[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID]; + $recip[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS]; + $recip[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE]; + $recip[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME]; + $recip[PR_RECIPIENT_TYPE] = MAPI_TO; + + switch($status) { + case olResponseAccepted: + $classpostfix = "Pos"; + $subjectprefix = _("Accepted"); + break; + case olResponseDeclined: + $classpostfix = "Neg"; + $subjectprefix = _("Declined"); + break; + case olResponseTentative: + $classpostfix = "Tent"; + $subjectprefix = _("Tentatively accepted"); + break; + } + + if($proposalStartTime && $proposalEndTime){ + // if attendee has proposed new time then change subject prefix + $subjectprefix = _("New Time Proposed"); + } + + $props[PR_SUBJECT] = $subjectprefix . ": " . $messageprops[PR_SUBJECT]; + + $props[PR_MESSAGE_CLASS] = "IPM.Schedule.Meeting.Resp." . $classpostfix; + if(isset($messageprops[PR_OWNER_APPT_ID])) + $props[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID]; + + // Set GLOBALID AND CLEANGLOBALID, if exception then also set basedate into GLOBALID(0x3). + $props[$this->proptags['goid']] = $this->setBasedateInGlobalID($messageprops[$this->proptags['goid2']], $basedate); + $props[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']]; + $props[$this->proptags['updatecounter']] = $messageprops[$this->proptags['updatecounter']]; + + // get the default store, in which we have to store the accepted email by delegate or normal user. + $defaultStore = $this->openDefaultStore(); + $props[PR_SENTMAIL_ENTRYID] = $this->getDefaultSentmailEntryID($defaultStore); + + if($proposalStartTime && $proposalEndTime){ + $props[$this->proptags['proposed_start_whole']] = $proposalStartTime; + $props[$this->proptags['proposed_end_whole']] = $proposalEndTime; + $props[$this->proptags['proposed_duration']] = round($proposalEndTime - $proposalStartTime)/60; + $props[$this->proptags['counter_proposal']] = true; + } + + //Set body message in Appointment + if(isset($body)) { + $props[PR_BODY] = $this->getMeetingTimeInfo() ? $this->getMeetingTimeInfo() : $body; + } + + // PR_START_DATE/PR_END_DATE is used in the UI in Outlook on the response message + $props[PR_START_DATE] = $messageprops[$this->proptags['startdate']]; + $props[PR_END_DATE] = $messageprops[$this->proptags['duedate']]; + + // Set startdate and duedate in response mail. + $props[$this->proptags['startdate']] = $messageprops[$this->proptags['startdate']]; + $props[$this->proptags['duedate']] = $messageprops[$this->proptags['duedate']]; + + // responselocation is used in the UI in Outlook on the response message + if (isset($messageprops[$this->proptags['location']])) { + $props[$this->proptags['responselocation']] = $messageprops[$this->proptags['location']]; + $props[$this->proptags['location']] = $messageprops[$this->proptags['location']]; + } + + // check if $store is set and it is not equal to $defaultStore (means its the delegation case) + if(isset($store) && isset($defaultStore)) { + $storeProps = mapi_getprops($store, array(PR_ENTRYID)); + $defaultStoreProps = mapi_getprops($defaultStore, array(PR_ENTRYID)); + + if($storeProps[PR_ENTRYID] !== $defaultStoreProps[PR_ENTRYID]){ + // get the properties of the other user (for which the logged in user is a delegate). + $storeProps = mapi_getprops($store, array(PR_MAILBOX_OWNER_ENTRYID)); + $addrbook = mapi_openaddressbook($this->session); + $addrbookitem = mapi_ab_openentry($addrbook, $storeProps[PR_MAILBOX_OWNER_ENTRYID]); + $addrbookitemprops = mapi_getprops($addrbookitem, array(PR_DISPLAY_NAME, PR_EMAIL_ADDRESS)); + + // setting the following properties will ensure that the delegation part of message. + $props[PR_SENT_REPRESENTING_ENTRYID] = $storeProps[PR_MAILBOX_OWNER_ENTRYID]; + $props[PR_SENT_REPRESENTING_NAME] = $addrbookitemprops[PR_DISPLAY_NAME]; + $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $addrbookitemprops[PR_EMAIL_ADDRESS]; + $props[PR_SENT_REPRESENTING_ADDRTYPE] = "ZARAFA"; + + // get the properties of default store and set it accordingly + $defaultStoreProps = mapi_getprops($defaultStore, array(PR_MAILBOX_OWNER_ENTRYID)); + $addrbookitem = mapi_ab_openentry($addrbook, $defaultStoreProps[PR_MAILBOX_OWNER_ENTRYID]); + $addrbookitemprops = mapi_getprops($addrbookitem, array(PR_DISPLAY_NAME, PR_EMAIL_ADDRESS)); + + // set the following properties will ensure the sender's details, which will be the default user in this case. + //the function returns array($name, $emailaddr, $addrtype, $entryid, $searchkey); + $defaultUserDetails = $this->getOwnerAddress($defaultStore); + $props[PR_SENDER_ENTRYID] = $defaultUserDetails[3]; + $props[PR_SENDER_EMAIL_ADDRESS] = $defaultUserDetails[1]; + $props[PR_SENDER_NAME] = $defaultUserDetails[0]; + $props[PR_SENDER_ADDRTYPE] = $defaultUserDetails[2]; + } + } + + // pass the default store to get the required store. + $outbox = $this->openDefaultOutbox($defaultStore); + + $message = mapi_folder_createmessage($outbox); + mapi_setprops($message, $props); + mapi_message_modifyrecipients($message, MODRECIP_ADD, Array($recip)); + mapi_message_savechanges($message); + mapi_message_submitmessage($message); + } + + /** + * Function which finds items in calendar based on specified parameters. + *@param binary $goid GlobalID(0x3) of item + *@param resource $calendar MAPI_folder of user + *@param boolean $use_cleanGlobalID if true then search should be performed on cleanGlobalID(0x23) else globalID(0x3) + */ + function findCalendarItems($goid, $calendar = false, $use_cleanGlobalID = false) { + if(!$calendar) { + // Open the Calendar + $calendar = $this->openDefaultCalendar(); + } + + // Find the item by restricting all items to the correct ID + $restrict = Array(RES_AND, Array()); + + array_push($restrict[1], Array(RES_PROPERTY, + Array(RELOP => RELOP_EQ, + ULPROPTAG => ($use_cleanGlobalID ? $this->proptags['goid2'] : $this->proptags['goid']), + VALUE => $goid + ) + )); + + $calendarcontents = mapi_folder_getcontentstable($calendar); + + $rows = mapi_table_queryallrows($calendarcontents, Array(PR_ENTRYID), $restrict); + + if(empty($rows)) + return; + + $calendaritems = Array(); + + // In principle, there should only be one row, but we'll handle them all just in case + foreach($rows as $row) { + $calendaritems[] = $row[PR_ENTRYID]; + } + + return $calendaritems; + } + + // Returns TRUE if both entryid's are equal. Equality is defined by both entryid's pointing at the + // same SMTP address when converted to SMTP + function compareABEntryIDs($entryid1, $entryid2) { + // If the session was not passed, just do a 'normal' compare. + if(!$this->session) + return $entryid1 == $entryid2; + + $smtp1 = $this->getSMTPAddress($entryid1); + $smtp2 = $this->getSMTPAddress($entryid2); + + if($smtp1 == $smtp2) + return true; + else + return false; + } + + // Gets the SMTP address of the passed addressbook entryid + function getSMTPAddress($entryid) { + if(!$this->session) + return false; + + $ab = mapi_openaddressbook($this->session); + + $abitem = mapi_ab_openentry($ab, $entryid); + + if(!$abitem) + return ""; + + $props = mapi_getprops($abitem, array(PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS)); + + if($props[PR_ADDRTYPE] == "SMTP") { + return $props[PR_EMAIL_ADDRESS]; + } + else return $props[PR_SMTP_ADDRESS]; + } + + /** + * Gets the properties associated with the owner of the passed store: + * PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_ADDRTYPE, PR_ENTRYID, PR_SEARCH_KEY + * + * @param $store message store + * @param $fallbackToLoggedInUser if true then return properties of logged in user instead of mailbox owner + * not used when passed store is public store. for public store we are always returning logged in user's info. + * @return properties of logged in user in an array in sequence of display_name, email address, address tyep, + * entryid and search key. + */ + function getOwnerAddress($store, $fallbackToLoggedInUser = true) + { + if(!$this->session) + return false; + + $storeProps = mapi_getprops($store, array(PR_MAILBOX_OWNER_ENTRYID, PR_USER_ENTRYID)); + + $ownerEntryId = false; + if(isset($storeProps[PR_USER_ENTRYID]) && $storeProps[PR_USER_ENTRYID]) { + $ownerEntryId = $storeProps[PR_USER_ENTRYID]; + } + + if(isset($storeProps[PR_MAILBOX_OWNER_ENTRYID]) && $storeProps[PR_MAILBOX_OWNER_ENTRYID] && !$fallbackToLoggedInUser) { + $ownerEntryId = $storeProps[PR_MAILBOX_OWNER_ENTRYID]; + } + + if($ownerEntryId) { + $ab = mapi_openaddressbook($this->session); + + $zarafaUser = mapi_ab_openentry($ab, $ownerEntryId); + if(!$zarafaUser) + return false; + + $ownerProps = mapi_getprops($zarafaUser, array(PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS)); + + $addrType = $ownerProps[PR_ADDRTYPE]; + $name = $ownerProps[PR_DISPLAY_NAME]; + $emailAddr = $ownerProps[PR_EMAIL_ADDRESS]; + $searchKey = strtoupper($addrType) . ":" . strtoupper($emailAddr); + $entryId = $ownerEntryId; + + return array($name, $emailAddr, $addrType, $entryId, $searchKey); + } + + return false; + } + + // Opens this session's default message store + function openDefaultStore() + { + $storestable = mapi_getmsgstorestable($this->session); + $rows = mapi_table_queryallrows($storestable, array(PR_ENTRYID, PR_DEFAULT_STORE)); + $entry = false; + + foreach($rows as $row) { + if(isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE]) { + $entryid = $row[PR_ENTRYID]; + break; + } + } + + if(!$entryid) + return false; + + return mapi_openmsgstore($this->session, $entryid); + } + /** + * Function which adds organizer to recipient list which is passed. + * This function also checks if it has organizer. + * + * @param array $messageProps message properties + * @param array $recipients recipients list of message. + * @param boolean $isException true if we are processing recipient of exception + */ + function addOrganizer($messageProps, &$recipients, $isException = false){ + + $hasOrganizer = false; + // Check if meeting already has an organizer. + foreach ($recipients as $key => $recipient){ + if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) { + $hasOrganizer = true; + } else if ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])){ + // Recipients for an occurrence + $recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse; + } + } + + if (!$hasOrganizer){ + // Create organizer. + $organizer = array(); + $organizer[PR_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID]; + $organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME]; + $organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS]; + $organizer[PR_RECIPIENT_TYPE] = MAPI_TO; + $organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME]; + $organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP':$messageProps[PR_SENT_REPRESENTING_ADDRTYPE]; + $organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone; + $organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer; + + // Add organizer to recipients list. + array_unshift($recipients, $organizer); + } + } + + /** + * Function adds recipients in recips array from the string. + * + * @param array $recips recipient array. + * @param string $recipString recipient string attendees. + * @param int $type type of the recipient, MAPI_TO/MAPI_CC. + */ + function setRecipsFromString(&$recips, $recipString, $recipType = MAPI_TO) + { + $extraRecipient = array(); + $recipArray = explode(";", $recipString); + + foreach($recipArray as $recip) { + $recip = trim($recip); + if (!empty($recip)) { + $extraRecipient[PR_RECIPIENT_TYPE] = $recipType; + $extraRecipient[PR_DISPLAY_NAME] = $recip; + array_push($recips, $extraRecipient); + } + } + + } + + /** + * Function which removes an exception/occurrence from recurrencing meeting + * when a meeting cancellation of an occurrence is processed. + *@param string $basedate basedate of an occurrence + *@param resource $message recurring item from which occurrence has to be deleted + *@param resource $store MAPI_MSG_Store which contains the item + */ + function doRemoveExceptionFromCalendar($basedate, $message, $store) + { + $recurr = new Recurrence($store, $message); + $recurr->createException(array(), $basedate, true); + mapi_savechanges($message); + } + + /** + * Function which returns basedate of an changed occurrance from globalID of meeting request. + *@param binary $goid globalID + *@return boolean true if basedate is found else false it not found + */ + function getBasedateFromGlobalID($goid) + { + $hexguid = bin2hex($goid); + $hexbase = substr($hexguid, 32, 8); + $day = hexdec(substr($hexbase, 6, 2)); + $month = hexdec(substr($hexbase, 4, 2)); + $year = hexdec(substr($hexbase, 0, 4)); + + if ($day && $month && $year) + return gmmktime(0, 0, 0, $month, $day, $year); + else + return false; + } + + /** + * Function which sets basedate in globalID of changed occurrance which is to be send. + *@param binary $goid globalID + *@param string basedate of changed occurrance + *@return binary globalID with basedate in it + */ + function setBasedateInGlobalID($goid, $basedate = false) + { + $hexguid = bin2hex($goid); + $year = $basedate ? sprintf('%04s', dechex(date('Y', $basedate))) : '0000'; + $month = $basedate ? sprintf('%02s', dechex(date('m', $basedate))) : '00'; + $day = $basedate ? sprintf('%02s', dechex(date('d', $basedate))) : '00'; + + return hex2bin(strtoupper(substr($hexguid, 0, 32) . $year . $month . $day . substr($hexguid, 40))); + } + /** + * Function which replaces attachments with copy_from in copy_to. + *@param resource $copy_from MAPI_message from which attachments are to be copied. + *@param resource $copy_to MAPI_message to which attachment are to be copied. + *@param boolean $copyExceptions if true then all exceptions should also be sent as attachments + */ + function replaceAttachments($copy_from, $copy_to, $copyExceptions = true) + { + /* remove all old attachments */ + $attachmentTable = mapi_message_getattachmenttable($copy_to); + if($attachmentTable) { + $attachments = mapi_table_queryallrows($attachmentTable, array(PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME)); + + foreach($attachments as $attach_props){ + /* remove exceptions too? */ + if (!$copyExceptions && $attach_props[PR_ATTACH_METHOD] == 5 && isset($attach_props[PR_EXCEPTION_STARTTIME])) + continue; + mapi_message_deleteattach($copy_to, $attach_props[PR_ATTACH_NUM]); + } + } + $attachmentTable = false; + + /* copy new attachments */ + $attachmentTable = mapi_message_getattachmenttable($copy_from); + if($attachmentTable) { + $attachments = mapi_table_queryallrows($attachmentTable, array(PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME)); + + foreach($attachments as $attach_props){ + if (!$copyExceptions && $attach_props[PR_ATTACH_METHOD] == 5 && isset($attach_props[PR_EXCEPTION_STARTTIME])) + continue; + + $attach_old = mapi_message_openattach($copy_from, (int) $attach_props[PR_ATTACH_NUM]); + $attach_newResourceMsg = mapi_message_createattach($copy_to); + mapi_copyto($attach_old, array(), array(), $attach_newResourceMsg, 0); + mapi_savechanges($attach_newResourceMsg); + } + } + } + /** + * Function which replaces recipients in copy_to with recipients from copy_from. + *@param resource $copy_from MAPI_message from which recipients are to be copied. + *@param resource $copy_to MAPI_message to which recipients are to be copied. + */ + function replaceRecipients($copy_from, $copy_to, $isDelegate = false) + { + $recipienttable = mapi_message_getrecipienttable($copy_from); + + // If delegate, then do not add the delegate in recipients + if ($isDelegate) { + $delegate = mapi_getprops($copy_from, array(PR_RECEIVED_BY_EMAIL_ADDRESS)); + $res = array(RES_PROPERTY, array(RELOP => RELOP_NE, ULPROPTAG => PR_EMAIL_ADDRESS, VALUE => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS])); + $recipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $res); + } else { + $recipients = mapi_table_queryallrows($recipienttable, $this->recipprops); + } + + $copy_to_recipientTable = mapi_message_getrecipienttable($copy_to); + $copy_to_recipientRows = mapi_table_queryallrows($copy_to_recipientTable, array(PR_ROWID)); + foreach($copy_to_recipientRows as $recipient) { + mapi_message_modifyrecipients($copy_to, MODRECIP_REMOVE, array($recipient)); + } + + mapi_message_modifyrecipients($copy_to, MODRECIP_ADD, $recipients); + } + /** + * Function creates meeting item in resource's calendar. + *@param resource $message MAPI_message which is to create in resource's calendar + *@param boolean $cancel cancel meeting + *@param string $prefix prefix for subject of meeting + */ + function bookResources($message, $cancel, $prefix, $basedate = false) + { + if(!$this->enableDirectBooking) + return array(); + + // Get the properties of the message + $messageprops = mapi_getprops($message); + + if ($basedate) { + $recurrItemProps = mapi_getprops($this->message, array($this->proptags['goid'], $this->proptags['goid2'], $this->proptags['timezone_data'], $this->proptags['timezone'], PR_OWNER_APPT_ID)); + + $messageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($recurrItemProps[$this->proptags['goid']], $basedate); + $messageprops[$this->proptags['goid2']] = $recurrItemProps[$this->proptags['goid2']]; + + // Delete properties which are not needed. + $deleteProps = array($this->proptags['basedate'], PR_DISPLAY_NAME, PR_ATTACHMENT_FLAGS, PR_ATTACHMENT_HIDDEN, PR_ATTACHMENT_LINKID, PR_ATTACH_FLAGS, PR_ATTACH_METHOD); + foreach ($deleteProps as $propID) { + if (isset($messageprops[$propID])) { + unset($messageprops[$propID]); + } + } + + if (isset($messageprops[$this->proptags['recurring']])) $messageprops[$this->proptags['recurring']] = false; + + // Set Outlook properties + $messageprops[$this->proptags['clipstart']] = $messageprops[$this->proptags['startdate']]; + $messageprops[$this->proptags['clipend']] = $messageprops[$this->proptags['duedate']]; + $messageprops[$this->proptags['timezone_data']] = $recurrItemProps[$this->proptags['timezone_data']]; + $messageprops[$this->proptags['timezone']] = $recurrItemProps[$this->proptags['timezone']]; + $messageprops[$this->proptags['attendee_critical_change']] = time(); + $messageprops[$this->proptags['owner_critical_change']] = time(); + } + + // Get resource recipients + $getResourcesRestriction = Array(RES_AND, + Array(Array(RES_PROPERTY, + Array(RELOP => RELOP_EQ, // Equals recipient type 3: Resource + ULPROPTAG => PR_RECIPIENT_TYPE, + VALUE => array(PR_RECIPIENT_TYPE =>MAPI_BCC) + ) + )) + ); + $recipienttable = mapi_message_getrecipienttable($message); + $resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $getResourcesRestriction); + + $this->errorSetResource = false; + $resourceRecipData = Array(); + + // Put appointment into store resource users + $i = 0; + $len = count($resourceRecipients); + while(!$this->errorSetResource && $i < $len){ + $request = array(array(PR_DISPLAY_NAME => $resourceRecipients[$i][PR_DISPLAY_NAME])); + $ab = mapi_openaddressbook($this->session); + $ret = mapi_ab_resolvename($ab, $request, EMS_AB_ADDRESS_LOOKUP); + $result = mapi_last_hresult(); + if ($result == NOERROR){ + $result = $ret[0][PR_ENTRYID]; + } + $resourceUsername = $ret[0][PR_EMAIL_ADDRESS]; + $resourceABEntryID = $ret[0][PR_ENTRYID]; + + // Get StoreEntryID by username + $user_entryid = mapi_msgstore_createentryid($this->store, $resourceUsername); + + // Open store of the user + $userStore = mapi_openmsgstore($this->session, $user_entryid); + // Open root folder + $userRoot = mapi_msgstore_openentry($userStore, null); + // Get calendar entryID + $userRootProps = mapi_getprops($userRoot, array(PR_STORE_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_FREEBUSY_ENTRYIDS)); + + // Open Calendar folder [check hresult==0] + $accessToFolder = false; + try { + $calFolder = mapi_msgstore_openentry($userStore, $userRootProps[PR_IPM_APPOINTMENT_ENTRYID]); + if($calFolder){ + $calFolderProps = mapi_getProps($calFolder, Array(PR_ACCESS)); + if(($calFolderProps[PR_ACCESS] & MAPI_ACCESS_CREATE_CONTENTS) !== 0){ + $accessToFolder = true; + } + } + } catch (MAPIException $e) { + $e->setHandled(); + $this->errorSetResource = 1; // No access + } + + if($accessToFolder) { + /** + * Get the LocalFreebusy message that contains the properties that + * are set to accept or decline resource meeting requests + */ + // Use PR_FREEBUSY_ENTRYIDS[1] to open folder the LocalFreeBusy msg + $localFreebusyMsg = mapi_msgstore_openentry($userStore, $userRootProps[PR_FREEBUSY_ENTRYIDS][1]); + if($localFreebusyMsg){ + $props = mapi_getprops($localFreebusyMsg, array(PR_PROCESS_MEETING_REQUESTS, PR_DECLINE_RECURRING_MEETING_REQUESTS, PR_DECLINE_CONFLICTING_MEETING_REQUESTS)); + + $acceptMeetingRequests = ($props[PR_PROCESS_MEETING_REQUESTS])?1:0; + $declineRecurringMeetingRequests = ($props[PR_DECLINE_RECURRING_MEETING_REQUESTS])?1:0; + $declineConflictingMeetingRequests = ($props[PR_DECLINE_CONFLICTING_MEETING_REQUESTS])?1:0; + if(!$acceptMeetingRequests){ + /** + * When a resource has not been set to automatically accept meeting requests, + * the meeting request has to be sent to him rather than being put directly into + * his calendar. No error should be returned. + */ + //$errorSetResource = 2; + $this->nonAcceptingResources[] = $resourceRecipients[$i]; + }else{ + if($declineRecurringMeetingRequests && !$cancel){ + // Check if appointment is recurring + if($messageprops[ $this->proptags['recurring'] ]){ + $this->errorSetResource = 3; + } + } + if($declineConflictingMeetingRequests && !$cancel){ + // Check for conflicting items + $conflicting = false; + + // Open the calendar + $calFolder = mapi_msgstore_openentry($userStore, $userRootProps[PR_IPM_APPOINTMENT_ENTRYID]); + + if($calFolder) { + if ($this->isMeetingConflicting($message, $userStore, $calFolder, $messageprops)) + $conflicting = true; + } else { + $this->errorSetResource = 1; // No access + } + + if($conflicting){ + $this->errorSetResource = 4; // Conflict + } + } + } + } + } + + if(!$this->errorSetResource && $accessToFolder){ + /** + * First search on GlobalID(0x3) + * If (recurring and occurrence) If Resource was booked for only this occurrence then Resource should have only this occurrence in Calendar and not whole series. + * If (normal meeting) then GlobalID(0x3) and CleanGlobalID(0x23) are same, so doesnt matter if search is based on GlobalID. + */ + $rows = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder); + + /** + * If no entry is found then + * 1) Resource doesnt have meeting in Calendar. Seriously!! + * OR + * 2) We were looking for occurrence item but Resource has whole series + */ + if(empty($rows)){ + /** + * Now search on CleanGlobalID(0x23) WHY??? + * Because we are looking recurring item + * + * Possible results of this search + * 1) If Resource was booked for more than one occurrences then this search will return all those occurrence because search is perform on CleanGlobalID + * 2) If Resource was booked for whole series then it should return series. + */ + $rows = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder, true); + + $newResourceMsg = false; + if (!empty($rows)) { + // Since we are looking for recurring item, open every result and check for 'recurring' property. + foreach($rows as $row) { + $ResourceMsg = mapi_msgstore_openentry($userStore, $row); + $ResourceMsgProps = mapi_getprops($ResourceMsg, array($this->proptags['recurring'])); + + if (isset($ResourceMsgProps[$this->proptags['recurring']]) && $ResourceMsgProps[$this->proptags['recurring']]) { + $newResourceMsg = $ResourceMsg; + break; + } + } + } + + // Still no results found. I giveup, create new message. + if (!$newResourceMsg) + $newResourceMsg = mapi_folder_createmessage($calFolder); + }else{ + $newResourceMsg = mapi_msgstore_openentry($userStore, $rows[0]); + } + + // Prefix the subject if needed + if($prefix && isset($messageprops[PR_SUBJECT])) { + $messageprops[PR_SUBJECT] = $prefix . $messageprops[PR_SUBJECT]; + } + + // Set status to cancelled if needed + $messageprops[$this->proptags['busystatus']] = fbBusy; // The default status (Busy) + if($cancel) { + $messageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // The meeting has been canceled + $messageprops[$this->proptags['busystatus']] = fbFree; // Free + } else { + $messageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request + } + $messageprops[$this->proptags['responsestatus']] = olResponseAccepted; // The resource autmatically accepts the appointment + + $messageprops[PR_MESSAGE_CLASS] = "IPM.Appointment"; + + // Remove the PR_ICON_INDEX as it is not needed in the sent message and it also + // confuses the Zarafa webaccess + $messageprops[PR_ICON_INDEX] = null; + $messageprops[PR_RESPONSE_REQUESTED] = true; + + $addrinfo = $this->getOwnerAddress($this->store); + + if($addrinfo) { + list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrinfo; + + $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr; + $messageprops[PR_SENT_REPRESENTING_NAME] = $ownername; + $messageprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype; + $messageprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid; + $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey; + + $messageprops[$this->proptags['apptreplyname']] = $ownername; + $messageprops[$this->proptags['replytime']] = time(); + } + + if ($basedate && isset($ResourceMsgProps[$this->proptags['recurring']]) && $ResourceMsgProps[$this->proptags['recurring']]) { + $recurr = new Recurrence($userStore, $newResourceMsg); + + // Copy recipients list + $reciptable = mapi_message_getrecipienttable($message); + $recips = mapi_table_queryallrows($reciptable, $this->recipprops); + // add owner to recipient table + $this->addOrganizer($messageprops, $recips, true); + + // Update occurrence + if($recurr->isException($basedate)) + $recurr->modifyException($messageprops, $basedate, $recips); + else + $recurr->createException($messageprops, $basedate, false, $recips); + } else { + + mapi_setprops($newResourceMsg, $messageprops); + + // Copy attachments + $this->replaceAttachments($message, $newResourceMsg); + + // Copy all recipients too + $this->replaceRecipients($message, $newResourceMsg); + + // Now add organizer also to recipient table + $recips = Array(); + $this->addOrganizer($messageprops, $recips); + mapi_message_modifyrecipients($newResourceMsg, MODRECIP_ADD, $recips); + } + + mapi_savechanges($newResourceMsg); + + $resourceRecipData[] = Array( + 'store' => $userStore, + 'folder' => $calFolder, + 'msg' => $newResourceMsg, + ); + $this->includesResources = true; + }else{ + /** + * If no other errors occured and you have no access to the + * folder of the resource, throw an error=1. + */ + if(!$this->errorSetResource){ + $this->errorSetResource = 1; + } + + for($j = 0, $len = count($resourceRecipData); $j < $len; $j++){ + // Get the EntryID + $props = mapi_message_getprops($resourceRecipData[$j]['msg']); + + mapi_folder_deletemessages($resourceRecipData[$j]['folder'], Array($props[PR_ENTRYID]), DELETE_HARD_DELETE); + } + $this->recipientDisplayname = $resourceRecipients[$i][PR_DISPLAY_NAME]; + } + $i++; + } + + /************************************************************** + * Set the BCC-recipients (resources) tackstatus to accepted. + */ + // Get resource recipients + $getResourcesRestriction = Array(RES_AND, + Array(Array(RES_PROPERTY, + Array(RELOP => RELOP_EQ, // Equals recipient type 3: Resource + ULPROPTAG => PR_RECIPIENT_TYPE, + VALUE => array(PR_RECIPIENT_TYPE =>MAPI_BCC) + ) + )) + ); + $recipienttable = mapi_message_getrecipienttable($message); + $resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $getResourcesRestriction); + if(!empty($resourceRecipients)){ + // Set Tracking status of resource recipients to olResponseAccepted (3) + for($i = 0, $len = count($resourceRecipients); $i < $len; $i++){ + $resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusAccepted; + $resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS_TIME] = time(); + } + mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $resourceRecipients); + } + + // Publish updated free/busy information + if(!$this->errorSetResource){ + for($i = 0, $len = count($resourceRecipData); $i < $len; $i++){ + $storeProps = mapi_msgstore_getprops($resourceRecipData[$i]['store'], array(PR_MAILBOX_OWNER_ENTRYID)); + if (isset($storeProps[PR_MAILBOX_OWNER_ENTRYID])){ + $pub = new FreeBusyPublish($this->session, $resourceRecipData[$i]['store'], $resourceRecipData[$i]['folder'], $storeProps[PR_MAILBOX_OWNER_ENTRYID]); + $pub->publishFB(time() - (7 * 24 * 60 * 60), 6 * 30 * 24 * 60 * 60); // publish from one week ago, 6 months ahead + } + } + } + + return $resourceRecipData; + } + /** + * Function which save an exception into recurring item + * + * @param resource $recurringItem reference to MAPI_message of recurring item + * @param resource $occurrenceItem reference to MAPI_message of occurrence + * @param string $basedate basedate of occurrence + * @param boolean $move if true then occurrence item is deleted + * @param boolean $tentative true if user has tentatively accepted it or false if user has accepted it. + * @param boolean $userAction true if user has manually responded to meeting request + * @param resource $store user store + * @param boolean $isDelegate true if delegate is processing this meeting request + */ + function acceptException(&$recurringItem, &$occurrenceItem, $basedate, $move = false, $tentative, $userAction = false, $store, $isDelegate = false) + { + $recurr = new Recurrence($store, $recurringItem); + + // Copy properties from meeting request + $exception_props = mapi_getprops($occurrenceItem); + + // Copy recipients list + $reciptable = mapi_message_getrecipienttable($occurrenceItem); + // If delegate, then do not add the delegate in recipients + if ($isDelegate) { + $delegate = mapi_getprops($this->message, array(PR_RECEIVED_BY_EMAIL_ADDRESS)); + $res = array(RES_PROPERTY, array(RELOP => RELOP_NE, ULPROPTAG => PR_EMAIL_ADDRESS, VALUE => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS])); + $recips = mapi_table_queryallrows($reciptable, $this->recipprops, $res); + } else { + $recips = mapi_table_queryallrows($reciptable, $this->recipprops); + } + + + // add owner to recipient table + $this->addOrganizer($exception_props, $recips, true); + + // add delegator to meetings + if ($isDelegate) $this->addDelegator($exception_props, $recips); + + $exception_props[$this->proptags['meetingstatus']] = olMeetingReceived; + $exception_props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded; + // Set basedate property (ExceptionReplaceTime) + + if (isset($exception_props[$this->proptags['intendedbusystatus']])) { + if($tentative && $exception_props[$this->proptags['intendedbusystatus']] !== fbFree) { + $exception_props[$this->proptags['busystatus']] = $tentative; + } else { + $exception_props[$this->proptags['busystatus']] = $exception_props[$this->proptags['intendedbusystatus']]; + } + // we already have intendedbusystatus value in $exception_props so no need to copy it + } else { + $exception_props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy; + } + + if($userAction) { + // if user has responded then set replytime + $exception_props[$this->proptags['replytime']] = time(); + } + + if($recurr->isException($basedate)) + $recurr->modifyException($exception_props, $basedate, $recips, $occurrenceItem); + else + $recurr->createException($exception_props, $basedate, false, $recips, $occurrenceItem); + + // Move the occurrenceItem to the waste basket + if ($move) { + $wastebasket = $this->openDefaultWastebasket(); + $sourcefolder = mapi_msgstore_openentry($this->store, $exception_props[PR_PARENT_ENTRYID]); + mapi_folder_copymessages($sourcefolder, Array($exception_props[PR_ENTRYID]), $wastebasket, MESSAGE_MOVE); + } + + mapi_savechanges($recurringItem); + } + + /** + * Function which submits meeting request based on arguments passed to it. + *@param resource $message MAPI_message whose meeting request is to be send + *@param boolean $cancel if true send request, else send cancellation + *@param string $prefix subject prefix + *@param integer $basedate basedate for an occurrence + *@param Object $recurObject recurrence object of mr + *@param boolean $copyExceptions When sending update mail for recurring item then we dont send exceptions in attachments + */ + function submitMeetingRequest($message, $cancel, $prefix, $basedate = false, $recurObject = false, $copyExceptions = true, $deletedRecips = false) + { + $newmessageprops = $messageprops = mapi_getprops($this->message); + $new = $this->createOutgoingMessage(); + + // Copy the entire message into the new meeting request message + if ($basedate) { + // messageprops contains properties of whole recurring series + // and newmessageprops contains properties of exception item + $newmessageprops = mapi_getprops($message); + + // Ensure that the correct basedate is set in the new message + $newmessageprops[$this->proptags['basedate']] = $basedate; + + // Set isRecurring to false, because this is an exception + $newmessageprops[$this->proptags['recurring']] = false; + + // set LID_IS_EXCEPTION to true + $newmessageprops[$this->proptags['is_exception']] = true; + + // Set to high importance + if($cancel) $newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH; + + // Set startdate and enddate of exception + if ($cancel && $recurObject) { + $newmessageprops[$this->proptags['startdate']] = $recurObject->getOccurrenceStart($basedate); + $newmessageprops[$this->proptags['duedate']] = $recurObject->getOccurrenceEnd($basedate); + } + + // Set basedate in guid (0x3) + $newmessageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($messageprops[$this->proptags['goid2']], $basedate); + $newmessageprops[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']]; + $newmessageprops[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID]; + + // Get deleted recipiets from exception msg + $restriction = Array(RES_AND, + Array( + Array(RES_BITMASK, + Array( ULTYPE => BMR_NEZ, + ULPROPTAG => PR_RECIPIENT_FLAGS, + ULMASK => recipExceptionalDeleted + ) + ), + Array(RES_BITMASK, + Array( ULTYPE => BMR_EQZ, + ULPROPTAG => PR_RECIPIENT_FLAGS, + ULMASK => recipOrganizer + ) + ), + ) + ); + + // In direct-booking mode, we don't need to send cancellations to resources + if($this->enableDirectBooking) { + $restriction[1][] = Array(RES_PROPERTY, + Array(RELOP => RELOP_NE, // Does not equal recipient type: MAPI_BCC (Resource) + ULPROPTAG => PR_RECIPIENT_TYPE, + VALUE => array(PR_RECIPIENT_TYPE => MAPI_BCC) + ) + ); + } + + $recipienttable = mapi_message_getrecipienttable($message); + $recipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $restriction); + + if (!$deletedRecips) { + $deletedRecips = array_merge(array(), $recipients); + } else { + $deletedRecips = array_merge($deletedRecips, $recipients); + } + } + + // Remove the PR_ICON_INDEX as it is not needed in the sent message and it also + // confuses the Zarafa webaccess + $newmessageprops[PR_ICON_INDEX] = null; + $newmessageprops[PR_RESPONSE_REQUESTED] = true; + + // PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar + $newmessageprops[PR_START_DATE] = $newmessageprops[$this->proptags['startdate']]; + $newmessageprops[PR_END_DATE] = $newmessageprops[$this->proptags['duedate']]; + + // Set updatecounter/AppointmentSequenceNumber + // get the value of latest updatecounter for the whole series and use it + $newmessageprops[$this->proptags['updatecounter']] = $messageprops[$this->proptags['last_updatecounter']]; + + $meetingTimeInfo = $this->getMeetingTimeInfo(); + + if($meetingTimeInfo) + $newmessageprops[PR_BODY] = $meetingTimeInfo; + + // Send all recurrence info in mail, if this is a recurrence meeting. + if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']]) { + if(!empty($messageprops[$this->proptags['recurring_pattern']])) { + $newmessageprops[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']]; + } + $newmessageprops[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']]; + $newmessageprops[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']]; + $newmessageprops[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']]; + + if($recurObject) { + $this->generateRecurDates($recurObject, $messageprops, $newmessageprops); + } + } + + if (isset($newmessageprops[$this->proptags['counter_proposal']])) { + unset($newmessageprops[$this->proptags['counter_proposal']]); + } + + // Prefix the subject if needed + if ($prefix && isset($newmessageprops[PR_SUBJECT])) + $newmessageprops[PR_SUBJECT] = $prefix . $newmessageprops[PR_SUBJECT]; + + mapi_setprops($new, $newmessageprops); + + // Copy attachments + $this->replaceAttachments($message, $new, $copyExceptions); + + // Retrieve only those recipient who should receive this meeting request. + $stripResourcesRestriction = Array(RES_AND, + Array( + Array(RES_BITMASK, + Array( ULTYPE => BMR_EQZ, + ULPROPTAG => PR_RECIPIENT_FLAGS, + ULMASK => recipExceptionalDeleted + ) + ), + Array(RES_BITMASK, + Array( ULTYPE => BMR_EQZ, + ULPROPTAG => PR_RECIPIENT_FLAGS, + ULMASK => recipOrganizer + ) + ), + ) + ); + + // In direct-booking mode, resources do not receive a meeting request + if($this->enableDirectBooking) { + $stripResourcesRestriction[1][] = + Array(RES_PROPERTY, + Array(RELOP => RELOP_NE, // Does not equal recipient type: MAPI_BCC (Resource) + ULPROPTAG => PR_RECIPIENT_TYPE, + VALUE => array(PR_RECIPIENT_TYPE => MAPI_BCC) + ) + ); + } + + $recipienttable = mapi_message_getrecipienttable($message); + $recipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction); + + if ($basedate && empty($recipients)) { + // Retrieve full list + $recipienttable = mapi_message_getrecipienttable($this->message); + $recipients = mapi_table_queryallrows($recipienttable, $this->recipprops); + + // Save recipients in exceptions + mapi_message_modifyrecipients($message, MODRECIP_ADD, $recipients); + + // Now retrieve only those recipient who should receive this meeting request. + $recipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction); + } + + //@TODO: handle nonAcceptingResources + /** + * Add resource recipients that did not automatically accept the meeting request. + * (note: meaning that they did not decline the meeting request) + *//* + for($i=0;$inonAcceptingResources);$i++){ + $recipients[] = $this->nonAcceptingResources[$i]; + }*/ + + if(!empty($recipients)) { + // Strip out the sender/"owner" recipient + mapi_message_modifyrecipients($new, MODRECIP_ADD, $recipients); + + // Set some properties that are different in the sent request than + // in the item in our calendar + + // we should store busystatus value to intendedbusystatus property, because busystatus for outgoing meeting request + // should always be fbTentative + $newmessageprops[$this->proptags['intendedbusystatus']] = isset($newmessageprops[$this->proptags['busystatus']]) ? $newmessageprops[$this->proptags['busystatus']] : $messageprops[$this->proptags['busystatus']]; + $newmessageprops[$this->proptags['busystatus']] = fbTentative; // The default status when not accepted + $newmessageprops[$this->proptags['responsestatus']] = olResponseNotResponded; // The recipient has not responded yet + $newmessageprops[$this->proptags['attendee_critical_change']] = time(); + $newmessageprops[$this->proptags['owner_critical_change']] = time(); + $newmessageprops[$this->proptags['meetingtype']] = mtgRequest; + + if ($cancel) { + $newmessageprops[PR_MESSAGE_CLASS] = "IPM.Schedule.Meeting.Canceled"; + $newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request + $newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free + } else { + $newmessageprops[PR_MESSAGE_CLASS] = "IPM.Schedule.Meeting.Request"; + $newmessageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request + } + + mapi_setprops($new, $newmessageprops); + mapi_message_savechanges($new); + + // Submit message to non-resource recipients + mapi_message_submitmessage($new); + } + + // Send cancellation to deleted attendees + if ($deletedRecips && !empty($deletedRecips)) { + $new = $this->createOutgoingMessage(); + + mapi_message_modifyrecipients($new, MODRECIP_ADD, $deletedRecips); + + $newmessageprops[PR_MESSAGE_CLASS] = "IPM.Schedule.Meeting.Canceled"; + $newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request + $newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free + $newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH; // HIGH Importance + if (isset($newmessageprops[PR_SUBJECT])) { + $newmessageprops[PR_SUBJECT] = _('Canceled: ') . $newmessageprops[PR_SUBJECT]; + } + + mapi_setprops($new, $newmessageprops); + mapi_message_savechanges($new); + + // Submit message to non-resource recipients + mapi_message_submitmessage($new); + } + + // Set properties on meeting object in calendar + // Set requestsent to 'true' (turns on 'tracking', etc) + $props = array(); + $props[$this->proptags['meetingstatus']] = olMeeting; + $props[$this->proptags['responsestatus']] = olResponseOrganized; + $props[$this->proptags['requestsent']] = (!empty($recipients)) || ($this->includesResources && !$this->errorSetResource); + $props[$this->proptags['attendee_critical_change']] = time(); + $props[$this->proptags['owner_critical_change']] = time(); + $props[$this->proptags['meetingtype']] = mtgRequest; + // save the new updatecounter to exception/recurring series/normal meeting + $props[$this->proptags['updatecounter']] = $newmessageprops[$this->proptags['updatecounter']]; + + // PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar + $props[PR_START_DATE] = $messageprops[$this->proptags['startdate']]; + $props[PR_END_DATE] = $messageprops[$this->proptags['duedate']]; + + mapi_setprops($message, $props); + + // saving of these properties on calendar item should be handled by caller function + // based on sending meeting request was successfull or not + } + + /** + * OL2007 uses these 4 properties to specify occurence that should be updated. + * ical generates RECURRENCE-ID property based on exception's basedate (PidLidExceptionReplaceTime), + * but OL07 doesn't send this property, so ical will generate RECURRENCE-ID property based on date + * from GlobalObjId and time from StartRecurTime property, so we are sending basedate property and + * also additionally we are sending these properties. + * Ref: MS-OXCICAL 2.2.1.20.20 Property: RECURRENCE-ID + * @param Object $recurObject instance of recurrence class for this message + * @param Array $messageprops properties of meeting object that is going to be send + * @param Array $newmessageprops properties of meeting request/response that is going to be send + */ + function generateRecurDates($recurObject, $messageprops, &$newmessageprops) + { + if($messageprops[$this->proptags['startdate']] && $messageprops[$this->proptags['duedate']]) { + $startDate = date("Y:n:j:G:i:s", $recurObject->fromGMT($recurObject->tz, $messageprops[$this->proptags['startdate']])); + $endDate = date("Y:n:j:G:i:s", $recurObject->fromGMT($recurObject->tz, $messageprops[$this->proptags['duedate']])); + + $startDate = explode(":", $startDate); + $endDate = explode(":", $endDate); + + // [0] => year, [1] => month, [2] => day, [3] => hour, [4] => minutes, [5] => seconds + // RecurStartDate = year * 512 + month_number * 32 + day_number + $newmessageprops[$this->proptags["start_recur_date"]] = (((int) $startDate[0]) * 512) + (((int) $startDate[1]) * 32) + ((int) $startDate[2]); + // RecurStartTime = hour * 4096 + minutes * 64 + seconds + $newmessageprops[$this->proptags["start_recur_time"]] = (((int) $startDate[3]) * 4096) + (((int) $startDate[4]) * 64) + ((int) $startDate[5]); + + $newmessageprops[$this->proptags["end_recur_date"]] = (((int) $endDate[0]) * 512) + (((int) $endDate[1]) * 32) + ((int) $endDate[2]); + $newmessageprops[$this->proptags["end_recur_time"]] = (((int) $endDate[3]) * 4096) + (((int) $endDate[4]) * 64) + ((int) $endDate[5]); + } + } + + function createOutgoingMessage() + { + $sentprops = array(); + $outbox = $this->openDefaultOutbox($this->openDefaultStore()); + + $outgoing = mapi_folder_createmessage($outbox); + if(!$outgoing) return false; + + $addrinfo = $this->getOwnerAddress($this->store); + if($addrinfo) { + list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrinfo; + $sentprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr; + $sentprops[PR_SENT_REPRESENTING_NAME] = $ownername; + $sentprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype; + $sentprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid; + $sentprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey; + } + + $sentprops[PR_SENTMAIL_ENTRYID] = $this->getDefaultSentmailEntryID($this->openDefaultStore()); + + mapi_setprops($outgoing, $sentprops); + + return $outgoing; + } + + /** + * Function which checks received meeting request is either old(outofdate) or new. + * @return boolean true if meeting request is outofdate else false if it is new + */ + function isMeetingOutOfDate() + { + $result = false; + $store = $this->store; + $props = mapi_getprops($this->message, array($this->proptags['goid'], $this->proptags['goid2'], $this->proptags['updatecounter'], $this->proptags['meetingtype'], $this->proptags['owner_critical_change'])); + + if (isset($props[$this->proptags['meetingtype']]) && ($props[$this->proptags['meetingtype']] & mtgOutOfDate) == mtgOutOfDate) { + return true; + } + + // get the basedate to check for exception + $basedate = $this->getBasedateFromGlobalID($props[$this->proptags['goid']]); + + $calendarItems = $this->getCorrespondedCalendarItems(); + + foreach($calendarItems as $calendarItem) { + if ($calendarItem) { + $calendarItemProps = mapi_getprops($calendarItem, array( + $this->proptags['owner_critical_change'], + $this->proptags['updatecounter'], + $this->proptags['recurring'] + )); + + // If these items is recurring and basedate is found then open exception to compare it with meeting request + if (isset($calendarItemProps[$this->proptags['recurring']]) && $calendarItemProps[$this->proptags['recurring']] && $basedate) { + $recurr = new Recurrence($store, $calendarItem); + + if ($recurr->isException($basedate)) { + $attach = $recurr->getExceptionAttachment($basedate); + $exception = mapi_attach_openobj($attach, 0); + $occurrenceItemProps = mapi_getprops($exception, array( + $this->proptags['owner_critical_change'], + $this->proptags['updatecounter'] + )); + } + + // we found the exception, compare with it + if(isset($occurrenceItemProps)) { + if ((isset($occurrenceItemProps[$this->proptags['updatecounter']]) && $props[$this->proptags['updatecounter']] < $occurrenceItemProps[$this->proptags['updatecounter']]) + || (isset($occurrenceItemProps[$this->proptags['owner_critical_change']]) && $props[$this->proptags['owner_critical_change']] < $occurrenceItemProps[$this->proptags['owner_critical_change']])) { + + mapi_setprops($this->message, array($this->proptags['meetingtype'] => mtgOutOfDate, PR_ICON_INDEX => 1033)); + mapi_savechanges($this->message); + $result = true; + } + } else { + // we are not able to find exception, could mean that a significant change has occured on series + // and it deleted all exceptions, so compare with series + if ((isset($calendarItemProps[$this->proptags['updatecounter']]) && $props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]) + || (isset($calendarItemProps[$this->proptags['owner_critical_change']]) && $props[$this->proptags['owner_critical_change']] < $calendarItemProps[$this->proptags['owner_critical_change']])) { + + mapi_setprops($this->message, array($this->proptags['meetingtype'] => mtgOutOfDate, PR_ICON_INDEX => 1033)); + mapi_savechanges($this->message); + $result = true; + } + } + } else { + // normal / recurring series + if ((isset($calendarItemProps[$this->proptags['updatecounter']]) && $props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]) + || (isset($calendarItemProps[$this->proptags['owner_critical_change']]) && $props[$this->proptags['owner_critical_change']] < $calendarItemProps[$this->proptags['owner_critical_change']])) { + + mapi_setprops($this->message, array($this->proptags['meetingtype'] => mtgOutOfDate, PR_ICON_INDEX => 1033)); + mapi_savechanges($this->message); + $result = true; + } + } + } + } + + return $result; + } + + /** + * Function which checks received meeting request is updated later or not. + * @return boolean true if meeting request is updated later. + * @TODO: Implement handling for recurrings and exceptions. + */ + function isMeetingUpdated() + { + $result = false; + $store = $this->store; + $props = mapi_getprops($this->message, array($this->proptags['goid'], $this->proptags['goid2'], $this->proptags['updatecounter'], $this->proptags['owner_critical_change'], $this->proptags['updatecounter'])); + + $calendarItems = $this->getCorrespondedCalendarItems(); + + foreach($calendarItems as $calendarItem) { + if ($calendarItem) { + $calendarItemProps = mapi_getprops($calendarItem, array( + $this->proptags['updatecounter'], + $this->proptags['recurring'] + )); + + if(isset($calendarItemProps[$this->proptags['updatecounter']]) && isset($props[$this->proptags['updatecounter']]) && $calendarItemProps[$this->proptags['updatecounter']] > $props[$this->proptags['updatecounter']]) { + $result = true; + } + } + } + + return $result; + } + + /** + * Checks if there has been any significant changes on appointment/meeting item. + * Significant changes be: + * 1) startdate has been changed + * 2) duedate has been changed OR + * 3) recurrence pattern has been created, modified or removed + * + * @param Array oldProps old props before an update + * @param Number basedate basedate + * @param Boolean isRecurrenceChanged for change in recurrence pattern. + * isRecurrenceChanged true means Recurrence pattern has been changed, so clear all attendees response + */ + function checkSignificantChanges($oldProps, $basedate, $isRecurrenceChanged = false) + { + $message = null; + $attach = null; + + // If basedate is specified then we need to open exception message to clear recipient responses + if($basedate) { + $recurrence = new Recurrence($this->store, $this->message); + if($recurrence->isException($basedate)){ + $attach = $recurrence->getExceptionAttachment($basedate); + if ($attach) { + $message = mapi_attach_openobj($attach, MAPI_MODIFY); + } + } + } else { + // use normal message or recurring series message + $message = $this->message; + } + + if(!$message) { + return; + } + + $newProps = mapi_getprops($message, array($this->proptags['startdate'], $this->proptags['duedate'], $this->proptags['updatecounter'])); + + // Check whether message is updated or not. + if(isset($newProps[$this->proptags['updatecounter']]) && $newProps[$this->proptags['updatecounter']] == 0) { + return; + } + + if (($newProps[$this->proptags['startdate']] != $oldProps[$this->proptags['startdate']]) + || ($newProps[$this->proptags['duedate']] != $oldProps[$this->proptags['duedate']]) + || $isRecurrenceChanged) { + $this->clearRecipientResponse($message); + + mapi_setprops($message, array($this->proptags['owner_critical_change'] => time())); + + mapi_savechanges($message); + if ($attach) { // Also save attachment Object. + mapi_savechanges($attach); + } + } + } + + /** + * Clear responses of all attendees who have replied in past. + * @param MAPI_MESSAGE $message on which responses should be cleared + */ + function clearRecipientResponse($message) + { + $recipTable = mapi_message_getrecipienttable($message); + $recipsRows = mapi_table_queryallrows($recipTable, $this->recipprops); + + foreach($recipsRows as $recipient) { + if(($recipient[PR_RECIPIENT_FLAGS] & recipOrganizer) != recipOrganizer){ + // Recipient is attendee, set the trackstatus to "Not Responded" + $recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone; + } else { + // Recipient is organizer, this is not possible, but for safety + // it is best to clear the trackstatus for him as well by setting + // the trackstatus to "Organized". + $recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone; + } + mapi_message_modifyrecipients($message, MODRECIP_MODIFY, array($recipient)); + } + } + + /** + * Function returns corresponded calendar items attached with + * the meeting request. + * @return Array array of correlated calendar items. + */ + function getCorrespondedCalendarItems() + { + $store = $this->store; + $props = mapi_getprops($this->message, array($this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_NAME)); + + $basedate = $this->getBasedateFromGlobalID($props[$this->proptags['goid']]); + + // If Delegate is processing mr for Delegator then retrieve Delegator's store and calendar. + if (isset($props[PR_RCVD_REPRESENTING_NAME])) { + $delegatorStore = $this->getDelegatorStore($props); + $store = $delegatorStore['store']; + $calFolder = $delegatorStore['calFolder']; + } else { + $calFolder = $this->openDefaultCalendar(); + } + + // Finding item in calendar with GlobalID(0x3), not necessary that attendee is having recurring item, he/she can also have only a occurrence + $entryids = $this->findCalendarItems($props[$this->proptags['goid']], $calFolder); + + // Basedate found, so this meeting request is an update of an occurrence. + if ($basedate) { + if (!$entryids) { + // Find main recurring item in calendar with GlobalID(0x23) + $entryids = $this->findCalendarItems($props[$this->proptags['goid2']], $calFolder); + } + } + + $calendarItems = array(); + if ($entryids) { + foreach($entryids as $entryid) { + $calendarItems[] = mapi_msgstore_openentry($store, $entryid); + } + } + + return $calendarItems; + } + + /** + * Function which checks whether received meeting request is either conflicting with other appointments or not. + *@return mixed(boolean/integer) true if normal meeting is conflicting or an integer which specifies no of instances + * conflict of recurring meeting and false if meeting is not conflicting. + */ + function isMeetingConflicting($message = false, $userStore = false, $calFolder = false, $msgprops = false) + { + $returnValue = false; + $conflicting = false; + $noOfInstances = 0; + + if (!$message) $message = $this->message; + + if (!$userStore) $userStore = $this->store; + + if (!$calFolder) { + $root = mapi_msgstore_openentry($userStore); + $rootprops = mapi_getprops($root, array(PR_STORE_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_FREEBUSY_ENTRYIDS)); + + if(!isset($rootprops[PR_IPM_APPOINTMENT_ENTRYID])) { + return; + } + + $calFolder = mapi_msgstore_openentry($userStore, $rootprops[PR_IPM_APPOINTMENT_ENTRYID]); + } + + if (!$msgprops) $msgprops = mapi_getprops($message, array($this->proptags['goid'], $this->proptags['goid2'], $this->proptags['startdate'], $this->proptags['duedate'], $this->proptags['recurring'], $this->proptags['clipstart'], $this->proptags['clipend'])); + + if ($calFolder) { + // Meeting request is recurring, so get all occurrence and check for each occurrence whether it conflicts with other appointments in Calendar. + if (isset($msgprops[$this->proptags['recurring']]) && $msgprops[$this->proptags['recurring']]) { + // Apply recurrence class and retrieve all occurrences(max: 30 occurrence because recurrence can also be set as 'no end date') + $recurr = new Recurrence($userStore, $message); + $items = $recurr->getItems($msgprops[$this->proptags['clipstart']], $msgprops[$this->proptags['clipend']] * (24*24*60), 30); + + foreach ($items as $item) { + // Get all items in the timeframe that we want to book, and get the goid and busystatus for each item + $calendarItems = $recurr->getCalendarItems($userStore, $calFolder, $item[$this->proptags['startdate']], $item[$this->proptags['duedate']], array($this->proptags['goid'], $this->proptags['busystatus'], PR_OWNER_APPT_ID)); + + foreach ($calendarItems as $calendarItem) { + if ($calendarItem[$this->proptags['busystatus']] != fbFree) { + /** + * Only meeting requests have globalID, normal appointments do not have globalID + * so if any normal appointment if found then it is assumed to be conflict. + */ + if(isset($calendarItem[$this->proptags['goid']])) { + if ($calendarItem[$this->proptags['goid']] !== $msgprops[$this->proptags['goid']]) { + $noOfInstances++; + break; + } + } else { + $noOfInstances++; + break; + } + } + } + } + + $returnValue = $noOfInstances; + } else { + // Get all items in the timeframe that we want to book, and get the goid and busystatus for each item + $items = getCalendarItems($userStore, $calFolder, $msgprops[$this->proptags['startdate']], $msgprops[$this->proptags['duedate']], array($this->proptags['goid'], $this->proptags['busystatus'], PR_OWNER_APPT_ID)); + + foreach($items as $item) { + if ($item[$this->proptags['busystatus']] != fbFree) { + if(isset($item[$this->proptags['goid']])) { + if (($item[$this->proptags['goid']] !== $msgprops[$this->proptags['goid']]) + && ($item[$this->proptags['goid']] !== $msgprops[$this->proptags['goid2']])) { + $conflicting = true; + break; + } + } else { + $conflicting = true; + break; + } + } + } + + if ($conflicting) $returnValue = true; + } + } + return $returnValue; + } + + /** + * Function which adds organizer to recipient list which is passed. + * This function also checks if it has organizer. + * + * @param array $messageProps message properties + * @param array $recipients recipients list of message. + * @param boolean $isException true if we are processing recipient of exception + */ + function addDelegator($messageProps, &$recipients) + { + $hasDelegator = false; + // Check if meeting already has an organizer. + foreach ($recipients as $key => $recipient){ + if (isset($messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]) && $recipient[PR_EMAIL_ADDRESS] == $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]) + $hasDelegator = true; + } + + if (!$hasDelegator){ + // Create delegator. + $delegator = array(); + $delegator[PR_ENTRYID] = $messageProps[PR_RCVD_REPRESENTING_ENTRYID]; + $delegator[PR_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME]; + $delegator[PR_EMAIL_ADDRESS] = $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]; + $delegator[PR_RECIPIENT_TYPE] = MAPI_TO; + $delegator[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME]; + $delegator[PR_ADDRTYPE] = empty($messageProps[PR_RCVD_REPRESENTING_ADDRTYPE]) ? 'SMTP':$messageProps[PR_RCVD_REPRESENTING_ADDRTYPE]; + $delegator[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone; + $delegator[PR_RECIPIENT_FLAGS] = recipSendable; + + // Add organizer to recipients list. + array_unshift($recipients, $delegator); + } + } + + function getDelegatorStore($messageprops) + { + // Find the organiser of appointment in addressbook + $delegatorName = array(array(PR_DISPLAY_NAME => $messageprops[PR_RCVD_REPRESENTING_NAME])); + $ab = mapi_openaddressbook($this->session); + $user = mapi_ab_resolvename($ab, $delegatorName, EMS_AB_ADDRESS_LOOKUP); + + // Get StoreEntryID by username + $delegatorEntryid = mapi_msgstore_createentryid($this->store, $user[0][PR_EMAIL_ADDRESS]); + // Open store of the delegator + $delegatorStore = mapi_openmsgstore($this->session, $delegatorEntryid); + // Open root folder + $delegatorRoot = mapi_msgstore_openentry($delegatorStore, null); + // Get calendar entryID + $delegatorRootProps = mapi_getprops($delegatorRoot, array(PR_IPM_APPOINTMENT_ENTRYID)); + // Open the calendar Folder + $calFolder = mapi_msgstore_openentry($delegatorStore, $delegatorRootProps[PR_IPM_APPOINTMENT_ENTRYID]); + + return Array('store' => $delegatorStore, 'calFolder' => $calFolder); + } + + /** + * Function returns extra info about meeting timing along with message body + * which will be included in body while sending meeting request/response. + * + * @return string $meetingTimeInfo info about meeting timing along with message body + */ + function getMeetingTimeInfo() + { + return $this->meetingTimeInfo; + } + + /** + * Function sets extra info about meeting timing along with message body + * which will be included in body while sending meeting request/response. + * + * @param string $meetingTimeInfo info about meeting timing along with message body + */ + function setMeetingTimeInfo($meetingTimeInfo) + { + $this->meetingTimeInfo = $meetingTimeInfo; + } +} +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapi/class.recurrence.php b/sources/backend/zarafa/mapi/class.recurrence.php new file mode 100644 index 0000000..227d605 --- /dev/null +++ b/sources/backend/zarafa/mapi/class.recurrence.php @@ -0,0 +1,1577 @@ +. + * + */ + + include_once('backend/zarafa/mapi/class.baserecurrence.php'); + + /** + * Recurrence + * @author Steve Hardy + * @author Michel de Ron + */ + class Recurrence extends BaseRecurrence + { + /* + * ABOUT TIMEZONES + * + * Timezones are rather complicated here so here are some rules to think about: + * + * - Timestamps in mapi-like properties (so in PT_SYSTIME properties) are always in GMT (including + * the 'basedate' property in exceptions !!) + * - Timestamps for recurrence (so start/end of recurrence, and basedates for exceptions (everything + * outside the 'basedate' property in the exception !!), and start/endtimes for exceptions) are + * always in LOCAL time. + */ + + // All properties for a recipient that are interesting + var $recipprops = Array( + PR_ENTRYID, + PR_SEARCH_KEY, + PR_DISPLAY_NAME, + PR_EMAIL_ADDRESS, + PR_RECIPIENT_ENTRYID, + PR_RECIPIENT_TYPE, + PR_SEND_INTERNET_ENCODING, + PR_SEND_RICH_INFO, + PR_RECIPIENT_DISPLAY_NAME, + PR_ADDRTYPE, + PR_DISPLAY_TYPE, + PR_DISPLAY_TYPE_EX, + PR_RECIPIENT_TRACKSTATUS, + PR_RECIPIENT_TRACKSTATUS_TIME, + PR_RECIPIENT_FLAGS, + PR_ROWID + ); + + /** + * Constructor + * @param resource $store MAPI Message Store Object + * @param resource $message the MAPI (appointment) message + */ + function Recurrence($store, $message) + { + + $properties = array(); + $properties["entryid"] = PR_ENTRYID; + $properties["parent_entryid"] = PR_PARENT_ENTRYID; + $properties["message_class"] = PR_MESSAGE_CLASS; + $properties["icon_index"] = PR_ICON_INDEX; + $properties["subject"] = PR_SUBJECT; + $properties["display_to"] = PR_DISPLAY_TO; + $properties["importance"] = PR_IMPORTANCE; + $properties["sensitivity"] = PR_SENSITIVITY; + $properties["startdate"] = "PT_SYSTIME:PSETID_Appointment:0x820d"; + $properties["duedate"] = "PT_SYSTIME:PSETID_Appointment:0x820e"; + $properties["recurring"] = "PT_BOOLEAN:PSETID_Appointment:0x8223"; + $properties["recurring_data"] = "PT_BINARY:PSETID_Appointment:0x8216"; + $properties["busystatus"] = "PT_LONG:PSETID_Appointment:0x8205"; + $properties["label"] = "PT_LONG:PSETID_Appointment:0x8214"; + $properties["alldayevent"] = "PT_BOOLEAN:PSETID_Appointment:0x8215"; + $properties["private"] = "PT_BOOLEAN:PSETID_Common:0x8506"; + $properties["meeting"] = "PT_LONG:PSETID_Appointment:0x8217"; + $properties["startdate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8235"; + $properties["enddate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8236"; + $properties["recurring_pattern"] = "PT_STRING8:PSETID_Appointment:0x8232"; + $properties["location"] = "PT_STRING8:PSETID_Appointment:0x8208"; + $properties["duration"] = "PT_LONG:PSETID_Appointment:0x8213"; + $properties["responsestatus"] = "PT_LONG:PSETID_Appointment:0x8218"; + $properties["reminder"] = "PT_BOOLEAN:PSETID_Common:0x8503"; + $properties["reminder_minutes"] = "PT_LONG:PSETID_Common:0x8501"; + $properties["recurrencetype"] = "PT_LONG:PSETID_Appointment:0x8231"; + $properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a"; + $properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586"; + $properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords"; + $properties["reminder_time"] = "PT_SYSTIME:PSETID_Common:0x8502"; + $properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516"; + $properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517"; + $properties["basedate"] = "PT_SYSTIME:PSETID_Appointment:0x8228"; + $properties["timezone_data"] = "PT_BINARY:PSETID_Appointment:0x8233"; + $properties["timezone"] = "PT_STRING8:PSETID_Appointment:0x8234"; + $properties["flagdueby"] = "PT_SYSTIME:PSETID_Common:0x8560"; + $properties["side_effects"] = "PT_LONG:PSETID_Common:0x8510"; + $properties["hideattachments"] = "PT_BOOLEAN:PSETID_Common:0x8514"; + + $this->proptags = getPropIdsFromStrings($store, $properties); + + parent::BaseRecurrence($store, $message); + } + + /** + * Create an exception + * @param array $exception_props the exception properties (same properties as normal recurring items) + * @param date $base_date the base date of the exception (LOCAL time of non-exception occurrence) + * @param boolean $delete true - delete occurrence, false - create new exception or modify existing + * @param array $exception_recips true - delete occurrence, false - create new exception or modify existing + * @param mapi_message $copy_attach_from mapi message from which attachments should be copied + */ + function createException($exception_props, $base_date, $delete = false, $exception_recips = array(), $copy_attach_from = false) + { + $baseday = $this->dayStartOf($base_date); + $basetime = $baseday + $this->recur["startocc"] * 60; + + // Remove any pre-existing exception on this base date + if($this->isException($baseday)) { + $this->deleteException($baseday); // note that deleting an exception is different from creating a deleted exception (deleting an occurrence). + } + + if(!$delete) { + if(isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) { + return false; + } + // Properties in the attachment are the properties of the base object, plus $exception_props plus the base date + foreach (array("subject", "location", "label", "reminder", "reminder_minutes", "alldayevent", "busystatus") as $propname) { + if(isset($this->messageprops[$this->proptags[$propname]])) + $props[$this->proptags[$propname]] = $this->messageprops[$this->proptags[$propname]]; + } + + $props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}"; + unset($exception_props[PR_MESSAGE_CLASS]); + unset($exception_props[PR_ICON_INDEX]); + $props = $exception_props + $props; + + // Basedate in the exception attachment is the GMT time at which the original occurrence would have been + $props[$this->proptags["basedate"]] = $this->toGMT($this->tz, $basetime); + + if (!isset($exception_props[$this->proptags["startdate"]])) { + $props[$this->proptags["startdate"]] = $this->getOccurrenceStart($base_date); + } + + if (!isset($exception_props[$this->proptags["duedate"]])) { + $props[$this->proptags["duedate"]] = $this->getOccurrenceEnd($base_date); + } + + // synchronize commonstart/commonend with startdate/duedate + if(isset($props[$this->proptags["startdate"]])) { + $props[$this->proptags["commonstart"]] = $props[$this->proptags["startdate"]]; + } + + if(isset($props[$this->proptags["duedate"]])) { + $props[$this->proptags["commonend"]] = $props[$this->proptags["duedate"]]; + } + + // Save the data into an attachment + $this->createExceptionAttachment($props, $exception_recips, $copy_attach_from); + + $changed_item = array(); + + $changed_item["basedate"] = $baseday; + $changed_item["start"] = $this->fromGMT($this->tz, $props[$this->proptags["startdate"]]); + $changed_item["end"] = $this->fromGMT($this->tz, $props[$this->proptags["duedate"]]); + + if(array_key_exists($this->proptags["subject"], $exception_props)) { + $changed_item["subject"] = $exception_props[$this->proptags["subject"]]; + } + + if(array_key_exists($this->proptags["location"], $exception_props)) { + $changed_item["location"] = $exception_props[$this->proptags["location"]]; + } + + if(array_key_exists($this->proptags["label"], $exception_props)) { + $changed_item["label"] = $exception_props[$this->proptags["label"]]; + } + + if(array_key_exists($this->proptags["reminder"], $exception_props)) { + $changed_item["reminder_set"] = $exception_props[$this->proptags["reminder"]]; + } + + if(array_key_exists($this->proptags["reminder_minutes"], $exception_props)) { + $changed_item["remind_before"] = $exception_props[$this->proptags["reminder_minutes"]]; + } + + if(array_key_exists($this->proptags["alldayevent"], $exception_props)) { + $changed_item["alldayevent"] = $exception_props[$this->proptags["alldayevent"]]; + } + + if(array_key_exists($this->proptags["busystatus"], $exception_props)) { + $changed_item["busystatus"] = $exception_props[$this->proptags["busystatus"]]; + } + + // Add the changed occurrence to the list + array_push($this->recur["changed_occurences"], $changed_item); + } else { + // Delete the occurrence by placing it in the deleted occurrences list + array_push($this->recur["deleted_occurences"], $baseday); + } + + // Turn on hideattachments, because the attachments in this item are the exceptions + mapi_setprops($this->message, array ( $this->proptags["hideattachments"] => true )); + + // Save recurrence data to message + $this->saveRecurrence(); + + return true; + } + + /** + * Modifies an existing exception, but only updates the given properties + * NOTE: You can't remove properites from an exception, only add new ones + */ + function modifyException($exception_props, $base_date, $exception_recips = array(), $copy_attach_from = false) + { + if(isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) { + return false; + } + + $baseday = $this->dayStartOf($base_date); + $basetime = $baseday + $this->recur["startocc"] * 60; + $extomodify = false; + + for($i = 0, $len = count($this->recur["changed_occurences"]); $i < $len; $i++) { + if($this->isSameDay($this->recur["changed_occurences"][$i]["basedate"], $baseday)) + $extomodify = &$this->recur["changed_occurences"][$i]; + } + + if(!$extomodify) + return false; + + // remove basedate property as we want to preserve the old value + // client will send basedate with time part as zero, so discard that value + unset($exception_props[$this->proptags["basedate"]]); + + if(array_key_exists($this->proptags["startdate"], $exception_props)) { + $extomodify["start"] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]); + } + + if(array_key_exists($this->proptags["duedate"], $exception_props)) { + $extomodify["end"] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]); + } + + if(array_key_exists($this->proptags["subject"], $exception_props)) { + $extomodify["subject"] = $exception_props[$this->proptags["subject"]]; + } + + if(array_key_exists($this->proptags["location"], $exception_props)) { + $extomodify["location"] = $exception_props[$this->proptags["location"]]; + } + + if(array_key_exists($this->proptags["label"], $exception_props)) { + $extomodify["label"] = $exception_props[$this->proptags["label"]]; + } + + if(array_key_exists($this->proptags["reminder"], $exception_props)) { + $extomodify["reminder_set"] = $exception_props[$this->proptags["reminder"]]; + } + + if(array_key_exists($this->proptags["reminder_minutes"], $exception_props)) { + $extomodify["remind_before"] = $exception_props[$this->proptags["reminder_minutes"]]; + } + + if(array_key_exists($this->proptags["alldayevent"], $exception_props)) { + $extomodify["alldayevent"] = $exception_props[$this->proptags["alldayevent"]]; + } + + if(array_key_exists($this->proptags["busystatus"], $exception_props)) { + $extomodify["busystatus"] = $exception_props[$this->proptags["busystatus"]]; + } + + $exception_props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}"; + + // synchronize commonstart/commonend with startdate/duedate + if(isset($exception_props[$this->proptags["startdate"]])) { + $exception_props[$this->proptags["commonstart"]] = $exception_props[$this->proptags["startdate"]]; + } + + if(isset($exception_props[$this->proptags["duedate"]])) { + $exception_props[$this->proptags["commonend"]] = $exception_props[$this->proptags["duedate"]]; + } + + $attach = $this->getExceptionAttachment($baseday); + if(!$attach) { + if ($copy_attach_from) { + $this->deleteExceptionAttachment($base_date); + $this->createException($exception_props, $base_date, false, $exception_recips, $copy_attach_from); + } else { + $this->createExceptionAttachment($exception_props, $exception_recips, $copy_attach_from); + } + } else { + $message = mapi_attach_openobj($attach, MAPI_MODIFY); + + // Set exception properties on embedded message and save + mapi_setprops($message, $exception_props); + $this->setExceptionRecipients($message, $exception_recips, false); + mapi_savechanges($message); + + // If a new start or duedate is provided, we update the properties 'PR_EXCEPTION_STARTTIME' and 'PR_EXCEPTION_ENDTIME' + // on the attachment which holds the embedded msg and save everything. + $props = array(); + if (isset($exception_props[$this->proptags["startdate"]])) { + $props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]); + } + if (isset($exception_props[$this->proptags["duedate"]])) { + $props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]); + } + if (!empty($props)) { + mapi_setprops($attach, $props); + } + + mapi_savechanges($attach); + } + + // Save recurrence data to message + $this->saveRecurrence(); + + return true; + } + + // Checks to see if the following is true: + // 1) The exception to be created doesn't create two exceptions starting on one day (however, they can END on the same day by modifying duration) + // 2) The exception to be created doesn't 'jump' over another occurrence (which may be an exception itself!) + // + // Both $basedate and $start are in LOCAL time + function isValidExceptionDate($basedate, $start) + { + // The way we do this is to look at the days that we're 'moving' the item in the exception. Each + // of these days may only contain the item that we're modifying. Any other item violates the rules. + + if($this->isException($basedate)) { + // If we're modifying an exception, we want to look at the days that we're 'moving' compared to where + // the exception used to be. + $oldexception = $this->getChangeException($basedate); + $prevday = $this->dayStartOf($oldexception["start"]); + } else { + // If its a new exception, we want to look at the original placement of this item. + $prevday = $basedate; + } + + $startday = $this->dayStartOf($start); + + // Get all the occurrences on the days between the basedate (may be reversed) + if($prevday < $startday) + $items = $this->getItems($this->toGMT($this->tz, $prevday), $this->toGMT($this->tz, $startday + 24 * 60 * 60)); + else + $items = $this->getItems($this->toGMT($this->tz, $startday), $this->toGMT($this->tz, $prevday + 24 * 60 * 60)); + + // There should now be exactly one item, namely the item that we are modifying. If there are any other items in the range, + // then we abort the change, since one of the rules has been violated. + return count($items) == 1; + } + + /** + * Check to see if the exception proposed at a certain basedate is allowed concerning reminder times: + * + * Both must be true: + * - reminder time of this item is not before the starttime of the previous recurring item + * - reminder time of the next item is not before the starttime of this item + * + * @param date $basedate the base date of the exception (LOCAL time of non-exception occurrence) + * @param string $reminderminutes reminder minutes which is set of the item + * @param date $startdate the startdate of the selected item + * @returns boolean if the reminder minutes value valid (FALSE if either of the rules above are FALSE) + */ + function isValidReminderTime($basedate, $reminderminutes, $startdate) + { + $isreminderrangeset = false; + + // get all occurence items before the seleceted items occurence starttime + $occitems = $this->getItems($this->messageprops[$this->proptags["startdate"]], $this->toGMT($this->tz, $basedate)); + + if(!empty($occitems)) { + // as occitems array is sorted in ascending order of startdate, to get the previous occurence we take the last items in occitems . + $previousitem_startdate = $occitems[count($occitems) - 1][$this->proptags["startdate"]]; + + // if our reminder is set before or equal to the beginning of the previous occurrence, then that's not allowed + if($startdate - ($reminderminutes*60) <= $previousitem_startdate) + return false; + } + + // Get the endtime of the current occurrence and find the next two occurrences (including the current occurrence) + $currentOcc = $this->getItems($this->toGMT($this->tz, $basedate), 0x7ff00000, 2, true); + + // If there are another two occurrences, then the first is the current occurrence, and the one after that + // is the next occurrence. + if(count($currentOcc) > 1) { + $next = $currentOcc[1]; + // Get reminder time of the next occurrence. + $nextOccReminderTime = $next[$this->proptags["startdate"]] - ($next[$this->proptags["reminder_minutes"]] * 60); + // If the reminder time of the next item is before the start of this item, then that's not allowed + if($nextOccReminderTime <= $startdate) + return false; + } + + // All was ok + return true; + } + + function setRecurrence($tz, $recur) + { + // only reset timezone if specified + if($tz) + $this->tz = $tz; + + $this->recur = $recur; + + if(!isset($this->recur["changed_occurences"])) + $this->recur["changed_occurences"] = Array(); + + if(!isset($this->recur["deleted_occurences"])) + $this->recur["deleted_occurences"] = Array(); + + $this->deleteAttachments(); + $this->saveRecurrence(); + + // if client has not set the recurring_pattern then we should generate it and save it + $messageProps = mapi_getprops($this->message, Array($this->proptags["recurring_pattern"])); + if(empty($messageProps[$this->proptags["recurring_pattern"]])) { + $this->saveRecurrencePattern(); + } + } + + // Returns the start or end time of the occurrence on the given base date. + // This assumes that the basedate you supply is in LOCAL time + function getOccurrenceStart($basedate) { + $daystart = $this->dayStartOf($basedate); + return $this->toGMT($this->tz, $daystart + $this->recur["startocc"] * 60); + } + + function getOccurrenceEnd($basedate) { + $daystart = $this->dayStartOf($basedate); + return $this->toGMT($this->tz, $daystart + $this->recur["endocc"] * 60); + } + + + // Backwards compatible code + function getOccurenceStart($basedate) { + return $this->getOccurrenceStart($basedate); + } + function getOccurenceEnd($basedate) { + return $this->getOccurrenceEnd($basedate); + } + + /** + * This function returns the next remindertime starting from $timestamp + * When no next reminder exists, false is returned. + * + * Note: Before saving this new reminder time (when snoozing), you must check for + * yourself if this reminder time is earlier than your snooze time, else + * use your snooze time and not this reminder time. + */ + function getNextReminderTime($timestamp) + { + /** + * Get next item from now until forever, but max 1 item with reminder set + * Note 0x7ff00000 instead of 0x7fffffff because of possible overflow failures when converting to GMT.... + * Here for getting next 10 occurences assuming that next here we will be able to find + * nextreminder occurence in 10 occureneces + */ + $items = $this->getItems($timestamp, 0x7ff00000, 10, true); + + // Initially setting nextreminder to false so when no next reminder exists, false is returned. + $nextreminder = false; + /** + * Loop through all reminder which we get in items variable + * and check whether the remindertime is greater than timestamp. + * On the first occurence of greater nextreminder break the loop + * and return the value to calling function. + */ + for($i = 0, $len = count($items); $i < $len; $i++) + { + $item = $items[$i]; + $tempnextreminder = $item[$this->proptags["startdate"]] - ( $item[$this->proptags["reminder_minutes"]] * 60 ); + + // If tempnextreminder is greater than timestamp then save it in nextreminder and break from the loop. + if($tempnextreminder > $timestamp) + { + $nextreminder = $tempnextreminder; + break; + } + } + return $nextreminder; + } + + /** + * Note: Static function, more like a utility function. + * + * Gets all the items (including recurring items) in the specified calendar in the given timeframe. Items are + * included as a whole if they overlap the interval <$start, $end> (non-inclusive). This means that if the interval + * is <08:00 - 14:00>, the item [6:00 - 8:00> is NOT included, nor is the item [14:00 - 16:00>. However, the item + * [7:00 - 9:00> is included as a whole, and is NOT capped to [8:00 - 9:00>. + * + * @param $store resource The store in which the calendar resides + * @param $calendar resource The calendar to get the items from + * @param $viewstart int Timestamp of beginning of view window + * @param $viewend int Timestamp of end of view window + * @param $propsrequested array Array of properties to return + * @param $rows array Array of rowdata as if they were returned directly from mapi_table_queryrows. Each recurring item is + * expanded so that it seems that there are only many single appointments in the table. + */ + static function getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested) + { + return getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested); + } + + + /***************************************************************************************************************** + * CODE BELOW THIS LINE IS FOR INTERNAL USE ONLY + ***************************************************************************************************************** + */ + + /** + * Generates and stores recurrence pattern string to recurring_pattern property. + */ + function saveRecurrencePattern() + { + // Start formatting the properties in such a way we can apply + // them directly into the recurrence pattern. + $type = $this->recur['type']; + $everyn = $this->recur['everyn']; + $start = $this->recur['start']; + $end = $this->recur['end']; + $term = $this->recur['term']; + $numocc = isset($this->recur['numoccur']) ? $this->recur['numoccur'] : false; + $startocc = $this->recur['startocc']; + $endocc = $this->recur['endocc']; + $pattern = ''; + $occSingleDayRank = false; + $occTimeRange = ($startocc != 0 && $endocc != 0); + + switch ($type) { + // Daily + case 0x0A: + if ($everyn == 1) { + $type = _('workday'); + $occSingleDayRank = true; + } else if ($everyn == (24 * 60)) { + $type = _('day'); + $occSingleDayRank = true; + } else { + $everyn /= (24 * 60); + $type = _('days'); + $occSingleDayRank = false; + } + break; + // Weekly + case 0x0B: + if ($everyn == 1) { + $type = _('week'); + $occSingleDayRank = true; + } else { + $type = _('weeks'); + $occSingleDayRank = false; + } + break; + // Monthly + case 0x0C: + if ($everyn == 1) { + $type = _('month'); + $occSingleDayRank = true; + } else { + $type = _('months'); + $occSingleDayRank = false; + } + break; + // Yearly + case 0x0D: + if ($everyn <= 12) { + $everyn = 1; + $type = _('year'); + $occSingleDayRank = true; + } else { + $everyn = $everyn / 12; + $type = _('years'); + $occSingleDayRank = false; + } + break; + } + + // get timings of the first occurence + $firstoccstartdate = isset($startocc) ? $start + (((int) $startocc) * 60) : $start; + $firstoccenddate = isset($endocc) ? $end + (((int) $endocc) * 60) : $end; + + $start = gmdate(_('d-m-Y'), $firstoccstartdate); + $end = gmdate(_('d-m-Y'), $firstoccenddate); + $startocc = gmdate(_('G:i'), $firstoccstartdate); + $endocc = gmdate(_('G:i'), $firstoccenddate); + + // Based on the properties, we need to generate the recurrence pattern string. + // This is obviously very easy since we can simply concatenate a bunch of strings, + // however this messes up translations for languages which order their words + // differently. + // To improve translation quality we create a series of default strings, in which + // we only have to fill in the correct variables. The base string is thus selected + // based on the available properties. + if ($term == 0x23) { + // Never ends + if ($occTimeRange) { + if ($occSingleDayRank) { + $pattern = sprintf(_('Occurs every %s effective %s from %s to %s.'), $type, $start, $startocc, $endocc); + } else { + $pattern = sprintf(_('Occurs every %s %s effective %s from %s to %s.'), $everyn, $type, $start, $startocc, $endocc); + } + } else { + if ($occSingleDayRank) { + $pattern = sprintf(_('Occurs every %s effective %s.'), $type, $start); + } else { + $pattern = sprintf(_('Occurs every %s %s effective %s.'), $everyn, $type, $start); + } + } + } else if ($term == 0x22) { + // After a number of times + if ($occTimeRange) { + if ($occSingleDayRank) { + $pattern = sprintf(ngettext('Occurs every %s effective %s for %s occurence from %s to %s.', + 'Occurs every %s effective %s for %s occurences from %s to %s.', $numocc), $type, $start, $numocc, $startocc, $endocc); + } else { + $pattern = sprintf(ngettext('Occurs every %s %s effective %s for %s occurence from %s to %s.', + 'Occurs every %s %s effective %s for %s occurences %s to %s.', $numocc), $everyn, $type, $start, $numocc, $startocc, $endocc); + } + } else { + if ($occSingleDayRank) { + $pattern = sprintf(ngettext('Occurs every %s effective %s for %s occurence.', + 'Occurs every %s effective %s for %s occurences.', $numocc), $type, $start, $numocc); + } else { + $pattern = sprintf(ngettext('Occurs every %s %s effective %s for %s occurence.', + 'Occurs every %s %s effective %s for %s occurences.', $numocc), $everyn, $type, $start, $numocc); + } + } + } else if ($term == 0x21) { + // After the given enddate + if ($occTimeRange) { + if ($occSingleDayRank) { + $pattern = sprintf(_('Occurs every %s effective %s until %s from %s to %s.'), $type, $start, $end, $startocc, $endocc); + } else { + $pattern = sprintf(_('Occurs every %s %s effective %s until %s from %s to %s.'), $everyn, $type, $start, $end, $startocc, $endocc); + } + } else { + if ($occSingleDayRank) { + $pattern = sprintf(_('Occurs every %s effective %s until %s.'), $type, $start, $end); + } else { + $pattern = sprintf(_('Occurs every %s %s effective %s until %s.'), $everyn, $type, $start, $end); + } + } + } + + if(!empty($pattern)) { + mapi_setprops($this->message, Array($this->proptags["recurring_pattern"] => $pattern )); + } + } + + /* + * Remove an exception by base_date. This is the base date in local daystart time + */ + function deleteException($base_date) + { + // Remove all exceptions on $base_date from the deleted and changed occurrences lists + + // Remove all items in $todelete from deleted_occurences + $new = Array(); + + foreach($this->recur["deleted_occurences"] as $entry) { + if($entry != $base_date) + $new[] = $entry; + } + $this->recur["deleted_occurences"] = $new; + + $new = Array(); + + foreach($this->recur["changed_occurences"] as $entry) { + if(!$this->isSameDay($entry["basedate"], $base_date)) + $new[] = $entry; + else + $this->deleteExceptionAttachment($this->toGMT($this->tz, $base_date + $this->recur["startocc"] * 60)); + } + + $this->recur["changed_occurences"] = $new; + } + + /** + * Function which saves the exception data in an attachment. + * @param array $exception_props the exception data (like any other MAPI appointment) + * @param array $exception_recips list of recipients + * @param mapi_message $copy_attach_from mapi message from which attachments should be copied + * @return array properties of the exception + */ + function createExceptionAttachment($exception_props, $exception_recips = array(), $copy_attach_from = false) + { + // Create new attachment. + $attachment = mapi_message_createattach($this->message); + $props = array(); + $props[PR_ATTACHMENT_FLAGS] = 2; + $props[PR_ATTACHMENT_HIDDEN] = true; + $props[PR_ATTACHMENT_LINKID] = 0; + $props[PR_ATTACH_FLAGS] = 0; + $props[PR_ATTACH_METHOD] = 5; + $props[PR_DISPLAY_NAME] = "Exception"; + $props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]); + $props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]); + mapi_message_setprops($attachment, $props); + + $imessage = mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY); + + if ($copy_attach_from) { + $attachmentTable = mapi_message_getattachmenttable($copy_attach_from); + if($attachmentTable) { + $attachments = mapi_table_queryallrows($attachmentTable, array(PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD)); + + foreach($attachments as $attach_props){ + $attach_old = mapi_message_openattach($copy_attach_from, (int) $attach_props[PR_ATTACH_NUM]); + $attach_newResourceMsg = mapi_message_createattach($imessage); + mapi_copyto($attach_old, array(), array(), $attach_newResourceMsg, 0); + mapi_savechanges($attach_newResourceMsg); + } + } + } + + $props = $props + $exception_props; + + // FIXME: the following piece of code is written to fix the creation + // of an exception. This is only a quickfix as it is not yet possible + // to change an existing exception. + // remove mv properties when needed + foreach($props as $propTag=>$propVal){ + if ((mapi_prop_type($propTag) & MV_FLAG) == MV_FLAG && is_null($propVal)){ + unset($props[$propTag]); + } + } + + mapi_message_setprops($imessage, $props); + + $this->setExceptionRecipients($imessage, $exception_recips, true); + + mapi_message_savechanges($imessage); + mapi_message_savechanges($attachment); + } + + /** + * Function which deletes the attachment of an exception. + * @param date $base_date base date of the attachment. Should be in GMT. The attachment + * actually saves the real time of the original date, so we have + * to check whether it's on the same day. + */ + function deleteExceptionAttachment($base_date) + { + $attachments = mapi_message_getattachmenttable($this->message); + $attachTable = mapi_table_queryallrows($attachments, Array(PR_ATTACH_NUM)); + + foreach($attachTable as $attachRow) + { + $tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]); + $exception = mapi_attach_openobj($tempattach); + + $data = mapi_message_getprops($exception, array($this->proptags["basedate"])); + + if($this->dayStartOf($this->fromGMT($this->tz, $data[$this->proptags["basedate"]])) == $this->dayStartOf($base_date)) { + mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]); + } + } + } + + /** + * Function which deletes all attachments of a message. + */ + function deleteAttachments() + { + $attachments = mapi_message_getattachmenttable($this->message); + $attachTable = mapi_table_queryallrows($attachments, Array(PR_ATTACH_NUM, PR_ATTACHMENT_HIDDEN)); + + foreach($attachTable as $attachRow) + { + if(isset($attachRow[PR_ATTACHMENT_HIDDEN]) && $attachRow[PR_ATTACHMENT_HIDDEN]) { + mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]); + } + } + } + + /** + * Get an exception attachment based on its basedate + */ + function getExceptionAttachment($base_date) + { + // Retrieve only embedded messages + $attach_res = Array(RES_AND, + Array( + Array(RES_PROPERTY, + Array(RELOP => RELOP_EQ, + ULPROPTAG => PR_ATTACH_METHOD, + VALUE => array(PR_ATTACH_METHOD => 5) + ) + ) + ) + ); + $attachments = mapi_message_getattachmenttable($this->message); + $attachRows = mapi_table_queryallrows($attachments, Array(PR_ATTACH_NUM), $attach_res); + + if(is_array($attachRows)) { + foreach($attachRows as $attachRow) + { + $tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]); + $exception = mapi_attach_openobj($tempattach); + + $data = mapi_message_getprops($exception, array($this->proptags["basedate"])); + + if(isset($data[$this->proptags["basedate"]]) && $this->isSameDay($this->fromGMT($this->tz,$data[$this->proptags["basedate"]]), $base_date)) { + return $tempattach; + } + } + } + + return false; + } + + /** + * processOccurrenceItem, adds an item to a list of occurrences, but only if the following criteria are met: + * - The resulting occurrence (or exception) starts or ends in the interval <$start, $end> + * - The ocurrence isn't specified as a deleted occurrence + * @param array $items reference to the array to be added to + * @param date $start start of timeframe in GMT TIME + * @param date $end end of timeframe in GMT TIME + * @param date $basedate (hour/sec/min assumed to be 00:00:00) in LOCAL TIME OF THE OCCURRENCE + * @param int $startocc start of occurrence since beginning of day in minutes + * @param int $endocc end of occurrence since beginning of day in minutes + * @param int $tz the timezone info for this occurrence ( applied to $basedate / $startocc / $endocc ) + * @param bool $reminderonly If TRUE, only add the item if the reminder is set + */ + function processOccurrenceItem(&$items, $start, $end, $basedate, $startocc, $endocc, $tz, $reminderonly) + { + $exception = $this->isException($basedate); + if($exception){ + return false; + }else{ + $occstart = $basedate + $startocc * 60; + $occend = $basedate + $endocc * 60; + + // Convert to GMT + $occstart = $this->toGMT($tz, $occstart); + $occend = $this->toGMT($tz, $occend); + + /** + * FIRST PART : Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot + * see any part of the appointment. Partial overlaps DO match. + * + * SECOND PART : check if occurence is not a zero duration occurrence which + * starts at 00:00 and ends on 00:00. if it is so, then process + * the occurrence and send it in response. + */ + if(($occstart >= $end || $occend <= $start) && !($occstart == $occend && $occstart == $start)) + return; + + // Properties for this occurrence are the same as the main object, + // With these properties overridden + $newitem = $this->messageprops; + $newitem[$this->proptags["startdate"]] = $occstart; + $newitem[$this->proptags["duedate"]] = $occend; + $newitem[$this->proptags["commonstart"]] = $occstart; + $newitem[$this->proptags["commonend"]] = $occend; + $newitem["basedate"] = $basedate; + } + + // If reminderonly is set, only add reminders + if($reminderonly && (!isset($newitem[$this->proptags["reminder"]]) || $newitem[$this->proptags["reminder"]] == false)) + return; + + $items[] = $newitem; + } + + /** + * processExceptionItem, adds an all exception item to a list of occurrences, without any constraint on timeframe + * @param array $items reference to the array to be added to + * @param date $start start of timeframe in GMT TIME + * @param date $end end of timeframe in GMT TIME + */ + function processExceptionItems(&$items, $start, $end) + { + $limit = 0; + foreach($this->recur["changed_occurences"] as $exception) { + + // Convert to GMT + $occstart = $this->toGMT($this->tz, $exception["start"]); + $occend = $this->toGMT($this->tz, $exception["end"]); + + // Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot + // see any part of the appointment. Partial overlaps DO match. + if($occstart >= $end || $occend <= $start) + continue; + + array_push($items, $this->getExceptionProperties($exception)); + if($limit && (count($items) == $limit)) + break; + } + } + + /** + * Function which verifies if on the given date an exception, delete or change, occurs. + * @param date $date the date + * @return array the exception, true - if an occurrence is deleted on the given date, false - no exception occurs on the given date + */ + function isException($basedate) + { + if($this->isDeleteException($basedate)) + return true; + + if($this->getChangeException($basedate) != false) + return true; + + return false; + } + + /** + * Returns TRUE if there is a DELETE exception on the given base date + */ + function isDeleteException($basedate) + { + // Check if the occurrence is deleted on the specified date + foreach($this->recur["deleted_occurences"] as $deleted) + { + if($this->isSameDay($deleted, $basedate)) + return true; + } + + return false; + } + + /** + * Returns the exception if there is a CHANGE exception on the given base date, or FALSE otherwise + */ + function getChangeException($basedate) + { + // Check if the occurrence is modified on the specified date + foreach($this->recur["changed_occurences"] as $changed) + { + if($this->isSameDay($changed["basedate"], $basedate)) + return $changed; + } + + return false; + } + + /** + * Function to see if two dates are on the same day + * @param date $time1 date 1 + * @param date $time2 date 2 + * @return boolean Returns TRUE when both dates are on the same day + */ + function isSameDay($date1, $date2) + { + $time1 = $this->gmtime($date1); + $time2 = $this->gmtime($date2); + + return $time1["tm_mon"] == $time2["tm_mon"] && $time1["tm_year"] == $time2["tm_year"] && $time1["tm_mday"] == $time2["tm_mday"]; + } + + /** + * Function to get all properties of a single changed exception. + * @param date $date base date of exception + * @return array associative array of properties for the exception, compatible with + */ + function getExceptionProperties($exception) + { + // Exception has same properties as main object, with some properties overridden: + $item = $this->messageprops; + + // Special properties + $item["exception"] = true; + $item["basedate"] = $exception["basedate"]; // note that the basedate is always in local time ! + + // MAPI-compatible properties (you can handle an exception as a normal calendar item like this) + $item[$this->proptags["startdate"]] = $this->toGMT($this->tz, $exception["start"]); + $item[$this->proptags["duedate"]] = $this->toGMT($this->tz, $exception["end"]); + $item[$this->proptags["commonstart"]] = $item[$this->proptags["startdate"]]; + $item[$this->proptags["commonend"]] = $item[$this->proptags["duedate"]]; + + if(isset($exception["subject"])) { + $item[$this->proptags["subject"]] = $exception["subject"]; + } + + if(isset($exception["label"])) { + $item[$this->proptags["label"]] = $exception["label"]; + } + + if(isset($exception["alldayevent"])) { + $item[$this->proptags["alldayevent"]] = $exception["alldayevent"]; + } + + if(isset($exception["location"])) { + $item[$this->proptags["location"]] = $exception["location"]; + } + + if(isset($exception["remind_before"])) { + $item[$this->proptags["reminder_minutes"]] = $exception["remind_before"]; + } + + if(isset($exception["reminder_set"])) { + $item[$this->proptags["reminder"]] = $exception["reminder_set"]; + } + + if(isset($exception["busystatus"])) { + $item[$this->proptags["busystatus"]] = $exception["busystatus"]; + } + + return $item; + } + + /** + * Function which sets recipients for an exception. + * + * The $exception_recips can be provided in 2 ways: + * - A delta which indicates which recipients must be added, removed or deleted. + * - A complete array of the recipients which should be applied to the message. + * + * The first option is preferred as it will require less work to be executed. + * + * @param resource $message exception attachment of recurring item + * @param array $exception_recips list of recipients + * @param boolean $copy_orig_recips True to copy all recipients which are on the original + * message to the attachment by default. False if only the $exception_recips changes should + * be applied. + */ + function setExceptionRecipients($message, $exception_recips, $copy_orig_recips = true) + { + if (isset($exception_recips['add']) || isset($exception_recips['remove']) || isset($exception_recips['modify'])) { + $this->setDeltaExceptionRecipients($message, $exception_recips, $copy_orig_recips); + } else { + $this->setAllExceptionRecipients($message, $exception_recips); + } + } + + /** + * Function which applies the provided delta for recipients changes to the exception. + * + * The $exception_recips should be an array containing the following keys: + * - "add": this contains an array of recipients which must be added + * - "remove": This contains an array of recipients which must be removed + * - "modify": This contains an array of recipients which must be modified + * + * @param resource $message exception attachment of recurring item + * @param array $exception_recips list of recipients + * @param boolean $copy_orig_recips True to copy all recipients which are on the original + * message to the attachment by default. False if only the $exception_recips changes should + * be applied. + */ + function setDeltaExceptionRecipients($exception, $exception_recips, $copy_orig_recips) + { + // Check if the recipients from the original message should be copied, + // if so, open the recipient table of the parent message and apply all + // rows on the target recipient. + if ($copy_orig_recips === true) { + $origTable = mapi_message_getrecipienttable($this->message); + $recipientRows = mapi_table_queryallrows($origTable, $this->recipprops); + mapi_message_modifyrecipients($exception, MODRECIP_ADD, $recipientRows); + } + + // Add organizer to meeting only if it is not organized. + $msgprops = mapi_getprops($exception, array(PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_ADDRTYPE, $this->proptags['responsestatus'])); + if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized){ + $this->addOrganizer($msgprops, $exception_recips['add']); + } + + // Remove all deleted recipients + if (isset($exception_recips['remove'])) { + foreach ($exception_recips['remove'] as &$recip) { + if (!isset($recipient[PR_RECIPIENT_FLAGS]) || $recip[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) { + $recip[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted; + } else { + $recip[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable; + } + $recip[PR_RECIPIENT_TRACKSTATUS] = olResponseNone; // No Response required + } + unset($recip); + mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['remove']); + } + + // Add all new recipients + if (isset($exception_recips['add'])) { + mapi_message_modifyrecipients($exception, MODRECIP_ADD, $exception_recips['add']); + } + + // Modify the existing recipients + if (isset($exception_recips['modify'])) { + mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['modify']); + } + } + + /** + * Function which applies the provided recipients to the exception, also checks for deleted recipients. + * + * The $exception_recips should be an array containing all recipients which must be applied + * to the exception. This will copy all recipients from the original message and then start filter + * out all recipients which are not provided by the $exception_recips list. + * + * @param resource $message exception attachment of recurring item + * @param array $exception_recips list of recipients + */ + function setAllExceptionRecipients($message, $exception_recips) + { + $deletedRecipients = array(); + $useMessageRecipients = false; + + $recipientTable = mapi_message_getrecipienttable($message); + $recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops); + + if (empty($recipientRows)) { + $useMessageRecipients = true; + $recipientTable = mapi_message_getrecipienttable($this->message); + $recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops); + } + + // Add organizer to meeting only if it is not organized. + $msgprops = mapi_getprops($message, array(PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_ADDRTYPE, $this->proptags['responsestatus'])); + if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized){ + $this->addOrganizer($msgprops, $exception_recips); + } + + if (!empty($exception_recips)) { + foreach($recipientRows as $key => $recipient) { + $found = false; + foreach($exception_recips as $excep_recip) { + if (isset($recipient[PR_SEARCH_KEY]) && isset($excep_recip[PR_SEARCH_KEY]) && $recipient[PR_SEARCH_KEY] == $excep_recip[PR_SEARCH_KEY]) + $found = true; + } + + if (!$found) { + $foundInDeletedRecipients = false; + // Look if the $recipient is in the list of deleted recipients + if (!empty($deletedRecipients)) { + foreach($deletedRecipients as $recip) { + if ($recip[PR_SEARCH_KEY] == $recipient[PR_SEARCH_KEY]){ + $foundInDeletedRecipients = true; + break; + } + } + } + + // If recipient is not in list of deleted recipient, add him + if (!$foundInDeletedRecipients) { + if (!isset($recipient[PR_RECIPIENT_FLAGS]) || $recipient[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) { + $recipient[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted; + } else { + $recipient[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable; + } + $recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone; // No Response required + $deletedRecipients[] = $recipient; + } + } + + // When $message contains a non-empty recipienttable, we must delete the recipients + // before re-adding them. However, when $message is doesn't contain any recipients, + // we are using the recipient table of the original message ($this->message) + // rather then $message. In that case, we don't need to remove the recipients + // from the $message, as the recipient table is already empty, and + // mapi_message_modifyrecipients() will throw an error. + if ($useMessageRecipients === false) { + mapi_message_modifyrecipients($message, MODRECIP_REMOVE, array($recipient)); + } + } + $exception_recips = array_merge($exception_recips, $deletedRecipients); + } else { + $exception_recips = $recipientRows; + } + + if (!empty($exception_recips)) { + // Set the new list of recipients on the exception message, this also removes the existing recipients + mapi_message_modifyrecipients($message, 0, $exception_recips); + } + } + + /** + * Function returns basedates of all changed occurrences + *@return array array( + 0 => 123459321 + ) + */ + function getAllExceptions() + { + $result = false; + if (!empty($this->recur["changed_occurences"])) { + $result = array(); + foreach($this->recur["changed_occurences"] as $exception) { + $result[] = $exception["basedate"]; + } + return $result; + } + return $result; + } + + /** + * Function which adds organizer to recipient list which is passed. + * This function also checks if it has organizer. + * + * @param array $messageProps message properties + * @param array $recipients recipients list of message. + * @param boolean $isException true if we are processing recipient of exception + */ + function addOrganizer($messageProps, &$recipients, $isException = false){ + + $hasOrganizer = false; + // Check if meeting already has an organizer. + foreach ($recipients as $key => $recipient){ + if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) { + $hasOrganizer = true; + } else if ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])){ + // Recipients for an occurrence + $recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse; + } + } + + if (!$hasOrganizer){ + // Create organizer. + $organizer = array(); + $organizer[PR_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID]; + $organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME]; + $organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS]; + $organizer[PR_RECIPIENT_TYPE] = MAPI_TO; + $organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME]; + $organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE])?'SMTP':$messageProps[PR_SENT_REPRESENTING_ADDRTYPE]; + $organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone; + $organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer; + + // Add organizer to recipients list. + array_unshift($recipients, $organizer); + } + } + } + + /* + + From http://www.ohelp-one.com/new-6765483-3268.html: + + Recurrence Data Structure Offset Type Value + + 0 ULONG (?) Constant : { 0x04, 0x30, 0x04, 0x30} + + 4 UCHAR 0x0A + recurrence type: 0x0A for daily, 0x0B for weekly, 0x0C for + monthly, 0x0D for yearly + + 5 UCHAR Constant: { 0x20} + + 6 ULONG Seems to be a variant of the recurrence type: 1 for daily every n + days, 2 for daily every weekday and weekly, 3 for monthly or yearly. The + special exception is regenerating tasks that regenerate on a weekly basis: 0 + is used in that case (I have no idea why). + + Here's the recurrence-type-specific data. Because the daily every N days + data are 4 bytes shorter than the data for the other types, the offsets for + the rest of the data will be 4 bytes off depending on the recurrence type. + + Daily every N days: + + 10 ULONG ( N - 1) * ( 24 * 60). I'm not sure what this is used for, but it's consistent. + + 14 ULONG N * 24 * 60: minutes between recurrences + + 18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for + regenerating tasks. + + Daily every weekday (this is essentially a subtype of weekly recurrence): + + 10 ULONG 6 * 24 * 60: minutes between recurrences ( a week... sort of) + + 14 ULONG 1: recur every week (corresponds to the second parameter for weekly + recurrence) + + 18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for + regenerating tasks. + + 22 ULONG 0x3E: bitmask for recurring every weekday (corresponds to fourth + parameter for weekly recurrence) + + Weekly every N weeks for all events and non-regenerating tasks: + + 10 ULONG 6 * 24 * 60: minutes between recurrences (a week... sort of) + + 14 ULONG N: recurrence interval + + 18 ULONG Constant: 0 + + 22 ULONG Bitmask for determining which days of the week the event recurs on + ( 1 << dayOfWeek, where Sunday is 0). + + Weekly every N weeks for regenerating tasks: 10 ULONG Constant: 0 + + 14 ULONG N * 7 * 24 * 60: recurrence interval in minutes between occurrences + + 18 ULONG Constant: 1 + + Monthly every N months on day D: + + 10 ULONG This is the most complicated value + in the entire mess. It's basically a very complicated way of stating the + recurrence interval. I tweaked fbs' basic algorithm. DateTime::MonthInDays + simply returns the number of days in a given month, e.g. 31 for July for 28 + for February (the algorithm doesn't take into account leap years, but it + doesn't seem to matter). My DateTime object, like Microsoft's COleDateTime, + uses 1-based months (i.e. January is 1, not 0). With that in mind, this + works: + + long monthIndex = ( ( ( ( 12 % schedule-=GetInterval()) * + + ( ( schedule-=GetStartDate().GetYear() - 1601) % + + schedule-=GetInterval())) % schedule-=GetInterval()) + + + ( schedule-=GetStartDate().GetMonth() - 1)) % schedule-=GetInterval(); + + for( int i = 0; i < monthIndex; i++) + + { + + value += DateTime::GetDaysInMonth( ( i % 12) + 1) * 24 * 60; + + } + + This should work for any recurrence interval, including those greater than + 12. + + 14 ULONG N: recurrence interval + + 18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for + regenerating tasks. + + 22 ULONG D: day of month the event recurs on (if this value is greater than + the number of days in a given month [e.g. 31 for and recurs in June], then + the event will recur on the last day of the month) + + Monthly every N months on the Xth Y (e.g. "2nd Tuesday"): + + 10 ULONG See above: same as for monthly every N months on day D + + 14 ULONG N: recurrence interval + + 18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for + regenerating tasks. + + 22 ULONG Y: bitmask for determining which day of the week the event recurs + on (see weekly every N weeks). Some useful values are 0x7F for any day, 0x3E + for a weekday, or 0x41 for a weekend day. + + 26 ULONG X: 1 for first occurrence, 2 for second, etc. 5 for last + occurrence. E.g. for "2nd Tuesday", you should have values of 0x04 for the + prior value and 2 for this one. + + Yearly on day D of month M: + + 10 ULONG M (sort of): This is another messy + value. It's the number of minute since the startning of the year to the + given month. For an explanation of GetDaysInMonth, see monthly every N + months. This will work: + + ULONG monthOfYearInMinutes = 0; + + for( int i = DateTime::cJanuary; i < schedule-=GetMonth(); i++) + + { + + monthOfYearInMinutes += DateTime::GetDaysInMonth( i) * 24 * 60; + + } + + + + 14 ULONG 12: recurrence interval in months. Naturally, 12. + + 18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for + regenerating tasks. + + 22 ULONG D: day of month the event recurs on. See monthly every N months on + day D. + + Yearly on the Xth Y of month M: 10 ULONG M (sort of): See yearly on day D of + month M. + + 14 ULONG 12: recurrence interval in months. Naturally, 12. + + 18 ULONG Constant: 0 + + 22 ULONG Y: see monthly every N months on the Xth Y. + + 26 ULONG X: see monthly every N months on the Xth Y. + + After these recurrence-type-specific values, the offsets will change + depending on the type. For every type except daily every N days, the offsets + will grow by at least 4. For those types using the Xth Y, the offsets will + grow by an additional 4, for a total of 8. The offsets for the rest of these + values will be given for the most basic case, daily every N days, i.e. + without any growth. Adjust as necessary. Also, the presence of exceptions + will change the offsets following the exception data by a variable number of + bytes, so the offsets given in the table are accurate only for those + recurrence patterns without any exceptions. + + + 22 UCHAR Type of pattern termination: 0x21 for terminating on a given date, 0x22 for terminating + after a given number of recurrences, or 0x23 for never terminating + (recurring infinitely) + + 23 UCHARx3 Constant: { 0x20, 0x00, 0x00} + + 26 ULONG Number of occurrences in pattern: 0 for infinite recurrence, + otherwise supply the value, even if it terminates on a given date, not after + a given number + + 30 ULONG Constant: 0 + + 34 ULONG Number of exceptions to pattern (i.e. deleted or changed + occurrences) + + .... ULONGxN Base date of each exception, given in hundreds of nanoseconds + since 1601, so see below to turn them into a comprehensible format. The base + date of an exception is the date (and only the date-- not the time) the + exception would have occurred on in the pattern. They must occur in + ascending order. + + 38 ULONG Number of changed exceptions (i.e. total number of exceptions - + number of deleted exceptions): if there are changed exceptions, again, more + data will be needed, but that will wait + + .... ULONGxN Start date (and only the date-- not the time) of each changed + exception, i.e. the exceptions which aren't deleted. These must also occur + in ascending order. If all of the exceptions are deleted, this data will be + absent. If present, they will be in the format above. Any dates that are in + the first list but not in the second are exceptions that have been deleted + (i.e. the difference between the two sets). Note that this is the start date + (including time), not the base date. Given that the values are unordered and + that they can't be matched up against the previous list in this iteration of + the recurrence data (they could in previous ones), it is very difficult to + tell which exceptions are deleted and which are changed. Fortunately, for + this new format, the base dates are given on the attachment representing the + changed exception (described below), so you can simply ignore this list of + changed exceptions. Just create a list of exceptions from the previous list + and assume they're all deleted unless you encounter an attachment with a + matching base date later on. + + 42 ULONG Start date of pattern given in hundreds of nanoseconds since 1601; + see below for an explanation. + + 46 ULONG End date of pattern: see start date of pattern + + 50 ULONG Constant: { 0x06, 0x30, 0x00, 0x00} + + NOTE: I find the following 8-byte sequence of bytes to be very useful for + orienting myself when looking at the raw data. If you can find { 0x06, 0x30, + 0x00, 0x00, 0x08, 0x30, 0x00, 0x00}, you can use these tables to work either + forwards or backwards to find the data you need. The sequence sort of + delineates certain critical exception-related data and delineates the + exceptions themselves from the rest of the data and is relatively easy to + find. If you're going to be meddling in here a lot, I suggest making a + friend of ol' 0x00003006. + + 54 UCHAR This number is some kind of version indicator. Use 0x08 for Outlook + 2003. I believe 0x06 is Outlook 2000 and possibly 98, while 0x07 is Outlook + XP. This number must be consistent with the features of the data structure + generated by the version of Outlook indicated thereby-- there are subtle + differences between the structures, and, if the version doesn't match the + data, Outlook will sometimes failto read the structure. + + 55 UCHARx3 Constant: { 0x30, 0x00, 0x00} + + 58 ULONG Start time of occurrence in minutes: e.g. 0 for midnight or 720 for + 12 PM + + 62 ULONG End time of occurrence in minutes: i.e. start time + duration, e.g. + 900 for an event that starts at 12 PM and ends at 3PM + + Exception Data 66 USHORT Number of changed exceptions: essentially a check + on the prior occurrence of this value; should be equivalent. + + NOTE: The following structure will occur N many times (where N = number of + changed exceptions), and each structure can be of variable length. + + .... ULONG Start date of changed exception given in hundreds of nanoseconds + since 1601 + + .... ULONG End date of changed exception given in hundreds of nanoseconds + since 1601 + + .... ULONG This is a value I don't clearly understand. It seems to be some + kind of archival value that matches the start time most of the time, but + will lag behind when the start time is changed and then match up again under + certain conditions later. In any case, setting to the same value as the + start time seems to work just fine (more information on this value would be + appreciated). + + .... USHORT Bitmask of changes to the exception (see below). This will be 0 + if the only changes to the exception were to its start or end time. + + .... ULONGxN Numeric values (e.g. label or minutes to remind before the + event) changed in the exception. These will occur in the order of their + corresponding bits (see below). If no numeric values were changed, then + these values will be absent. + + NOTE: The following three values constitute a single sub-structure that will + occur N many times, where N is the number of strings that are changed in the + exception. Since there are at most 2 string values that can be excepted + (i.e. subject [or description], and location), there can at most be two of + these, but there may be none. + + .... USHORT Length of changed string value with NULL character + + .... USHORT Length of changed string value without NULL character (i.e. + previous value - 1) + + .... CHARxN Changed string value (without NULL terminator) + + Unicode Data NOTE: If a string value was changed on an exception, those + changed string values will reappear here in Unicode format after 8 bytes of + NULL padding (possibly a Unicode terminator?). For each exception with a + changed string value, there will be an identifier, followed by the changed + strings in Unicode. The strings will occur in the order of their + corresponding bits (see below). E.g., if both subject and location were + changed in the exception, there would be the 3-ULONG identifier, then the + length of the subject, then the subject, then the length of the location, + then the location. + + 70 ULONGx2 Constant: { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}. This + padding serves as a barrier between the older data structure and the + appended Unicode data. This is the same sequence as the Unicode terminator, + but I'm not sure whether that's its identity or not. + + .... ULONGx3 These are the three times used to identify the exception above: + start date, end date, and repeated start date. These should be the same as + they were above. + + .... USHORT Length of changed string value without NULL character. This is + given as count of WCHARs, so it should be identical to the value above. + + .... WCHARxN Changed string value in Unicode (without NULL terminator) + + Terminator ... ULONGxN Constant: { 0x00, 0x00, 0x00, 0x00}. 4 bytes of NULL + padding per changed exception. If there were no changed exceptions, all + you'll need is the final terminator below. + + .... ULONGx2 Constant: { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}. + + */ +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapi/class.taskrecurrence.php b/sources/backend/zarafa/mapi/class.taskrecurrence.php new file mode 100644 index 0000000..4087216 --- /dev/null +++ b/sources/backend/zarafa/mapi/class.taskrecurrence.php @@ -0,0 +1,464 @@ +. + * + */ + + + require_once("backend/zarafa/mapi/class.baserecurrence.php"); + + class TaskRecurrence extends BaseRecurrence + { + /** + * Timezone info which is always false for task + */ + var $tz = false; + + function TaskRecurrence($store, $message) + { + $this->store = $store; + $this->message = $message; + + $properties = array(); + $properties["entryid"] = PR_ENTRYID; + $properties["parent_entryid"] = PR_PARENT_ENTRYID; + $properties["icon_index"] = PR_ICON_INDEX; + $properties["message_class"] = PR_MESSAGE_CLASS; + $properties["message_flags"] = PR_MESSAGE_FLAGS; + $properties["subject"] = PR_SUBJECT; + $properties["importance"] = PR_IMPORTANCE; + $properties["sensitivity"] = PR_SENSITIVITY; + $properties["last_modification_time"] = PR_LAST_MODIFICATION_TIME; + $properties["status"] = "PT_LONG:PSETID_Task:0x8101"; + $properties["percent_complete"] = "PT_DOUBLE:PSETID_Task:0x8102"; + $properties["startdate"] = "PT_SYSTIME:PSETID_Task:0x8104"; + $properties["duedate"] = "PT_SYSTIME:PSETID_Task:0x8105"; + $properties["reset_reminder"] = "PT_BOOLEAN:PSETID_Task:0x8107"; + $properties["dead_occurrence"] = "PT_BOOLEAN:PSETID_Task:0x8109"; + $properties["datecompleted"] = "PT_SYSTIME:PSETID_Task:0x810f"; + $properties["recurring_data"] = "PT_BINARY:PSETID_Task:0x8116"; + $properties["actualwork"] = "PT_LONG:PSETID_Task:0x8110"; + $properties["totalwork"] = "PT_LONG:PSETID_Task:0x8111"; + $properties["complete"] = "PT_BOOLEAN:PSETID_Task:0x811c"; + $properties["task_f_creator"] = "PT_BOOLEAN:PSETID_Task:0x811e"; + $properties["owner"] = "PT_STRING8:PSETID_Task:0x811f"; + $properties["recurring"] = "PT_BOOLEAN:PSETID_Task:0x8126"; + + $properties["reminder_minutes"] = "PT_LONG:PSETID_Common:0x8501"; + $properties["reminder_time"] = "PT_SYSTIME:PSETID_Common:0x8502"; + $properties["reminder"] = "PT_BOOLEAN:PSETID_Common:0x8503"; + + $properties["private"] = "PT_BOOLEAN:PSETID_Common:0x8506"; + $properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a"; + $properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586"; + $properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords"; + + $properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516"; + $properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517"; + $properties["commonassign"] = "PT_LONG:PSETID_Common:0x8518"; + $properties["flagdueby"] = "PT_SYSTIME:PSETID_Common:0x8560"; + $properties["side_effects"] = "PT_LONG:PSETID_Common:0x8510"; + $properties["reminder"] = "PT_BOOLEAN:PSETID_Common:0x8503"; + $properties["reminder_minutes"] = "PT_LONG:PSETID_Common:0x8501"; + + $this->proptags = getPropIdsFromStrings($store, $properties); + + parent::BaseRecurrence($store, $message, $properties); + } + + /** + * Function which saves recurrence and also regenerates task if necessary. + *@param array $recur new recurrence properties + *@return array of properties of regenerated task else false + */ + function setRecurrence(&$recur) + { + $this->recur = $recur; + $this->action =& $recur; + + if(!isset($this->recur["changed_occurences"])) + $this->recur["changed_occurences"] = Array(); + + if(!isset($this->recur["deleted_occurences"])) + $this->recur["deleted_occurences"] = Array(); + + if (!isset($this->recur['startocc'])) $this->recur['startocc'] = 0; + if (!isset($this->recur['endocc'])) $this->recur['endocc'] = 0; + + // Save recurrence because we need proper startrecurrdate and endrecurrdate + $this->saveRecurrence(); + + // Update $this->recur with proper startrecurrdate and endrecurrdate updated after saveing recurrence + $msgProps = mapi_getprops($this->message, array($this->proptags['recurring_data'])); + $recurring_data = $this->parseRecurrence($msgProps[$this->proptags['recurring_data']]); + foreach($recurring_data as $key => $value) { + $this->recur[$key] = $value; + } + + $this->setFirstOccurrence(); + + // Let's see if next occurrence has to be generated + return $this->moveToNextOccurrence(); + } + + /** + * Sets task object to first occurrence if startdate/duedate of task object is different from first occurrence + */ + function setFirstOccurrence() + { + // Check if it is already the first occurrence + if($this->action['start'] == $this->recur["start"]){ + return; + }else{ + $items = $this->getNextOccurrence(); + + $props = array(); + $props[$this->proptags['startdate']] = $items[$this->proptags['startdate']]; + $props[$this->proptags['commonstart']] = $items[$this->proptags['startdate']]; + + $props[$this->proptags['duedate']] = $items[$this->proptags['duedate']]; + $props[$this->proptags['commonend']] = $items[$this->proptags['duedate']]; + + mapi_setprops($this->message, $props); + } + } + + /** + * Function which creates new task as current occurrence and moves the + * existing task to next occurrence. + * + *@param array $recur $action from client + *@return boolean if moving to next occurrence succeed then it returns + * properties of either newly created task or existing task ELSE + * false because that was last occurrence + */ + function moveToNextOccurrence() + { + $result = false; + /** + * Every recurring task should have a 'duedate'. If a recurring task is created with no start/end date + * then we create first two occurrence separately and for first occurrence recurrence has ended. + */ + if ((empty($this->action['startdate']) && empty($this->action['duedate'])) + || ($this->action['complete'] == 1) || (isset($this->action['deleteOccurrence']) && $this->action['deleteOccurrence'])){ + + $nextOccurrence = $this->getNextOccurrence(); + $result = mapi_getprops($this->message, array(PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID)); + + $props = array(); + if ($nextOccurrence) { + if (!isset($this->action['deleteOccurrence'])) { + // Create current occurrence as separate task + $result = $this->regenerateTask($this->action['complete']); + } + + // Set reminder for next occurrence + $this->setReminder($nextOccurrence); + + // Update properties for next occurrence + $this->action['duedate'] = $props[$this->proptags['duedate']] = $nextOccurrence[$this->proptags['duedate']]; + $this->action['commonend'] = $props[$this->proptags['commonend']] = $nextOccurrence[$this->proptags['duedate']]; + + $this->action['startdate'] = $props[$this->proptags['startdate']] = $nextOccurrence[$this->proptags['startdate']]; + $this->action['commonstart'] = $props[$this->proptags['commonstart']] = $nextOccurrence[$this->proptags['startdate']]; + + // If current task as been mark as 'Complete' then next occurrence should be uncomplete. + if (isset($this->action['complete']) && $this->action['complete'] == 1) { + $this->action['status'] = $props[$this->proptags["status"]] = olTaskNotStarted; + $this->action['complete'] = $props[$this->proptags["complete"]] = false; + $this->action['percent_complete'] = $props[$this->proptags["percent_complete"]] = 0; + } + + $props[$this->proptags["dead_occurrence"]] = false; + } else { + if (isset($this->action['deleteOccurrence']) && $this->action['deleteOccurrence']) + return false; + + // Didn't get next occurrence, probably this is the last one, so recurrence ends here + $props[$this->proptags["dead_occurrence"]] = true; + $props[$this->proptags["datecompleted"]] = $this->action['datecompleted']; + $props[$this->proptags["task_f_creator"]] = true; + + //OL props + $props[$this->proptags["side_effects"]] = 1296; + $props[$this->proptags["icon_index"]] = 1280; + } + + mapi_setprops($this->message, $props); + } + + return $result; + } + + /** + * Function which return properties of next occurrence + *@return array startdate/enddate of next occurrence + */ + function getNextOccurrence() + { + if ($this->recur) { + $items = array(); + + //@TODO: fix start of range + $start = isset($this->messageprops[$this->proptags["duedate"]]) ? $this->messageprops[$this->proptags["duedate"]] : $this->action['start']; + $dayend = ($this->recur['term'] == 0x23) ? 0x7fffffff : $this->dayStartOf($this->recur["end"]); + + // Fix recur object + $this->recur['startocc'] = 0; + $this->recur['endocc'] = 0; + + // Retrieve next occurrence + $items = $this->getItems($start, $dayend, 1); + + return !empty($items) ? $items[0] : false; + } + } + + /** + * Function which clones current occurrence and sets appropriate properties. + * The original recurring item is moved to next occurrence. + *@param boolean $markComplete true if existing occurrence has to be mark complete else false. + */ + function regenerateTask($markComplete) + { + // Get all properties + $taskItemProps = mapi_getprops($this->message); + + if (isset($this->action["subject"])) $taskItemProps[$this->proptags["subject"]] = $this->action["subject"]; + if (isset($this->action["importance"])) $taskItemProps[$this->proptags["importance"]] = $this->action["importance"]; + if (isset($this->action["startdate"])) { + $taskItemProps[$this->proptags["startdate"]] = $this->action["startdate"]; + $taskItemProps[$this->proptags["commonstart"]] = $this->action["startdate"]; + } + if (isset($this->action["duedate"])) { + $taskItemProps[$this->proptags["duedate"]] = $this->action["duedate"]; + $taskItemProps[$this->proptags["commonend"]] = $this->action["duedate"]; + } + + $folder = mapi_msgstore_openentry($this->store, $taskItemProps[PR_PARENT_ENTRYID]); + $newMessage = mapi_folder_createmessage($folder); + + $taskItemProps[$this->proptags["status"]] = $markComplete ? olTaskComplete : olTaskNotStarted; + $taskItemProps[$this->proptags["complete"]] = $markComplete; + $taskItemProps[$this->proptags["percent_complete"]] = $markComplete ? 1 : 0; + + // This occurrence has been marked as 'Complete' so disable reminder + if ($markComplete) { + $taskItemProps[$this->proptags["reset_reminder"]] = false; + $taskItemProps[$this->proptags["reminder"]] = false; + $taskItemProps[$this->proptags["datecompleted"]] = $this->action["datecompleted"]; + + unset($this->action[$this->proptags['datecompleted']]); + } + + // Recurrence ends for this item + $taskItemProps[$this->proptags["dead_occurrence"]] = true; + $taskItemProps[$this->proptags["task_f_creator"]] = true; + + //OL props + $taskItemProps[$this->proptags["side_effects"]] = 1296; + $taskItemProps[$this->proptags["icon_index"]] = 1280; + + // Copy recipients + $recipienttable = mapi_message_getrecipienttable($this->message); + $recipients = mapi_table_queryallrows($recipienttable, array(PR_ENTRYID, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_RECIPIENT_ENTRYID, PR_RECIPIENT_TYPE, PR_SEND_INTERNET_ENCODING, PR_SEND_RICH_INFO, PR_RECIPIENT_DISPLAY_NAME, PR_ADDRTYPE, PR_DISPLAY_TYPE, PR_RECIPIENT_TRACKSTATUS, PR_RECIPIENT_TRACKSTATUS_TIME, PR_RECIPIENT_FLAGS, PR_ROWID)); + + $copy_to_recipientTable = mapi_message_getrecipienttable($newMessage); + $copy_to_recipientRows = mapi_table_queryallrows($copy_to_recipientTable, array(PR_ROWID)); + foreach($copy_to_recipientRows as $recipient) { + mapi_message_modifyrecipients($newMessage, MODRECIP_REMOVE, array($recipient)); + } + mapi_message_modifyrecipients($newMessage, MODRECIP_ADD, $recipients); + + // Copy attachments + $attachmentTable = mapi_message_getattachmenttable($this->message); + if($attachmentTable) { + $attachments = mapi_table_queryallrows($attachmentTable, array(PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD)); + + foreach($attachments as $attach_props){ + $attach_old = mapi_message_openattach($this->message, (int) $attach_props[PR_ATTACH_NUM]); + $attach_newResourceMsg = mapi_message_createattach($newMessage); + + mapi_copyto($attach_old, array(), array(), $attach_newResourceMsg, 0); + mapi_savechanges($attach_newResourceMsg); + } + } + + mapi_setprops($newMessage, $taskItemProps); + mapi_savechanges($newMessage); + + // Update body of original message + $msgbody = mapi_message_openproperty($this->message, PR_BODY); + $msgbody = trim($this->windows1252_to_utf8($msgbody), "\0"); + $separator = "------------\r\n"; + + if (!empty($msgbody) && strrpos($msgbody, $separator) === false) { + $msgbody = $separator . $msgbody; + $stream = mapi_openpropertytostream($this->message, PR_BODY, MAPI_CREATE | MAPI_MODIFY); + mapi_stream_setsize($stream, strlen($msgbody)); + mapi_stream_write($stream, $msgbody); + mapi_stream_commit($stream); + } + + // We need these properties to notify client + return mapi_getprops($newMessage, array(PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID)); + } + + /** + * processOccurrenceItem, adds an item to a list of occurrences, but only if the + * resulting occurrence starts or ends in the interval <$start, $end> + * @param array $items reference to the array to be added to + * @param date $start start of timeframe in GMT TIME + * @param date $end end of timeframe in GMT TIME + * @param date $basedate (hour/sec/min assumed to be 00:00:00) in LOCAL TIME OF THE OCCURRENCE + */ + function processOccurrenceItem(&$items, $start, $end, $now) + { + if ($now > $start) { + $newItem = array(); + $newItem[$this->proptags['startdate']] = $now; + + // If startdate and enddate are set on task, then slide enddate according to duration + if (isset($this->messageprops[$this->proptags["startdate"]]) && isset($this->messageprops[$this->proptags["duedate"]])) { + $newItem[$this->proptags['duedate']] = $newItem[$this->proptags['startdate']] + ($this->messageprops[$this->proptags["duedate"]] - $this->messageprops[$this->proptags["startdate"]]); + } else { + $newItem[$this->proptags['duedate']] = $newItem[$this->proptags['startdate']]; + } + + $items[] = $newItem; + } + } + + /** + * Function which marks existing occurrence to 'Complete' + *@param array $recur array action from client + *@return array of properties of regenerated task else false + */ + function markOccurrenceComplete(&$recur) + { + // Fix timezone object + $this->tz = false; + $this->action =& $recur; + $dead_occurrence = isset($this->messageprops[$this->proptags['dead_occurrence']]) ? $this->messageprops[$this->proptags['dead_occurrence']] : false; + + if (!$dead_occurrence) { + return $this->moveToNextOccurrence(); + } + + return false; + } + + /** + * Function which sets reminder on recurring task after existing occurrence has been deleted or marked complete. + *@param array $nextOccurrence properties of next occurrence + */ + function setReminder($nextOccurrence) + { + $props = array(); + if ($nextOccurrence) { + // Check if reminder is reset. Default is 'false' + $reset_reminder = isset($this->messageprops[$this->proptags['reset_reminder']]) ? $this->messageprops[$this->proptags['reset_reminder']] : false; + $reminder = $this->messageprops[$this->proptags['reminder']]; + + // Either reminder was already set OR reminder was set but was dismissed bty user + if ($reminder || $reset_reminder) { + // Reminder can be set at any time either before or after the duedate, so get duration between the reminder time and duedate + $reminder_time = isset($this->messageprops[$this->proptags['reminder_time']]) ? $this->messageprops[$this->proptags['reminder_time']] : 0; + $reminder_difference = isset($this->messageprops[$this->proptags['duedate']]) ? $this->messageprops[$this->proptags['duedate']] : 0; + $reminder_difference = $reminder_difference - $reminder_time; + + // Apply duration to next calculated duedate + $next_reminder_time = $nextOccurrence[$this->proptags['duedate']] - $reminder_difference; + + $props[$this->proptags['reminder_time']] = $next_reminder_time; + $props[$this->proptags['flagdueby']] = $next_reminder_time; + $this->action['reminder'] = $props[$this->proptags['reminder']] = true; + } + } else { + // Didn't get next occurrence, probably this is the last occurrence + $props[$this->proptags['reminder']] = false; + $props[$this->proptags['reset_reminder']] = false; + } + + if (!empty($props)) + mapi_setprops($this->message, $props); + } + + /** + * Function which recurring task to next occurrence. + * It simply doesn't regenerate task + @param array $action + */ + function deleteOccurrence($action) + { + $this->tz = false; + $this->action = $action; + $result = $this->moveToNextOccurrence(); + + mapi_savechanges($this->message); + + return $result; + } + + /** + * Convert from windows-1252 encoded string to UTF-8 string + * + * The same conversion rules as utf8_to_windows1252 apply. + * + * @param string $string the Windows-1252 string to convert + * @return string UTF-8 representation of the string + */ + function windows1252_to_utf8($string) + { + if (function_exists("iconv")){ + return iconv("Windows-1252", "UTF-8//TRANSLIT", $string); + }else{ + return utf8_encode($string); // no euro support here + } + } + } +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapi/class.taskrequest.php b/sources/backend/zarafa/mapi/class.taskrequest.php new file mode 100644 index 0000000..779ad50 --- /dev/null +++ b/sources/backend/zarafa/mapi/class.taskrequest.php @@ -0,0 +1,1036 @@ +. + * + */ + + + /* + * In general + * + * This class never actually modifies a task item unless we receive a task request update. This means + * that setting all the properties to make the task item itself behave like a task request is up to the + * caller. + * + * The only exception to this is the generation of the TaskGlobalObjId, the unique identifier identifying + * this task request to both the organizer and the assignee. The globalobjectid is generated when the + * task request is sent via sendTaskRequest. + */ + + /* The TaskMode value is only used for the IPM.TaskRequest items. It must 0 (tdmtNothing) on IPM.Task items. + * + * It is used to indicate the type of change that is being carried in the IPM.TaskRequest item (although this + * information seems redundant due to that information already being available in PR_MESSAGE_CLASS). + */ + define('tdmtNothing', 0); // Value in IPM.Task items + define('tdmtTaskReq', 1); // Assigner -> Assignee + define('tdmtTaskAcc', 2); // Assignee -> Assigner + define('tdmtTaskDec', 3); // Assignee -> Assigner + define('tdmtTaskUpd', 4); // Assignee -> Assigner + define('tdmtTaskSELF', 5); // Assigner -> Assigner (?) + + /* The TaskHistory is used to show the last action on the task on both the assigner and the assignee's side. + * + * It is used in combination with 'AssignedTime' and 'tasklastdelegate' or 'tasklastuser' to show the information + * at the top of the task request in the format 'Accepted by on 01-01-2010 11:00'. + */ + define('thNone', 0); + define('thAccepted', 1); // Set by assignee + define('thDeclined', 2); // Set by assignee + define('thUpdated', 3); // Set by assignee + define('thDueDateChanged', 4); + define('thAssigned', 5); // Set by assigner + + /* The TaskState value is used to differentiate the version of a task in the assigner's folder and the version in the + * assignee's folder. The buttons shown depend on this and the 'taskaccepted' boolean (for the assignee) + */ + define('tdsNOM', 0); // Got a response to a deleted task, and re-created the task for the assigner + define('tdsOWNNEW', 1); // Not assigned + define('tdsOWN', 2); // Assignee version + define('tdsACC', 3); // Assigner version + define('tdsDEC', 4); // Assigner version, but assignee declined + + /* The delegationstate is used for the assigner to indicate state + */ + define('olTaskNotDelegated', 0); + define('olTaskDelegationUnknown', 1); // After sending req + define('olTaskDelegationAccepted', 2); // After receiving accept + define('olTaskDelegationDeclined', 3); // After receiving decline + + /* The task ownership indicates the role of the current user relative to the task. + */ + define('olNewTask', 0); + define('olDelegatedTask', 1); // Task has been assigned + define('olOwnTask', 2); // Task owned + + /* taskmultrecips indicates whether the task request sent or received has multiple assignees or not. + */ + define('tmrNone', 0); + define('tmrSent', 1); // Task has been sent to multiple assignee + define('tmrReceived', 2); // Task Request received has multiple assignee + + class TaskRequest { + + // All recipient properties + var $recipprops = Array(PR_ENTRYID, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_RECIPIENT_ENTRYID, PR_RECIPIENT_TYPE, PR_SEND_INTERNET_ENCODING, PR_SEND_RICH_INFO, PR_RECIPIENT_DISPLAY_NAME, PR_ADDRTYPE, PR_DISPLAY_TYPE, PR_RECIPIENT_TRACKSTATUS, PR_RECIPIENT_TRACKSTATUS_TIME, PR_RECIPIENT_FLAGS, PR_ROWID, PR_SEARCH_KEY); + + /* Constructor + * + * Constructs a TaskRequest object for the specified message. This can be either the task request + * message itself (in the inbox) or the task in the tasks folder, depending on the action to be performed. + * + * As a general rule, the object message passed is the object 'in view' when the user performs one of the + * actions in this class. + * + * @param $store store MAPI Store in which $message resides. This is also the store where the tasks folder is assumed to be in + * @param $message message MAPI Message to which the task request referes (can be an email or a task) + * @param $session session MAPI Session which is used to open tasks folders for delegated task requests or responses + */ + function TaskRequest($store, $message, $session) { + $this->store = $store; + $this->message = $message; + $this->session = $session; + + $properties["owner"] = "PT_STRING8:PSETID_Task:0x811f"; + $properties["updatecount"] = "PT_LONG:PSETID_Task:0x8112"; + $properties["taskstate"] = "PT_LONG:PSETID_Task:0x8113"; + $properties["taskmultrecips"] = "PT_LONG:PSETID_Task:0x8120"; + $properties["taskupdates"] = "PT_BOOLEAN:PSETID_Task:0x811b"; + $properties["tasksoc"] = "PT_BOOLEAN:PSETID_Task:0x8119"; + $properties["taskhistory"] = "PT_LONG:PSETID_Task:0x811a"; + $properties["taskmode"] = "PT_LONG:PSETID_Common:0x8518"; + $properties["taskglobalobjid"] = "PT_BINARY:PSETID_Common:0x8519"; + $properties["complete"] = "PT_BOOLEAN:PSETID_Common:0x811c"; + $properties["assignedtime"] = "PT_SYSTIME:PSETID_Task:0x8115"; + $properties["taskfcreator"] = "PT_BOOLEAN:PSETID_Task:0x0x811e"; + $properties["tasklastuser"] = "PT_STRING8:PSETID_Task:0x8122"; + $properties["tasklastdelegate"] = "PT_STRING8:PSETID_Task:0x8125"; + $properties["taskaccepted"] = "PT_BOOLEAN:PSETID_Task:0x8108"; + $properties["delegationstate"] = "PT_LONG:PSETID_Task:0x812a"; + $properties["ownership"] = "PT_LONG:PSETID_Task:0x8129"; + + $properties["complete"] = "PT_BOOLEAN:PSETID_Task:0x811c"; + $properties["datecompleted"] = "PT_SYSTIME:PSETID_Task:0x810f"; + $properties["recurring"] = "PT_BOOLEAN:PSETID_Task:0x8126"; + $properties["startdate"] = "PT_SYSTIME:PSETID_Task:0x8104"; + $properties["duedate"] = "PT_SYSTIME:PSETID_Task:0x8105"; + $properties["status"] = "PT_LONG:PSETID_Task:0x8101"; + $properties["percent_complete"] = "PT_DOUBLE:PSETID_Task:0x8102"; + $properties["totalwork"] = "PT_LONG:PSETID_Task:0x8111"; + $properties["actualwork"] = "PT_LONG:PSETID_Task:0x8110"; + $properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords"; + $properties["companies"] = "PT_MV_STRING8:PSETID_Common:0x8539"; + $properties["mileage"] = "PT_STRING8:PSETID_Common:0x8534"; + $properties["billinginformation"] = "PT_STRING8:PSETID_Common:0x8535"; + + $this->props = getPropIdsFromStrings($store, $properties); + } + + // General functions + + /* Return TRUE if the item is a task request message + */ + function isTaskRequest() + { + $props = mapi_getprops($this->message, Array(PR_MESSAGE_CLASS)); + + if(isset($props[PR_MESSAGE_CLASS]) && $props[PR_MESSAGE_CLASS] == "IPM.TaskRequest") { + return true; + } + } + + /* Return TRUE if the item is a task response message + */ + function isTaskRequestResponse() { + $props = mapi_getprops($this->message, Array(PR_MESSAGE_CLASS)); + + if(isset($props[PR_MESSAGE_CLASS]) && strpos($props[PR_MESSAGE_CLASS], "IPM.TaskRequest.") === 0) { + return true; + } + } + + /* + * Gets the task associated with an IPM.TaskRequest message + * + * If the task does not exist yet, it is created, using the attachment object in the + * task request item. + */ + function getAssociatedTask($create) + { + $props = mapi_getprops($this->message, array(PR_MESSAGE_CLASS, $this->props['taskglobalobjid'])); + + if($props[PR_MESSAGE_CLASS] == "IPM.Task") + return $this->message; // Message itself is task, so return that + + $tfolder = $this->getDefaultTasksFolder(); + $globalobjid = $props[$this->props['taskglobalobjid']]; + + // Find the task by looking for the taskglobalobjid + $restriction = array(RES_PROPERTY, array(RELOP => RELOP_EQ, ULPROPTAG => $this->props['taskglobalobjid'], VALUE => $globalobjid)); + + $contents = mapi_folder_getcontentstable($tfolder); + + $rows = mapi_table_queryallrows($contents, array(PR_ENTRYID), $restriction); + + if(empty($rows)) { + // None found, create one if possible + if(!$create) + return false; + + $task = mapi_folder_createmessage($tfolder); + + $sub = $this->getEmbeddedTask($this->message); + mapi_copyto($sub, array(), array(), $task); + + // Copy sender information from the e-mail + $senderprops = mapi_getprops($this->message, array(PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_SEARCH_KEY)); + mapi_setprops($task, $senderprops); + + $senderprops = mapi_getprops($this->message, array(PR_SENDER_NAME, PR_SENDER_EMAIL_ADDRESS, PR_SENDER_ENTRYID, PR_SENDER_ADDRTYPE, PR_SENDER_SEARCH_KEY)); + mapi_setprops($task, $senderprops); + + } else { + // If there are multiple, just use the first + $entryid = $rows[0][PR_ENTRYID]; + + $store = $this->getTaskFolderStore(); + $task = mapi_msgstore_openentry($store, $entryid); + } + + return $task; + } + + + + // Organizer functions (called by the organizer) + + /* Processes a task request response, which can be any of the following: + * - Task accept (task history is marked as accepted) + * - Task decline (task history is marked as declined) + * - Task update (updates completion %, etc) + */ + function processTaskResponse() { + $messageprops = mapi_getprops($this->message, array(PR_PROCESSED)); + if(isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED]) + return true; + + // Get the task for this response + $task = $this->getAssociatedTask(false); + + if(!$task) { + // Got a response for a task that has been deleted, create a new one and mark it as such + $task = $this->getAssociatedTask(true); + + // tdsNOM indicates a task request that had gone missing + mapi_setprops($task, array($this->props['taskstate'] => tdsNOM )); + } + + // Get the embedded task information and copy it into our task + $sub = $this->getEmbeddedTask($this->message); + mapi_copyto($sub, array(), array($this->props['taskstate'], $this->props['taskhistory'], $this->props['taskmode'], $this->props['taskfcreator']), $task); + + $props = mapi_getprops($this->message, array(PR_MESSAGE_CLASS)); + + // Set correct taskmode and taskhistory depending on response type + switch($props[PR_MESSAGE_CLASS]) { + case 'IPM.TaskRequest.Accept': + $taskhistory = thAccepted; + $taskstate = tdsACC; + $delegationstate = olTaskDelegationAccepted; + break; + case 'IPM.TaskRequest.Decline': + $taskhistory = thDeclined; + $taskstate = tdsDEC; + $delegationstate = olTaskDelegationDeclined; + break; + case 'IPM.TaskRequest.Update': + $taskhistory = thUpdated; + $taskstate = tdsACC; // Doesn't actually change anything + $delegationstate = olTaskDelegationAccepted; + break; + } + + // Update taskstate (what the task looks like) and task history (last action done by the assignee) + mapi_setprops($task, array($this->props['taskhistory'] => $taskhistory, $this->props['taskstate'] => $taskstate, $this->props['delegationstate'] => $delegationstate, $this->props['ownership'] => olDelegatedTask)); + + mapi_setprops($this->message, array(PR_PROCESSED => true)); + mapi_savechanges($task); + + return true; + } + + /* Create a new message in the current user's outbox and submit it + * + * Takes the task passed in the constructor as the task to be sent; recipient should + * be pre-existing. The task request will be sent to all recipients. + */ + function sendTaskRequest($prefix) { + // Generate a TaskGlobalObjectId + $taskid = $this->createTGOID(); + $messageprops = mapi_getprops($this->message, array(PR_SUBJECT)); + + // Set properties on Task Request + mapi_setprops($this->message, array( + $this->props['taskglobalobjid'] => $taskid, /* our new taskglobalobjid */ + $this->props['taskstate'] => tdsACC, /* state for our outgoing request */ + $this->props['taskmode'] => tdmtNothing, /* we're not sending a change */ + $this->props['updatecount'] => 2, /* version 2 (no idea) */ + $this->props['delegationstate'] => olTaskDelegationUnknown, /* no reply yet */ + $this->props['ownership'] => olDelegatedTask, /* Task has been assigned */ + $this->props['taskhistory'] => thAssigned, /* Task has been assigned */ + PR_ICON_INDEX => 1283 /* Task request icon*/ + )); + $this->setLastUser(); + $this->setOwnerForAssignor(); + mapi_savechanges($this->message); + + // Create outgoing task request message + $outgoing = $this->createOutgoingMessage(); + // No need to copy attachments as task will be attached as embedded message. + mapi_copyto($this->message, array(), array(PR_MESSAGE_ATTACHMENTS), $outgoing); + + // Make it a task request, and put it in sent items after it is sent + mapi_setprops($outgoing, array( + PR_MESSAGE_CLASS => "IPM.TaskRequest", /* class is task request */ + $this->props['taskstate'] => tdsOWNNEW, /* for the recipient the task is new */ + $this->props['taskmode'] => tdmtTaskReq, /* for the recipient it's a request */ + $this->props['updatecount'] => 1, /* version 2 is in the attachment */ + PR_SUBJECT => $prefix . $messageprops[PR_SUBJECT], + PR_ICON_INDEX => 0xFFFFFFFF, /* show assigned icon */ + )); + + // Set Body + $body = $this->getBody(); + $stream = mapi_openpropertytostream($outgoing, PR_BODY, MAPI_CREATE | MAPI_MODIFY); + mapi_stream_setsize($stream, strlen($body)); + mapi_stream_write($stream, $body); + mapi_stream_commit($stream); + + $attach = mapi_message_createattach($outgoing); + mapi_setprops($attach, array(PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG, PR_DISPLAY_NAME => $messageprops[PR_SUBJECT])); + + $sub = mapi_attach_openproperty($attach, PR_ATTACH_DATA_OBJ, IID_IMessage, 0, MAPI_MODIFY | MAPI_CREATE); + + mapi_copyto($this->message, array(), array(), $sub); + mapi_savechanges($sub); + + mapi_savechanges($attach); + + mapi_savechanges($outgoing); + mapi_message_submitmessage($outgoing); + return true; + } + + // Assignee functions (called by the assignee) + + /* Update task version counter + * + * Must be called before each update to increase counter + */ + function updateTaskRequest() { + $messageprops = mapi_getprops($this->message, array($this->props['updatecount'])); + + if(isset($messageprops)) { + $messageprops[$this->props['updatecount']]++; + } else { + $messageprops[$this->props['updatecount']] = 1; + } + + mapi_setprops($this->message, $messageprops); + } + + /* Process a task request + * + * Message passed should be an IPM.TaskRequest message. The task request is then processed to create + * the task in the tasks folder if needed. + */ + function processTaskRequest() { + if(!$this->isTaskRequest()) + return false; + + $messageprops = mapi_getprops($this->message, array(PR_PROCESSED)); + if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED]) + return true; + + $task = $this->getAssociatedTask(true); + $taskProps = mapi_getprops($task, array($this->props['taskmultrecips'])); + + // Set the task state to say that we're the attendee receiving the message, that we have not yet responded and that this message represents no change + $taskProps[$this->props["taskstate"]] = tdsOWN; + $taskProps[$this->props["taskhistory"]] = thAssigned; + $taskProps[$this->props["taskmode"]] = tdmtNothing; + $taskProps[$this->props["taskaccepted"]] = false; + $taskProps[$this->props["taskfcreator"]] = false; + $taskProps[$this->props["ownership"]] = olOwnTask; + $taskProps[$this->props["delegationstate"]] = olTaskNotDelegated; + $taskProps[PR_ICON_INDEX] = 1282; + + // This task was assigned to multiple recips, so set this user as owner + if (isset($taskProps[$this->props['taskmultrecips']]) && $taskProps[$this->props['taskmultrecips']] == tmrSent) { + $loginUserData = $this->retrieveUserData(); + + if ($loginUserData) { + $taskProps[$this->props['owner']] = $loginUserData[PR_DISPLAY_NAME]; + $taskProps[$this->props['taskmultrecips']] = tmrReceived; + } + } + mapi_setprops($task, $taskProps); + + $this->setAssignorInRecipients($task); + + mapi_savechanges($task); + + $taskprops = mapi_getprops($task, array(PR_ENTRYID)); + + mapi_setprops($this->message, array(PR_PROCESSED => true)); + mapi_savechanges($this->message); + + return $taskprops[PR_ENTRYID]; + } + + /* Accept a task request and send the response. + * + * Message passed should be an IPM.Task (eg the task from getAssociatedTask()) + * + * Copies the task to the user's task folder, sets it to accepted, and sends the acceptation + * message back to the organizer. The caller is responsible for removing the message. + * + * @return entryid EntryID of the accepted task + */ + function doAccept($prefix) { + $messageprops = mapi_getprops($this->message, array($this->props['taskstate'])); + + if(!isset($messageprops[$this->props['taskstate']]) || $messageprops[$this->props['taskstate']] != tdsOWN) + return false; // Can only accept assignee task + + $this->setLastUser(); + $this->updateTaskRequest(); + + // Set as accepted + mapi_setprops($this->message, array($this->props['taskhistory'] => thAccepted, $this->props['assignedtime'] => time(), $this->props['taskaccepted'] => true, $this->props['delegationstate'] => olTaskNotDelegated)); + + mapi_savechanges($this->message); + + $this->sendResponse(tdmtTaskAcc, $prefix); + + //@TODO: delete received task request from Inbox + return $this->deleteReceivedTR(); + } + + /* Decline a task request and send the response. + * + * Passed message must be a task request message, ie isTaskRequest() must return TRUE. + * + * Sends the decline message back to the organizer. The caller is responsible for removing the message. + * + * @return boolean TRUE on success, FALSE on failure + */ + function doDecline($prefix) { + $messageprops = mapi_getprops($this->message, array($this->props['taskstate'])); + + if(!isset($messageprops[$this->props['taskstate']]) || $messageprops[$this->props['taskstate']] != tdsOWN) + return false; // Can only decline assignee task + + $this->setLastUser(); + $this->updateTaskRequest(); + + // Set as declined + mapi_setprops($this->message, array($this->props['taskhistory'] => thDeclined, $this->props['delegationstate'] => olTaskDelegationDeclined)); + + mapi_savechanges($this->message); + + $this->sendResponse(tdmtTaskDec, $prefix); + + return $this->deleteReceivedTR(); + } + + /* Send an update of the task if requested, and send the Status-On-Completion report if complete and requested + * + * If no updates were requested from the organizer, this function does nothing. + * + * @return boolean TRUE if the update succeeded, FALSE otherwise. + */ + function doUpdate($prefix, $prefixComplete) { + $messageprops = mapi_getprops($this->message, array($this->props['taskstate'], PR_SUBJECT)); + + if(!isset($messageprops[$this->props['taskstate']]) || $messageprops[$this->props['taskstate']] != tdsOWN) + return false; // Can only update assignee task + + $this->setLastUser(); + $this->updateTaskRequest(); + + // Set as updated + mapi_setprops($this->message, array($this->props['taskhistory'] => thUpdated)); + + mapi_savechanges($this->message); + + $props = mapi_getprops($this->message, array($this->props['taskupdates'], $this->props['tasksoc'], $this->props['recurring'], $this->props['complete'])); + if ($props[$this->props['taskupdates']] && !(isset($props[$this->props['recurring']]) && $props[$this->props['recurring']])) + $this->sendResponse(tdmtTaskUpd, $prefix); + + if($props[$this->props['tasksoc']] && $props[$this->props['complete']] ) { + $outgoing = $this->createOutgoingMessage(); + + mapi_setprops($outgoing, array(PR_SUBJECT => $prefixComplete . $messageprops[PR_SUBJECT])); + + $this->setRecipientsForResponse($outgoing, tdmtTaskUpd, true); + $body = $this->getBody(); + $stream = mapi_openpropertytostream($outgoing, PR_BODY, MAPI_CREATE | MAPI_MODIFY); + mapi_stream_setsize($stream, strlen($body)); + mapi_stream_write($stream, $body); + mapi_stream_commit($stream); + + mapi_savechanges($outgoing); + mapi_message_submitmessage($outgoing); + } + } + + // Internal functions + + /* Get the store associated with the task + * + * Normally this will just open the store that the processed message is in. However, if the message is opened + * by a delegate, this function opens the store that the message was delegated from. + */ + function getTaskFolderStore() + { + $ownerentryid = false; + + $rcvdprops = mapi_getprops($this->message, array(PR_RCVD_REPRESENTING_ENTRYID)); + if(isset($rcvdprops[PR_RCVD_REPRESENTING_ENTRYID])) { + $ownerentryid = $rcvdprops; + } + + if(!$ownerentryid) { + $store = $this->store; + } else { + $ab = mapi_openaddressbook($this->session); // seb changed from $session to $this->session + if(!$ab) return false; // manni $ before ab was missing + + $mailuser = mapi_ab_openentry($ab, $ownerentryid); + if(!$mailuser) return false; + + $mailuserprops = mapi_getprops($mailuser, array(PR_EMAIL_ADDRESS)); + if(!isset($mailuserprops[PR_EMAIL_ADDRESS])) return false; + + $storeid = mapi_msgstore_createentryid($this->store, $mailuserprops[PR_EMAIL_ADDRESS]); + + $store = mapi_openmsgstore($this->session, $storeid); + + } + return $store; + } + + /* Open the default task folder for the current user, or the specified user if passed + * + * @param $ownerentryid (Optional)EntryID of user for which we are opening the task folder + */ + function getDefaultTasksFolder() + { + $store = $this->getTaskFolderStore(); + + $inbox = mapi_msgstore_getreceivefolder($store); + $inboxprops = mapi_getprops($inbox, Array(PR_IPM_TASK_ENTRYID)); + if(!isset($inboxprops[PR_IPM_TASK_ENTRYID])) + return false; + + return mapi_msgstore_openentry($store, $inboxprops[PR_IPM_TASK_ENTRYID]); + } + + function getSentReprProps($store) + { + $storeprops = mapi_getprops($store, array(PR_MAILBOX_OWNER_ENTRYID)); + if(!isset($storeprops[PR_MAILBOX_OWNER_ENTRYID])) return false; + + $ab = mapi_openaddressbook($this->session); + $mailuser = mapi_ab_openentry($ab, $storeprops[PR_MAILBOX_OWNER_ENTRYID]); + $mailuserprops = mapi_getprops($mailuser, array(PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_DISPLAY_NAME, PR_SEARCH_KEY, PR_ENTRYID)); + + $props = array(); + $props[PR_SENT_REPRESENTING_ADDRTYPE] = $mailuserprops[PR_ADDRTYPE]; + $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $mailuserprops[PR_EMAIL_ADDRESS]; + $props[PR_SENT_REPRESENTING_NAME] = $mailuserprops[PR_DISPLAY_NAME]; + $props[PR_SENT_REPRESENTING_SEARCH_KEY] = $mailuserprops[PR_SEARCH_KEY]; + $props[PR_SENT_REPRESENTING_ENTRYID] = $mailuserprops[PR_ENTRYID]; + + return $props; + } + + /* + * Creates an outgoing message based on the passed message - will set delegate information + * and sentmail folder + */ + function createOutgoingMessage() + { + // Open our default store for this user (that's the only store we can submit in) + $store = $this->getDefaultStore(); + $storeprops = mapi_getprops($store, array(PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID)); + + $outbox = mapi_msgstore_openentry($store, $storeprops[PR_IPM_OUTBOX_ENTRYID]); + if(!$outbox) return false; + + $outgoing = mapi_folder_createmessage($outbox); + if(!$outgoing) return false; + + // Set SENT_REPRESENTING in case we're sending as a delegate + $ownerstore = $this->getTaskFolderStore(); + $sentreprprops = $this->getSentReprProps($ownerstore); + mapi_setprops($outgoing, $sentreprprops); + + mapi_setprops($outgoing, array(PR_SENTMAIL_ENTRYID => $storeprops[PR_IPM_SENTMAIL_ENTRYID])); + + return $outgoing; + } + + /* + * Send a response message (from assignee back to organizer). + * + * @param $type int Type of response (tdmtTaskAcc, tdmtTaskDec, tdmtTaskUpd); + * @return boolean TRUE on success + */ + function sendResponse($type, $prefix) + { + // Create a message in our outbox + $outgoing = $this->createOutgoingMessage(); + + $messageprops = mapi_getprops($this->message, array(PR_SUBJECT)); + + $attach = mapi_message_createattach($outgoing); + mapi_setprops($attach, array(PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG, PR_DISPLAY_NAME => $messageprops[PR_SUBJECT], PR_ATTACHMENT_HIDDEN => true)); + $sub = mapi_attach_openproperty($attach, PR_ATTACH_DATA_OBJ, IID_IMessage, 0, MAPI_CREATE | MAPI_MODIFY); + + mapi_copyto($this->message, array(), array(PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_SEARCH_KEY), $outgoing); + mapi_copyto($this->message, array(), array(), $sub); + + if (!$this->setRecipientsForResponse($outgoing, $type)) return false; + + switch($type) { + case tdmtTaskAcc: + $messageclass = "IPM.TaskRequest.Accept"; + break; + case tdmtTaskDec: + $messageclass = "IPM.TaskRequest.Decline"; + break; + case tdmtTaskUpd: + $messageclass = "IPM.TaskRequest.Update"; + break; + }; + + mapi_savechanges($sub); + mapi_savechanges($attach); + + // Set Body + $body = $this->getBody(); + $stream = mapi_openpropertytostream($outgoing, PR_BODY, MAPI_CREATE | MAPI_MODIFY); + mapi_stream_setsize($stream, strlen($body)); + mapi_stream_write($stream, $body); + mapi_stream_commit($stream); + + // Set subject, taskmode, message class, icon index, response time + mapi_setprops($outgoing, array(PR_SUBJECT => $prefix . $messageprops[PR_SUBJECT], + $this->props['taskmode'] => $type, + PR_MESSAGE_CLASS => $messageclass, + PR_ICON_INDEX => 0xFFFFFFFF, + $this->props['assignedtime'] => time())); + + mapi_savechanges($outgoing); + mapi_message_submitmessage($outgoing); + + return true; + } + + function getDefaultStore() + { + $table = mapi_getmsgstorestable($this->session); + $rows = mapi_table_queryallrows($table, array(PR_DEFAULT_STORE, PR_ENTRYID)); + + foreach($rows as $row) { + if($row[PR_DEFAULT_STORE]) + return mapi_openmsgstore($this->session, $row[PR_ENTRYID]); + } + + return false; + } + + /* Creates a new TaskGlobalObjId + * + * Just 16 bytes of random data + */ + function createTGOID() + { + $goid = ""; + for($i=0;$i<16;$i++) { + $goid .= chr(rand(0, 255)); + } + return $goid; + } + + function getEmbeddedTask($message) { + $table = mapi_message_getattachmenttable($message); + $rows = mapi_table_queryallrows($table, array(PR_ATTACH_NUM)); + + // Assume only one attachment + if(empty($rows)) + return false; + + $attach = mapi_message_openattach($message, $rows[0][PR_ATTACH_NUM]); + $message = mapi_openproperty($attach, PR_ATTACH_DATA_OBJ, IID_IMessage, 0, 0); + + return $message; + } + + function setLastUser() { + $delegatestore = $this->getDefaultStore(); + $taskstore = $this->getTaskFolderStore(); + + $delegateprops = mapi_getprops($delegatestore, array(PR_MAILBOX_OWNER_NAME)); + $taskprops = mapi_getprops($taskstore, array(PR_MAILBOX_OWNER_NAME)); + + // The owner of the task + $username = $delegateprops[PR_MAILBOX_OWNER_NAME]; + // This is me (the one calling the script) + $delegate = $taskprops[PR_MAILBOX_OWNER_NAME]; + + mapi_setprops($this->message, array($this->props["tasklastuser"] => $username, $this->props["tasklastdelegate"] => $delegate, $this->props['assignedtime'] => time())); + } + + /** Assignee becomes the owner when a user/assignor assigns any task to someone. Also there can be more than one assignee. + * This function sets assignee as owner in the assignor's copy of task. + */ + function setOwnerForAssignor() + { + $recipTable = mapi_message_getrecipienttable($this->message); + $recips = mapi_table_queryallrows($recipTable, array(PR_DISPLAY_NAME)); + + if (!empty($recips)) { + $owner = array(); + foreach ($recips as $value) { + $owner[] = $value[PR_DISPLAY_NAME]; + } + + $props = array($this->props['owner'] => implode("; ", $owner)); + mapi_setprops($this->message, $props); + } + } + + /** Sets assignor as recipients in assignee's copy of task. + * + * If assignor has requested task updates then the assignor is added as recipient type MAPI_CC. + * + * Also if assignor has request SOC then the assignor is also add as recipient type MAPI_BCC + * + * @param $task message MAPI message which assignee's copy of task + */ + function setAssignorInRecipients($task) + { + $recipTable = mapi_message_getrecipienttable($task); + + // Delete all MAPI_TO recipients + $recips = mapi_table_queryallrows($recipTable, array(PR_ROWID), array(RES_PROPERTY, + array( RELOP => RELOP_EQ, + ULPROPTAG => PR_RECIPIENT_TYPE, + VALUE => MAPI_TO + ))); + foreach($recips as $recip) + mapi_message_modifyrecipients($task, MODRECIP_REMOVE, array($recip)); + + $recips = array(); + $taskReqProps = mapi_getprops($this->message, array(PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_ADDRTYPE)); + $associatedTaskProps = mapi_getprops($task, array($this->props['taskupdates'], $this->props['tasksoc'], $this->props['taskmultrecips'])); + + // Build assignor info + $assignor = array( PR_ENTRYID => $taskReqProps[PR_SENT_REPRESENTING_ENTRYID], + PR_DISPLAY_NAME => $taskReqProps[PR_SENT_REPRESENTING_NAME], + PR_EMAIL_ADDRESS => $taskReqProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS], + PR_RECIPIENT_DISPLAY_NAME => $taskReqProps[PR_SENT_REPRESENTING_NAME], + PR_ADDRTYPE => empty($taskReqProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $taskReqProps[PR_SENT_REPRESENTING_ADDRTYPE], + PR_RECIPIENT_FLAGS => recipSendable + ); + + // Assignor has requested task updates, so set him/her as MAPI_CC in recipienttable. + if ((isset($associatedTaskProps[$this->props['taskupdates']]) && $associatedTaskProps[$this->props['taskupdates']]) + && !(isset($associatedTaskProps[$this->props['taskmultrecips']]) && $associatedTaskProps[$this->props['taskmultrecips']] == tmrReceived)) { + $assignor[PR_RECIPIENT_TYPE] = MAPI_CC; + $recips[] = $assignor; + } + + // Assignor wants to receive an email report when task is mark as 'Complete', so in recipients as MAPI_BCC + if (isset($associatedTaskProps[$this->props['taskupdates']]) && $associatedTaskProps[$this->props['tasksoc']]) { + $assignor[PR_RECIPIENT_TYPE] = MAPI_BCC; + $recips[] = $assignor; + } + + if (!empty($recips)) + mapi_message_modifyrecipients($task, MODRECIP_ADD, $recips); + } + + /** Returns user information who has task request + */ + function retrieveUserData() + { + // get user entryid + $storeProps = mapi_getprops($this->store, array(PR_USER_ENTRYID)); + if (!$storeProps[PR_USER_ENTRYID]) return false; + + $ab = mapi_openaddressbook($this->session); + // open the user entry + $user = mapi_ab_openentry($ab, $storeProps[PR_USER_ENTRYID]); + if (!$user) return false; + + // receive userdata + $userProps = mapi_getprops($user, array(PR_DISPLAY_NAME)); + if (!$userProps[PR_DISPLAY_NAME]) return false; + + return $userProps; + } + + /** Deletes incoming task request from Inbox + * + * @returns array returns PR_ENTRYID, PR_STORE_ENTRYID and PR_PARENT_ENTRYID of the deleted task request + */ + function deleteReceivedTR() + { + $store = $this->getTaskFolderStore(); + $inbox = mapi_msgstore_getreceivefolder($store); + + $storeProps = mapi_getprops($store, array(PR_IPM_WASTEBASKET_ENTRYID)); + $props = mapi_getprops($this->message, array($this->props['taskglobalobjid'])); + $globalobjid = $props[$this->props['taskglobalobjid']]; + + // Find the task by looking for the taskglobalobjid + $restriction = array(RES_PROPERTY, array(RELOP => RELOP_EQ, ULPROPTAG => $this->props['taskglobalobjid'], VALUE => $globalobjid)); + + $contents = mapi_folder_getcontentstable($inbox); + + $rows = mapi_table_queryallrows($contents, array(PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID), $restriction); + + $taskrequest = false; + if(!empty($rows)) { + // If there are multiple, just use the first + $entryid = $rows[0][PR_ENTRYID]; + $wastebasket = mapi_msgstore_openentry($store, $storeProps[PR_IPM_WASTEBASKET_ENTRYID]); + mapi_folder_copymessages($inbox, Array($entryid), $wastebasket, MESSAGE_MOVE); + + return array(PR_ENTRYID => $entryid, PR_PARENT_ENTRYID => $rows[0][PR_PARENT_ENTRYID], PR_STORE_ENTRYID => $rows[0][PR_STORE_ENTRYID]); + } + + return false; + } + + /** Converts already sent task request to normal task + */ + function createUnassignedCopy() + { + mapi_deleteprops($this->message, array($this->props['taskglobalobjid'])); + mapi_setprops($this->message, array($this->props['updatecount'] => 1)); + + // Remove all recipents + $this->deleteAllRecipients($this->message); + } + + /** Sets recipients for the outgoing message according to type of the response. + * + * If it is a task update, then only recipient type MAPI_CC are taken from the task message. + * + * If it is accept/decline response, then PR_SENT_REPRESENTATING_XXXX are taken as recipient. + * + *@param $outgoing MAPI_message outgoing mapi message + *@param $responseType String response type + *@param $sendSOC Boolean true if sending complete response else false. + */ + function setRecipientsForResponse($outgoing, $responseType, $sendSOC = false) + { + // Clear recipients from outgoing msg + $this->deleteAllRecipients($outgoing); + + // If it is a task update then get MAPI_CC recipients which are assignors who has asked for task update. + if ($responseType == tdmtTaskUpd) { + $recipTable = mapi_message_getrecipienttable($this->message); + $recips = mapi_table_queryallrows($recipTable, $this->recipprops, array(RES_PROPERTY, + array( RELOP => RELOP_EQ, + ULPROPTAG => PR_RECIPIENT_TYPE, + VALUE => ($sendSOC ? MAPI_BCC : MAPI_CC) + ) + )); + + // No recipients found, return error + if (empty($recips)) + return false; + + foreach($recips as $recip) { + $recip[PR_RECIPIENT_TYPE] = MAPI_TO; // Change recipient type to MAPI_TO + mapi_message_modifyrecipients($outgoing, MODRECIP_ADD, array($recip)); + } + return true; + } + + $orgprops = mapi_getprops($this->message, array(PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_ENTRYID, PR_SUBJECT)); + $recip = array(PR_DISPLAY_NAME => $orgprops[PR_SENT_REPRESENTING_NAME], PR_EMAIL_ADDRESS => $orgprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS], PR_ADDRTYPE => $orgprops[PR_SENT_REPRESENTING_ADDRTYPE], PR_ENTRYID => $orgprops[PR_SENT_REPRESENTING_ENTRYID], PR_RECIPIENT_TYPE => MAPI_TO); + + mapi_message_modifyrecipients($outgoing, MODRECIP_ADD, array($recip)); + + return true; + } + + /** Adds task details to message body and returns body. + * + *@return string contructed body with task details. + */ + function getBody() + { + //@TODO: Fix translations + + $msgProps = mapi_getprops($this->message); + $body = ""; + + if (isset($msgProps[PR_SUBJECT])) $body .= "\n" . _("Subject") . ":\t". $msgProps[PR_SUBJECT]; + if (isset($msgProps[$this->props['startdate']])) $body .= "\n" . _("Start Date") . ":\t". strftime(_("%A, %B %d, %Y"),$msgProps[$this->props['startdate']]); + if (isset($msgProps[$this->props['duedate']])) $body .= "\n" . _("Due Date") . ":\t". strftime(_("%A, %B %d, %Y"),$msgProps[$this->props['duedate']]); + $body .= "\n"; + + if (isset($msgProps[$this->props['status']])) { + $body .= "\n" . _("Status") . ":\t"; + if ($msgProps[$this->props['status']] == 0) $body .= _("Not Started"); + else if ($msgProps[$this->props['status']] == 1) $body .= _("In Progress"); + else if ($msgProps[$this->props['status']] == 2) $body .= _("Complete"); + else if ($msgProps[$this->props['status']] == 3) $body .= _("Wait for other person"); + else if ($msgProps[$this->props['status']] == 4) $body .= _("Deferred"); + } + + if (isset($msgProps[$this->props['percent_complete']])) { + $body .= "\n" . _("Percent Complete") . ":\t". ($msgProps[$this->props['percent_complete']] * 100).'%'; + + if ($msgProps[$this->props['percent_complete']] == 1 && isset($msgProps[$this->props['datecompleted']])) + $body .= "\n" . _("Date Completed") . ":\t". strftime("%A, %B %d, %Y",$msgProps[$this->props['datecompleted']]); + } + $body .= "\n"; + + if (isset($msgProps[$this->props['totalwork']])) $body .= "\n" . _("Total Work") . ":\t". ($msgProps[$this->props['totalwork']]/60) ." " . _("hours"); + if (isset($msgProps[$this->props['actualwork']])) $body .= "\n" . _("Actual Work") . ":\t". ($msgProps[$this->props['actualwork']]/60) ." " . _("hours"); + $body .="\n"; + + if (isset($msgProps[$this->props['owner']])) $body .= "\n" . _("Owner") . ":\t". $msgProps[$this->props['owner']]; + $body .="\n"; + + if (isset($msgProps[$this->props['categories']]) && !empty($msgProps[$this->props['categories']])) $body .= "\nCategories:\t". implode(', ', $msgProps[$this->props['categories']]); + if (isset($msgProps[$this->props['companies']]) && !empty($msgProps[$this->props['companies']])) $body .= "\nCompany:\t". implode(', ', $msgProps[$this->props['companies']]); + if (isset($msgProps[$this->props['billinginformation']])) $body .= "\n" . _("Billing Information") . ":\t". $msgProps[$this->props['billinginformation']]; + if (isset($msgProps[$this->props['mileage']])) $body .= "\n" . _("Mileage") . ":\t". $msgProps[$this->props['mileage']]; + $body .="\n"; + + $content = mapi_message_openproperty($this->message, PR_BODY); + $body .= "\n". trim($content, "\0"); + + return $body; + } + + /** + * Convert from windows-1252 encoded string to UTF-8 string + * + * The same conversion rules as utf8_to_windows1252 apply. + * + * @see Conversion::utf8_to_windows1252() + * + * @param string $string the Windows-1252 string to convert + * @return string UTF-8 representation of the string + */ + function windows1252_to_utf8($string) + { + if (function_exists("iconv")){ + return iconv("Windows-1252", "UTF-8//TRANSLIT", $string); + }else{ + return utf8_encode($string); // no euro support here + } + } + + /** Reclaims ownership of a decline task + * + * Deletes taskrequest properties and recipients from the task message. + */ + function reclaimownership() + { + // Delete task request properties + mapi_deleteprops($this->message, array($this->props['taskglobalobjid'], + $this->props['tasklastuser'], + $this->props['tasklastdelegate'])); + + mapi_setprops($this->message, array($this->props['updatecount'] => 2, + $this->props['taskfcreator'] => true)); + + // Delete recipients + $this->deleteAllRecipients($this->message); + } + + /** Deletes all recipients from given message object + * + *@param $message MAPI message from which recipients are to be removed. + */ + function deleteAllRecipients($message) + { + $recipTable = mapi_message_getrecipienttable($message); + $recipRows = mapi_table_queryallrows($recipTable, array(PR_ROWID)); + + foreach($recipRows as $recipient) + mapi_message_modifyrecipients($message, MODRECIP_REMOVE, array($recipient)); + } + + function sendCompleteUpdate($prefix, $action, $prefixComplete) + { + $messageprops = mapi_getprops($this->message, array($this->props['taskstate'])); + + if(!isset($messageprops[$this->props['taskstate']]) || $messageprops[$this->props['taskstate']] != tdsOWN) + return false; // Can only decline assignee task + + mapi_setprops($this->message, array($this->props['complete'] => true, + $this->props['datecompleted'] => $action["dateCompleted"], + $this->props['status'] => 2, + $this->props['percent_complete'] => 1)); + + $this->doUpdate($prefix, $prefixComplete); + } + } +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapi/mapi.util.php b/sources/backend/zarafa/mapi/mapi.util.php new file mode 100644 index 0000000..c132c4d --- /dev/null +++ b/sources/backend/zarafa/mapi/mapi.util.php @@ -0,0 +1,339 @@ +. + * + */ + + +/** + * Function to make a MAPIGUID from a php string. + * The C++ definition for the GUID is: + * typedef struct _GUID + * { + * unsigned long Data1; + * unsigned short Data2; + * unsigned short Data3; + * unsigned char Data4[8]; + * } GUID; + * + * A GUID is normally represented in the following form: + * {00062008-0000-0000-C000-000000000046} + * + * @param String GUID + */ +function makeGuid($guid) +{ + // remove the { and } from the string and explode it into an array + $guidArray = explode('-', substr($guid, 1,strlen($guid)-2)); + + // convert to hex! + $data1[0] = intval(substr($guidArray[0], 0, 4),16); // we need to split the unsigned long + $data1[1] = intval(substr($guidArray[0], 4, 4),16); + $data2 = intval($guidArray[1], 16); + $data3 = intval($guidArray[2], 16); + + $data4[0] = intval(substr($guidArray[3], 0, 2),16); + $data4[1] = intval(substr($guidArray[3], 2, 2),16); + + for($i=0; $i < 6; $i++) + { + $data4[] = intval(substr($guidArray[4], $i*2, 2),16); + } + + return pack("vvvvCCCCCCCC", $data1[1], $data1[0], $data2, $data3, $data4[0],$data4[1],$data4[2],$data4[3],$data4[4],$data4[5],$data4[6],$data4[7]); +} + +/** + * Function to get a human readable string from a MAPI error code + * + *@param int $errcode the MAPI error code, if not given, we use mapi_last_hresult + *@return string The defined name for the MAPI error code + */ +function get_mapi_error_name($errcode=null) +{ + if ($errcode === null){ + $errcode = mapi_last_hresult(); + } + + if ($errcode !== 0) { + // get_defined_constants(true) is preferred, but crashes PHP + // https://bugs.php.net/bug.php?id=61156 + $allConstants = get_defined_constants(); + + foreach ($allConstants as $key => $value) { + /** + * If PHP encounters a number beyond the bounds of the integer type, + * it will be interpreted as a float instead, so when comparing these error codes + * we have to manually typecast value to integer, so float will be converted in integer, + * but still its out of bound for integer limit so it will be auto adjusted to minus value + */ + if ($errcode == (int) $value) { + // Check that we have an actual MAPI error or warning definition + $prefix = substr($key, 0, 7); + if ($prefix == "MAPI_E_" || $prefix == "MAPI_W_") { + return $key; + } + } + } + } else { + return "NOERROR"; + } + + // error code not found, return hex value (this is a fix for 64-bit systems, we can't use the dechex() function for this) + $result = unpack("H*", pack("N", $errcode)); + return "0x" . $result[1]; +} + +/** + * Parses properties from an array of strings. Each "string" may be either an ULONG, which is a direct property ID, + * or a string with format "PT_TYPE:{GUID}:StringId" or "PT_TYPE:{GUID}:0xXXXX" for named + * properties. + * + * @returns array of properties + */ +function getPropIdsFromStrings($store, $mapping) +{ + $props = array(); + + $ids = array("name"=>array(), "id"=>array(), "guid"=>array(), "type"=>array()); // this array stores all the information needed to retrieve a named property + $num = 0; + + // caching + $guids = array(); + + foreach($mapping as $name=>$val){ + if(is_string($val)) { + $split = explode(":", $val); + + if(count($split) != 3){ // invalid string, ignore + trigger_error(sprintf("Invalid property: %s \"%s\"",$name,$val), E_USER_NOTICE); + continue; + } + + if(substr($split[2], 0, 2) == "0x") { + $id = hexdec(substr($split[2], 2)); + } else { + $id = $split[2]; + } + + // have we used this guid before? + if (!defined($split[1])){ + if (!array_key_exists($split[1], $guids)){ + $guids[$split[1]] = makeguid($split[1]); + } + $guid = $guids[$split[1]]; + }else{ + $guid = constant($split[1]); + } + + // temp store info about named prop, so we have to call mapi_getidsfromnames just one time + $ids["name"][$num] = $name; + $ids["id"][$num] = $id; + $ids["guid"][$num] = $guid; + $ids["type"][$num] = $split[0]; + $num++; + }else{ + // not a named property + $props[$name] = $val; + } + } + + if (empty($ids["id"])){ + return $props; + } + + // get the ids + $named = mapi_getidsfromnames($store, $ids["id"], $ids["guid"]); + foreach($named as $num=>$prop){ + $props[$ids["name"][$num]] = mapi_prop_tag(constant($ids["type"][$num]), mapi_prop_id($prop)); + } + + return $props; +} + +/** + * Check wether a call to mapi_getprops returned errors for some properties. + * mapi_getprops function tries to get values of properties requested but somehow if + * if a property value can not be fetched then it changes type of property tag as PT_ERROR + * and returns error for that particular property, probable errors + * that can be returned as value can be MAPI_E_NOT_FOUND, MAPI_E_NOT_ENOUGH_MEMORY + * + * @param long $property Property to check for error + * @param Array $propArray An array of properties + * @return mixed Gives back false when there is no error, if there is, gives the error + */ +function propIsError($property, $propArray) +{ + if (array_key_exists(mapi_prop_tag(PT_ERROR, mapi_prop_id($property)), $propArray)) { + return $propArray[mapi_prop_tag(PT_ERROR, mapi_prop_id($property))]; + } else { + return false; + } +} + +/******** Macro Functions for PR_DISPLAY_TYPE_EX values *********/ +/** + * check addressbook object is a remote mailuser + */ +function DTE_IS_REMOTE_VALID($value) { + return !!($value & DTE_FLAG_REMOTE_VALID); +} + +/** + * check addressbook object is able to receive permissions + */ +function DTE_IS_ACL_CAPABLE($value) { + return !!($value & DTE_FLAG_ACL_CAPABLE); +} + +function DTE_REMOTE($value) { + return (($value & DTE_MASK_REMOTE) >> 8); +} + +function DTE_LOCAL($value) { + return ($value & DTE_MASK_LOCAL); +} + +/** + * Note: Static function, more like a utility function. + * + * Gets all the items (including recurring items) in the specified calendar in the given timeframe. Items are + * included as a whole if they overlap the interval <$start, $end> (non-inclusive). This means that if the interval + * is <08:00 - 14:00>, the item [6:00 - 8:00> is NOT included, nor is the item [14:00 - 16:00>. However, the item + * [7:00 - 9:00> is included as a whole, and is NOT capped to [8:00 - 9:00>. + * + * @param $store resource The store in which the calendar resides + * @param $calendar resource The calendar to get the items from + * @param $viewstart int Timestamp of beginning of view window + * @param $viewend int Timestamp of end of view window + * @param $propsrequested array Array of properties to return + * @param $rows array Array of rowdata as if they were returned directly from mapi_table_queryrows. Each recurring item is + * expanded so that it seems that there are only many single appointments in the table. + */ +function getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested){ + $result = array(); + $properties = getPropIdsFromStrings($store, Array( "duedate" => "PT_SYSTIME:PSETID_Appointment:0x820e", + "startdate" => "PT_SYSTIME:PSETID_Appointment:0x820d", + "enddate_recurring" => "PT_SYSTIME:PSETID_Appointment:0x8236", + "recurring" => "PT_BOOLEAN:PSETID_Appointment:0x8223", + "recurring_data" => "PT_BINARY:PSETID_Appointment:0x8216", + "timezone_data" => "PT_BINARY:PSETID_Appointment:0x8233", + "label" => "PT_LONG:PSETID_Appointment:0x8214" + )); + + // Create a restriction that will discard rows of appointments that are definitely not in our + // requested time frame + + $table = mapi_folder_getcontentstable($calendar); + + $restriction = + // OR + Array(RES_OR, + Array( + Array(RES_AND, // Normal items: itemEnd must be after viewStart, itemStart must be before viewEnd + Array( + Array(RES_PROPERTY, + Array(RELOP => RELOP_GT, + ULPROPTAG => $properties["duedate"], + VALUE => $viewstart + ) + ), + Array(RES_PROPERTY, + Array(RELOP => RELOP_LT, + ULPROPTAG => $properties["startdate"], + VALUE => $viewend + ) + ) + ) + ), + // OR + Array(RES_PROPERTY, + Array(RELOP => RELOP_EQ, + ULPROPTAG => $properties["recurring"], + VALUE => true + ) + ) + ) // EXISTS OR + ); // global OR + + // Get requested properties, plus whatever we need + $proplist = array(PR_ENTRYID, $properties["recurring"], $properties["recurring_data"], $properties["timezone_data"]); + $proplist = array_merge($proplist, $propsrequested); + $propslist = array_unique($proplist); + + $rows = mapi_table_queryallrows($table, $proplist, $restriction); + + // $rows now contains all the items that MAY be in the window; a recurring item needs expansion before including in the output. + + foreach($rows as $row) { + $items = array(); + + if(isset($row[$properties["recurring"]]) && $row[$properties["recurring"]]) { + // Recurring item + $rec = new Recurrence($store, $row); + + // GetItems guarantees that the item overlaps the interval <$viewstart, $viewend> + $occurrences = $rec->getItems($viewstart, $viewend); + foreach($occurrences as $occurrence) { + // The occurrence takes all properties from the main row, but overrides some properties (like start and end obviously) + $item = $occurrence + $row; + array_push($items, $item); + } + + } else { + // Normal item, it matched the search criteria and therefore overlaps the interval <$viewstart, $viewend> + array_push($items, $row); + } + + $result = array_merge($result,$items); + } + + // All items are guaranteed to overlap the interval <$viewstart, $viewend>. Note that we may be returning a few extra + // properties that the caller did not request (recurring, etc). This shouldn't be a problem though. + return $result; +} +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapi/mapicode.php b/sources/backend/zarafa/mapi/mapicode.php new file mode 100644 index 0000000..66905b8 --- /dev/null +++ b/sources/backend/zarafa/mapi/mapicode.php @@ -0,0 +1,250 @@ +. + * + */ + + +/** +* Status codes returned by MAPI functions +* +* +*/ + +/* From winerror.h */ +// +// Success codes +// +define('S_OK', 0x00000000); +define('S_FALSE', 0x00000001); +define('SEVERITY_ERROR', 1); + +/* from winerror.h */ + +/** +* Function to make a error +*/ +function make_mapi_e($code) +{ + return (int) mapi_make_scode(1, $code); +} + + +/** +* Function to make an warning +*/ +function make_mapi_s($code) +{ + return (int) mapi_make_scode(0, $code); +} + +/* From mapicode.h */ +/* + * On Windows NT 3.5 and Windows 95, scodes are 32-bit values + * laid out as follows: + * + * 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 + * 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 + * +-+-+-+-+-+---------------------+-------------------------------+ + * |S|R|C|N|r| Facility | Code | + * +-+-+-+-+-+---------------------+-------------------------------+ + * + * where + * + * S - Severity - indicates success/fail + * + * 0 - Success + * 1 - Fail (COERROR) + * + * R - reserved portion of the facility code, corresponds to NT's + * second severity bit. + * + * C - reserved portion of the facility code, corresponds to NT's + * C field. + * + * N - reserved portion of the facility code. Used to indicate a + * mapped NT status value. + * + * r - reserved portion of the facility code. Reserved for internal + * use. Used to indicate HRESULT values that are not status + * values, but are instead message ids for display strings. + * + * Facility - is the facility code + * FACILITY_NULL 0x0 + * FACILITY_RPC 0x1 + * FACILITY_DISPATCH 0x2 + * FACILITY_STORAGE 0x3 + * FACILITY_ITF 0x4 + * FACILITY_WIN32 0x7 + * FACILITY_WINDOWS 0x8 + * + * Code - is the facility's status code + * + */ +define('NOERROR' ,0); + +define('MAPI_E_CALL_FAILED' ,(int) 0x80004005); +define('MAPI_E_NOT_ENOUGH_MEMORY' ,(int) 0x8007000E); +define('MAPI_E_INVALID_PARAMETER' ,(int) 0x80070057); +define('MAPI_E_INTERFACE_NOT_SUPPORTED' ,(int) 0x80004002); +define('MAPI_E_NO_ACCESS' ,(int) 0x80070005); + +define('MAPI_E_NO_SUPPORT' ,make_mapi_e(0x102)); +define('MAPI_E_BAD_CHARWIDTH' ,make_mapi_e(0x103)); +define('MAPI_E_STRING_TOO_LONG' ,make_mapi_e(0x105)); +define('MAPI_E_UNKNOWN_FLAGS' ,make_mapi_e(0x106)); +define('MAPI_E_INVALID_ENTRYID' ,make_mapi_e(0x107)); +define('MAPI_E_INVALID_OBJECT' ,make_mapi_e(0x108)); +define('MAPI_E_OBJECT_CHANGED' ,make_mapi_e(0x109)); +define('MAPI_E_OBJECT_DELETED' ,make_mapi_e(0x10A)); +define('MAPI_E_BUSY' ,make_mapi_e(0x10B)); +define('MAPI_E_NOT_ENOUGH_DISK' ,make_mapi_e(0x10D)); +define('MAPI_E_NOT_ENOUGH_RESOURCES' ,make_mapi_e(0x10E)); +define('MAPI_E_NOT_FOUND' ,make_mapi_e(0x10F)); +define('MAPI_E_VERSION' ,make_mapi_e(0x110)); +define('MAPI_E_LOGON_FAILED' ,make_mapi_e(0x111)); +define('MAPI_E_SESSION_LIMIT' ,make_mapi_e(0x112)); +define('MAPI_E_USER_CANCEL' ,make_mapi_e(0x113)); +define('MAPI_E_UNABLE_TO_ABORT' ,make_mapi_e(0x114)); +define('MAPI_E_NETWORK_ERROR' ,make_mapi_e(0x115)); +define('MAPI_E_DISK_ERROR' ,make_mapi_e(0x116)); +define('MAPI_E_TOO_COMPLEX' ,make_mapi_e(0x117)); +define('MAPI_E_BAD_COLUMN' ,make_mapi_e(0x118)); +define('MAPI_E_EXTENDED_ERROR' ,make_mapi_e(0x119)); +define('MAPI_E_COMPUTED' ,make_mapi_e(0x11A)); +define('MAPI_E_CORRUPT_DATA' ,make_mapi_e(0x11B)); +define('MAPI_E_UNCONFIGURED' ,make_mapi_e(0x11C)); +define('MAPI_E_FAILONEPROVIDER' ,make_mapi_e(0x11D)); +define('MAPI_E_UNKNOWN_CPID' ,make_mapi_e(0x11E)); +define('MAPI_E_UNKNOWN_LCID' ,make_mapi_e(0x11F)); + +/* Flavors of E_ACCESSDENIED, used at logon */ + +define('MAPI_E_PASSWORD_CHANGE_REQUIRED' ,make_mapi_e(0x120)); +define('MAPI_E_PASSWORD_EXPIRED' ,make_mapi_e(0x121)); +define('MAPI_E_INVALID_WORKSTATION_ACCOUNT' ,make_mapi_e(0x122)); +define('MAPI_E_INVALID_ACCESS_TIME' ,make_mapi_e(0x123)); +define('MAPI_E_ACCOUNT_DISABLED' ,make_mapi_e(0x124)); + +/* MAPI base function and status object specific errors and warnings */ + +define('MAPI_E_END_OF_SESSION' ,make_mapi_e(0x200)); +define('MAPI_E_UNKNOWN_ENTRYID' ,make_mapi_e(0x201)); +define('MAPI_E_MISSING_REQUIRED_COLUMN' ,make_mapi_e(0x202)); +define('MAPI_W_NO_SERVICE' ,make_mapi_s(0x203)); + +/* Property specific errors and warnings */ + +define('MAPI_E_BAD_VALUE' ,make_mapi_e(0x301)); +define('MAPI_E_INVALID_TYPE' ,make_mapi_e(0x302)); +define('MAPI_E_TYPE_NO_SUPPORT' ,make_mapi_e(0x303)); +define('MAPI_E_UNEXPECTED_TYPE' ,make_mapi_e(0x304)); +define('MAPI_E_TOO_BIG' ,make_mapi_e(0x305)); +define('MAPI_E_DECLINE_COPY' ,make_mapi_e(0x306)); +define('MAPI_E_UNEXPECTED_ID' ,make_mapi_e(0x307)); + +define('MAPI_W_ERRORS_RETURNED' ,make_mapi_s(0x380)); + +/* Table specific errors and warnings */ + +define('MAPI_E_UNABLE_TO_COMPLETE' ,make_mapi_e(0x400)); +define('MAPI_E_TIMEOUT' ,make_mapi_e(0x401)); +define('MAPI_E_TABLE_EMPTY' ,make_mapi_e(0x402)); +define('MAPI_E_TABLE_TOO_BIG' ,make_mapi_e(0x403)); + +define('MAPI_E_INVALID_BOOKMARK' ,make_mapi_e(0x405)); + +define('MAPI_W_POSITION_CHANGED' ,make_mapi_s(0x481)); +define('MAPI_W_APPROX_COUNT' ,make_mapi_s(0x482)); + +/* Transport specific errors and warnings */ + +define('MAPI_E_WAIT' ,make_mapi_e(0x500)); +define('MAPI_E_CANCEL' ,make_mapi_e(0x501)); +define('MAPI_E_NOT_ME' ,make_mapi_e(0x502)); + +define('MAPI_W_CANCEL_MESSAGE' ,make_mapi_s(0x580)); + +/* Message Store, Folder, and Message specific errors and warnings */ + +define('MAPI_E_CORRUPT_STORE' ,make_mapi_e(0x600)); +define('MAPI_E_NOT_IN_QUEUE' ,make_mapi_e(0x601)); +define('MAPI_E_NO_SUPPRESS' ,make_mapi_e(0x602)); +define('MAPI_E_COLLISION' ,make_mapi_e(0x604)); +define('MAPI_E_NOT_INITIALIZED' ,make_mapi_e(0x605)); +define('MAPI_E_NON_STANDARD' ,make_mapi_e(0x606)); +define('MAPI_E_NO_RECIPIENTS' ,make_mapi_e(0x607)); +define('MAPI_E_SUBMITTED' ,make_mapi_e(0x608)); +define('MAPI_E_HAS_FOLDERS' ,make_mapi_e(0x609)); +define('MAPI_E_HAS_MESSAGES' ,make_mapi_e(0x60A)); +define('MAPI_E_FOLDER_CYCLE' ,make_mapi_e(0x60B)); +define('MAPI_E_STORE_FULL' ,make_mapi_e(0x60C)); + +define('MAPI_W_PARTIAL_COMPLETION' ,make_mapi_s(0x680)); + +/* Address Book specific errors and warnings */ + +define('MAPI_E_AMBIGUOUS_RECIP' ,make_mapi_e(0x700)); + +/* ICS errors and warnings */ + +define('SYNC_E_UNKNOWN_FLAGS', MAPI_E_UNKNOWN_FLAGS); +define('SYNC_E_INVALID_PARAMETER', MAPI_E_INVALID_PARAMETER); +define('SYNC_E_ERROR', MAPI_E_CALL_FAILED); +define('SYNC_E_OBJECT_DELETED', make_mapi_e(0x800)); +define('SYNC_E_IGNORE', make_mapi_e(0x801)); +define('SYNC_E_CONFLICT', make_mapi_e(0x802)); +define('SYNC_E_NO_PARENT', make_mapi_e(0x803)); +define('SYNC_E_INCEST', make_mapi_e(0x804)); +define('SYNC_E_UNSYNCHRONIZED', make_mapi_e(0x805)); + +define('SYNC_W_PROGRESS', make_mapi_s(0x820)); +define('SYNC_W_CLIENT_CHANGE_NEWER', make_mapi_s(0x821)); + + + +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapi/mapidefs.php b/sources/backend/zarafa/mapi/mapidefs.php new file mode 100644 index 0000000..d450fc8 --- /dev/null +++ b/sources/backend/zarafa/mapi/mapidefs.php @@ -0,0 +1,667 @@ +. + * + */ + + +/* Resource types as defined in main.h of the mapi extension */ +define('RESOURCE_SESSION' ,'MAPI Session'); +define('RESOURCE_TABLE' ,'MAPI Table'); +define('RESOURCE_ROWSET' ,'MAPI Rowset'); +define('RESOURCE_MSGSTORE' ,'MAPI Message Store'); +define('RESOURCE_FOLDER' ,'MAPI Folder'); +define('RESOURCE_MESSAGE' ,'MAPI Message'); +define('RESOURCE_ATTACHMENT' ,'MAPI Attachment'); + + +/* Object type */ + +define('MAPI_STORE' ,0x00000001); /* Message Store */ +define('MAPI_ADDRBOOK' ,0x00000002); /* Address Book */ +define('MAPI_FOLDER' ,0x00000003); /* Folder */ +define('MAPI_ABCONT' ,0x00000004); /* Address Book Container */ +define('MAPI_MESSAGE' ,0x00000005); /* Message */ +define('MAPI_MAILUSER' ,0x00000006); /* Individual Recipient */ +define('MAPI_ATTACH' ,0x00000007); /* Attachment */ +define('MAPI_DISTLIST' ,0x00000008); /* Distribution List Recipient */ +define('MAPI_PROFSECT' ,0x00000009); /* Profile Section */ +define('MAPI_STATUS' ,0x0000000A); /* Status Object */ +define('MAPI_SESSION' ,0x0000000B); /* Session */ +define('MAPI_FORMINFO' ,0x0000000C); /* Form Information */ + +define('MV_FLAG' ,0x1000); +define('MV_INSTANCE' ,0x2000); +define('MVI_FLAG' ,MV_FLAG | MV_INSTANCE); + +define('PT_UNSPECIFIED' , 0); /* (Reserved for interface use) type doesn't matter to caller */ +define('PT_NULL' , 1); /* NULL property value */ +define('PT_I2' , 2); /* Signed 16-bit value */ +define('PT_LONG' , 3); /* Signed 32-bit value */ +define('PT_R4' , 4); /* 4-byte floating point */ +define('PT_DOUBLE' , 5); /* Floating point double */ +define('PT_CURRENCY' , 6); /* Signed 64-bit int (decimal w/ 4 digits right of decimal pt) */ +define('PT_APPTIME' , 7); /* Application time */ +define('PT_ERROR' , 10); /* 32-bit error value */ +define('PT_BOOLEAN' , 11); /* 16-bit boolean (non-zero true) */ +define('PT_OBJECT' , 13); /* Embedded object in a property */ +define('PT_I8' , 20); /* 8-byte signed integer */ +define('PT_STRING8' , 30); /* Null terminated 8-bit character string */ +define('PT_UNICODE' , 31); /* Null terminated Unicode string */ +define('PT_SYSTIME' , 64); /* FILETIME 64-bit int w/ number of 100ns periods since Jan 1,1601 */ +define('PT_CLSID' , 72); /* OLE GUID */ +define('PT_BINARY' ,258); /* Uninterpreted (counted byte array) */ +/* Changes are likely to these numbers, and to their structures. */ + +/* Alternate property type names for ease of use */ +define('PT_SHORT' ,PT_I2); +define('PT_I4' ,PT_LONG); +define('PT_FLOAT' ,PT_R4); +define('PT_R8' ,PT_DOUBLE); +define('PT_LONGLONG' ,PT_I8); + + +define('PT_TSTRING' ,PT_STRING8); + + + +define('PT_MV_I2' ,(MV_FLAG | PT_I2)); +define('PT_MV_LONG' ,(MV_FLAG | PT_LONG)); +define('PT_MV_R4' ,(MV_FLAG | PT_R4)); +define('PT_MV_DOUBLE' ,(MV_FLAG | PT_DOUBLE)); +define('PT_MV_CURRENCY' ,(MV_FLAG | PT_CURRENCY)); +define('PT_MV_APPTIME' ,(MV_FLAG | PT_APPTIME)); +define('PT_MV_SYSTIME' ,(MV_FLAG | PT_SYSTIME)); +define('PT_MV_STRING8' ,(MV_FLAG | PT_STRING8)); +define('PT_MV_BINARY' ,(MV_FLAG | PT_BINARY)); +define('PT_MV_UNICODE' ,(MV_FLAG | PT_UNICODE)); +define('PT_MV_CLSID' ,(MV_FLAG | PT_CLSID)); +define('PT_MV_I8' ,(MV_FLAG | PT_I8)); + +define('PT_MV_TSTRING' ,PT_MV_STRING8); +/* bit 0: set if descending, clear if ascending */ + +define('TABLE_SORT_ASCEND' ,(0x00000000)); +define('TABLE_SORT_DESCEND' ,(0x00000001)); +define('TABLE_SORT_COMBINE' ,(0x00000002)); + +define('MAPI_UNICODE' ,0x80000000); + +/* IMAPIFolder Interface --------------------------------------------------- */ +define('CONVENIENT_DEPTH' ,0x00000001); +define('SEARCH_RUNNING' ,0x00000001); +define('SEARCH_REBUILD' ,0x00000002); +define('SEARCH_RECURSIVE' ,0x00000004); +define('SEARCH_FOREGROUND' ,0x00000008); +define('STOP_SEARCH' ,0x00000001); +define('RESTART_SEARCH' ,0x00000002); +define('RECURSIVE_SEARCH' ,0x00000004); +define('SHALLOW_SEARCH' ,0x00000008); +define('FOREGROUND_SEARCH' ,0x00000010); +define('BACKGROUND_SEARCH' ,0x00000020); + +/* IMAPIFolder folder type (enum) */ + +define('FOLDER_ROOT' ,0x00000000); +define('FOLDER_GENERIC' ,0x00000001); +define('FOLDER_SEARCH' ,0x00000002); + +/* CreateMessage */ +/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */ +/****** MAPI_ASSOCIATED ((ULONG) 0x00000040) below */ + +/* CopyMessages */ + +define('MESSAGE_MOVE' ,0x00000001); +define('MESSAGE_DIALOG' ,0x00000002); +/****** MAPI_DECLINE_OK ((ULONG) 0x00000004) above */ + +/* CreateFolder */ + +define('OPEN_IF_EXISTS' ,0x00000001); +/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */ +/****** MAPI_UNICODE ((ULONG) 0x80000000) above */ + +/* DeleteFolder */ + +define('DEL_MESSAGES' ,0x00000001); +define('FOLDER_DIALOG' ,0x00000002); +define('DEL_FOLDERS' ,0x00000004); + +/* EmptyFolder */ +define('DEL_ASSOCIATED' ,0x00000008); + +/* CopyFolder */ + +define('FOLDER_MOVE' ,0x00000001); +/****** FOLDER_DIALOG ((ULONG) 0x00000002) above */ +/****** MAPI_DECLINE_OK ((ULONG) 0x00000004) above */ +define('COPY_SUBFOLDERS' ,0x00000010); +/****** MAPI_UNICODE ((ULONG) 0x80000000) above */ + + +/* SetReadFlags */ + +define('SUPPRESS_RECEIPT' ,0x00000001); +/****** FOLDER_DIALOG ((ULONG) 0x00000002) above */ +define('CLEAR_READ_FLAG' ,0x00000004); +/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */ +define('GENERATE_RECEIPT_ONLY' ,0x00000010); +define('CLEAR_RN_PENDING' ,0x00000020); +define('CLEAR_NRN_PENDING' ,0x00000040); + +/* Flags defined in PR_MESSAGE_FLAGS */ + +define('MSGFLAG_READ' ,0x00000001); +define('MSGFLAG_UNMODIFIED' ,0x00000002); +define('MSGFLAG_SUBMIT' ,0x00000004); +define('MSGFLAG_UNSENT' ,0x00000008); +define('MSGFLAG_HASATTACH' ,0x00000010); +define('MSGFLAG_FROMME' ,0x00000020); +define('MSGFLAG_ASSOCIATED' ,0x00000040); +define('MSGFLAG_RESEND' ,0x00000080); +define('MSGFLAG_RN_PENDING' ,0x00000100); +define('MSGFLAG_NRN_PENDING' ,0x00000200); + +/* GetMessageStatus */ + +define('MSGSTATUS_HIGHLIGHTED' ,0x00000001); +define('MSGSTATUS_TAGGED' ,0x00000002); +define('MSGSTATUS_HIDDEN' ,0x00000004); +define('MSGSTATUS_DELMARKED' ,0x00000008); + +/* Bits for remote message status */ + +define('MSGSTATUS_REMOTE_DOWNLOAD' ,0x00001000); +define('MSGSTATUS_REMOTE_DELETE' ,0x00002000); + +/* SaveContentsSort */ + +define('RECURSIVE_SORT' ,0x00000002); + +/* PR_STATUS property */ + +define('FLDSTATUS_HIGHLIGHTED' ,0x00000001); +define('FLDSTATUS_TAGGED' ,0x00000002); +define('FLDSTATUS_HIDDEN' ,0x00000004); +define('FLDSTATUS_DELMARKED' ,0x00000008); + + +/* IMAPIStatus Interface --------------------------------------------------- */ + +/* Values for PR_RESOURCE_TYPE, _METHODS, _FLAGS */ + +define('MAPI_STORE_PROVIDER' , 33); /* Message Store */ +define('MAPI_AB' , 34); /* Address Book */ +define('MAPI_AB_PROVIDER' , 35); /* Address Book Provider */ +define('MAPI_TRANSPORT_PROVIDER' , 36); /* Transport Provider */ +define('MAPI_SPOOLER' , 37); /* Message Spooler */ +define('MAPI_PROFILE_PROVIDER' , 38); /* Profile Provider */ +define('MAPI_SUBSYSTEM' , 39); /* Overall Subsystem Status */ +define('MAPI_HOOK_PROVIDER' , 40); /* Spooler Hook */ +define('STATUS_VALIDATE_STATE' ,0x00000001); +define('STATUS_SETTINGS_DIALOG' ,0x00000002); +define('STATUS_CHANGE_PASSWORD' ,0x00000004); +define('STATUS_FLUSH_QUEUES' ,0x00000008); + +define('STATUS_DEFAULT_OUTBOUND' ,0x00000001); +define('STATUS_DEFAULT_STORE' ,0x00000002); +define('STATUS_PRIMARY_IDENTITY' ,0x00000004); +define('STATUS_SIMPLE_STORE' ,0x00000008); +define('STATUS_XP_PREFER_LAST' ,0x00000010); +define('STATUS_NO_PRIMARY_IDENTITY' ,0x00000020); +define('STATUS_NO_DEFAULT_STORE' ,0x00000040); +define('STATUS_TEMP_SECTION' ,0x00000080); +define('STATUS_OWN_STORE' ,0x00000100); +define('STATUS_NEED_IPM_TREE' ,0x00000800); +define('STATUS_PRIMARY_STORE' ,0x00001000); +define('STATUS_SECONDARY_STORE' ,0x00002000); + + +/* ------------ */ +/* Random flags */ + +/* Flag for deferred error */ +define('MAPI_DEFERRED_ERRORS' ,0x00000008); + +/* Flag for creating and using Folder Associated Information Messages */ +define('MAPI_ASSOCIATED' ,0x00000040); + +/* Flags for OpenMessageStore() */ + +define('MDB_NO_DIALOG' ,0x00000001); +define('MDB_WRITE' ,0x00000004); +/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) above */ +/****** MAPI_BEST_ACCESS ((ULONG) 0x00000010) above */ +define('MDB_TEMPORARY' ,0x00000020); +define('MDB_NO_MAIL' ,0x00000080); + +/* Flags for OpenAddressBook */ + +define('AB_NO_DIALOG' ,0x00000001); + +/* ((ULONG) 0x00000001 is not a valid flag on ModifyRecipients. */ +define('MODRECIP_ADD' ,0x00000002); +define('MODRECIP_MODIFY' ,0x00000004); +define('MODRECIP_REMOVE' ,0x00000008); + + +define('MAPI_ORIG' ,0); /* Recipient is message originator */ +define('MAPI_TO' ,1); /* Recipient is a primary recipient */ +define('MAPI_CC' ,2); /* Recipient is a copy recipient */ +define('MAPI_BCC' ,3); /* Recipient is blind copy recipient */ + + +/* IAttach Interface ------------------------------------------------------- */ + +/* IAttach attachment methods: PR_ATTACH_METHOD values */ + +define('NO_ATTACHMENT' ,0x00000000); +define('ATTACH_BY_VALUE' ,0x00000001); +define('ATTACH_BY_REFERENCE' ,0x00000002); +define('ATTACH_BY_REF_RESOLVE' ,0x00000003); +define('ATTACH_BY_REF_ONLY' ,0x00000004); +define('ATTACH_EMBEDDED_MSG' ,0x00000005); +define('ATTACH_OLE' ,0x00000006); + +/* OpenProperty - ulFlags */ +define('MAPI_MODIFY' ,0x00000001); +define('MAPI_CREATE' ,0x00000002); +define('STREAM_APPEND' ,0x00000004); +/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */ + + +/* PR_PRIORITY values */ +define('PRIO_URGENT' , 1); +define('PRIO_NORMAL' , 0); +define('PRIO_NONURGENT' ,-1); + +/* PR_SENSITIVITY values */ +define('SENSITIVITY_NONE' ,0x00000000); +define('SENSITIVITY_PERSONAL' ,0x00000001); +define('SENSITIVITY_PRIVATE' ,0x00000002); +define('SENSITIVITY_COMPANY_CONFIDENTIAL' ,0x00000003); + +/* PR_IMPORTANCE values */ +define('IMPORTANCE_LOW' ,0); +define('IMPORTANCE_NORMAL' ,1); +define('IMPORTANCE_HIGH' ,2); + +/* Stream interace values */ +define('STREAM_SEEK_SET' ,0); +define('STREAM_SEEK_CUR' ,1); +define('STREAM_SEEK_END' ,2); + +define('SHOW_SOFT_DELETES' ,0x00000002); +define('DELETE_HARD_DELETE' ,0x00000010); + +/* + * The following flags are used to indicate to the client what access + * level is permissible in the object. They appear in PR_ACCESS in + * message and folder objects as well as in contents and associated + * contents tables + */ +define('MAPI_ACCESS_MODIFY' ,0x00000001); +define('MAPI_ACCESS_READ' ,0x00000002); +define('MAPI_ACCESS_DELETE' ,0x00000004); +define('MAPI_ACCESS_CREATE_HIERARCHY' ,0x00000008); +define('MAPI_ACCESS_CREATE_CONTENTS' ,0x00000010); +define('MAPI_ACCESS_CREATE_ASSOCIATED' ,0x00000020); + +define('MAPI_SEND_NO_RICH_INFO' ,0x00010000); + +/* flags for PR_STORE_SUPPORT_MASK */ +define('STORE_ANSI_OK' ,0x00020000); // The message store supports properties containing ANSI (8-bit) characters. +define('STORE_ATTACH_OK' ,0x00000020); // The message store supports attachments (OLE or non-OLE) to messages. +define('STORE_CATEGORIZE_OK' ,0x00000400); // The message store supports categorized views of tables. +define('STORE_CREATE_OK' ,0x00000010); // The message store supports creation of new messages. +define('STORE_ENTRYID_UNIQUE' ,0x00000001); // Entry identifiers for the objects in the message store are unique, that is, never reused during the life of the store. +define('STORE_HTML_OK' ,0x00010000); // The message store supports Hypertext Markup Language (HTML) messages, stored in the PR_BODY_HTML property. Note that STORE_HTML_OK is not defined in versions of MAPIDEFS.H included with Microsoft� Exchange 2000 Server and earlier. If your development environment uses a MAPIDEFS.H file that does not include STORE_HTML_OK, use the value 0x00010000 instead. +define('STORE_LOCALSTORE' ,0x00080000); // This flag is reserved and should not be used. +define('STORE_MODIFY_OK' ,0x00000008); // The message store supports modification of its existing messages. +define('STORE_MV_PROPS_OK' ,0x00000200); // The message store supports multivalued properties, guarantees the stability of value order in a multivalued property throughout a save operation, and supports instantiation of multivalued properties in tables. +define('STORE_NOTIFY_OK' ,0x00000100); // The message store supports notifications. +define('STORE_OLE_OK' ,0x00000040); // The message store supports OLE attachments. The OLE data is accessible through an IStorage interface, such as that available through the PR_ATTACH_DATA_OBJ property. +define('STORE_PUBLIC_FOLDERS' ,0x00004000); // The folders in this store are public (multi-user), not private (possibly multi-instance but not multi-user). +define('STORE_READONLY' ,0x00000002); // All interfaces for the message store have a read-only access level. +define('STORE_RESTRICTION_OK' ,0x00001000); // The message store supports restrictions. +define('STORE_RTF_OK' ,0x00000800); // The message store supports Rich Text Format (RTF) messages, usually stored compressed, and the store itself keeps PR_BODY and PR_RTF_COMPRESSED synchronized. +define('STORE_SEARCH_OK' ,0x00000004); // The message store supports search-results folders. +define('STORE_SORT_OK' ,0x00002000); // The message store supports sorting views of tables. +define('STORE_SUBMIT_OK' ,0x00000080); // The message store supports marking a message for submission. +define('STORE_UNCOMPRESSED_RTF' ,0x00008000); // The message store supports storage of Rich Text Format (RTF) messages in uncompressed form. An uncompressed RTF stream is identified by the value dwMagicUncompressedRTF in the stream header. The dwMagicUncompressedRTF value is defined in the RTFLIB.H file. +define('STORE_UNICODE_OK' ,0x00040000); // The message store supports properties containing Unicode characters. + + +/* PR_DISPLAY_TYPEs */ +/* For address book contents tables */ +define('DT_MAILUSER' ,0x00000000); +define('DT_DISTLIST' ,0x00000001); +define('DT_FORUM' ,0x00000002); +define('DT_AGENT' ,0x00000003); +define('DT_ORGANIZATION' ,0x00000004); +define('DT_PRIVATE_DISTLIST' ,0x00000005); +define('DT_REMOTE_MAILUSER' ,0x00000006); + +/* For address book hierarchy tables */ +define('DT_MODIFIABLE' ,0x00010000); +define('DT_GLOBAL' ,0x00020000); +define('DT_LOCAL' ,0x00030000); +define('DT_WAN' ,0x00040000); +define('DT_NOT_SPECIFIC' ,0x00050000); + +/* For folder hierarchy tables */ +define('DT_FOLDER' ,0x01000000); +define('DT_FOLDER_LINK' ,0x02000000); +define('DT_FOLDER_SPECIAL' ,0x04000000); + +/* PR_DISPLAY_TYPE_EX values */ +define('DT_ROOM' ,0x00000007); +define('DT_EQUIPMENT' ,0x00000008); +define('DT_SEC_DISTLIST' ,0x00000009); + +/* PR_DISPLAY_TYPE_EX flags */ +define('DTE_FLAG_REMOTE_VALID' ,0x80000000); +define('DTE_FLAG_ACL_CAPABLE' ,0x40000000); /* on for DT_MAILUSER and DT_SEC_DISTLIST */ +define('DTE_MASK_REMOTE' ,0x0000FF00); +define('DTE_MASK_LOCAL' ,0x000000FF); + +/* OlResponseStatus */ +define('olResponseNone' ,0); +define('olResponseOrganized' ,1); +define('olResponseTentative' ,2); +define('olResponseAccepted' ,3); +define('olResponseDeclined' ,4); +define('olResponseNotResponded' ,5); + +/* OlRecipientTrackStatus to set PR_RECIPIENT_TRACKSTATUS in recipient table + * Value of the recipient trackstatus are same as OlResponseStatus but + * recipient trackstatus doesn't have olResponseOrganized and olResponseNotResponded + * and olResponseNone has different interpretation with PR_RECIPIENT_TRACKSTATUS + * so to avoid confusions we have defined new constants. +*/ +define('olRecipientTrackStatusNone' ,0); +define('olRecipientTrackStatusTentative' ,2); +define('olRecipientTrackStatusAccepted' ,3); +define('olRecipientTrackStatusDeclined' ,4); + +/* OlMeetingStatus */ +define('olNonMeeting' ,0); +define('olMeeting' ,1); +define('olMeetingReceived' ,3); +define('olMeetingCanceled' ,5); +define('olMeetingReceivedAndCanceled' ,7); + +/* OlMeetingResponse */ +define('olMeetingTentative' ,2); +define('olMeetingAccepted' ,3); +define('olMeetingDeclined' ,4); + +/* OL Attendee type */ +define('olAttendeeRequired' ,1); +define('olAttendeeOptional' ,2); +define('olAttendeeResource' ,3); + +/* task status */ +define('olTaskNotStarted' ,0); +define('olTaskInProgress' ,1); +define('olTaskComplete' ,2); +define('olTaskWaiting' ,3); +define('olTaskDeferred' ,4); + +/* restrictions */ +define('RES_AND' ,0); +define('RES_OR' ,1); +define('RES_NOT' ,2); +define('RES_CONTENT' ,3); +define('RES_PROPERTY' ,4); +define('RES_COMPAREPROPS' ,5); +define('RES_BITMASK' ,6); +define('RES_SIZE' ,7); +define('RES_EXIST' ,8); +define('RES_SUBRESTRICTION' ,9); +define('RES_COMMENT' ,10); + +/* restriction compares */ +define('RELOP_LT' ,0); +define('RELOP_LE' ,1); +define('RELOP_GT' ,2); +define('RELOP_GE' ,3); +define('RELOP_EQ' ,4); +define('RELOP_NE' ,5); +define('RELOP_RE' ,6); + +/* string 'fuzzylevel' */ +define('FL_FULLSTRING' ,0x00000000); +define('FL_SUBSTRING' ,0x00000001); +define('FL_PREFIX' ,0x00000002); +define('FL_IGNORECASE' ,0x00010000); +define('FL_IGNORENONSPACE' ,0x00020000); +define('FL_LOOSE' ,0x00040000); + +/* bitmask restriction types */ +define('BMR_EQZ' ,0x00000000); +define('BMR_NEZ' ,0x00000001); + +/* array index values of restrictions -- same values are used in php-ext/main.cpp::PHPArraytoSRestriction() */ +define('VALUE' ,0); // propval +define('RELOP' ,1); // compare method +define('FUZZYLEVEL' ,2); // string search flags +define('CB' ,3); // size restriction +define('ULTYPE' ,4); // bit mask restriction type BMR_xxx +define('ULMASK' ,5); // bitmask +define('ULPROPTAG' ,6); // property +define('ULPROPTAG1' ,7); // RES_COMPAREPROPS 1st property +define('ULPROPTAG2' ,8); // RES_COMPAREPROPS 2nd property +define('PROPS' ,9); // RES_COMMENT properties +define('RESTRICTION' ,10); // RES_COMMENT and RES_SUBRESTRICTION restriction + +/* GUID's for PR_MDB_PROVIDER */ +define("ZARAFA_SERVICE_GUID" ,makeGuid("{3C253DCA-D227-443C-94FE-425FAB958C19}")); // default store +define("ZARAFA_STORE_PUBLIC_GUID" ,makeGuid("{D47F4A09-D3BD-493C-B2FC-3C90BBCB48D4}")); // public store +define("ZARAFA_STORE_DELEGATE_GUID" ,makeGuid("{7C7C1085-BC6D-4E53-9DAB-8A53F8DEF808}")); // other store +define('ZARAFA_STORE_ARCHIVER_GUID' ,makeGuid("{BC8953AD-2E3F-4172-9404-896FF459870F}")); // archive store + +/* global profile section guid */ +define('pbGlobalProfileSectionGuid' ,makeGuid("{C8B0DB13-05AA-1A10-9BB0-00AA002FC45A}")); + +/* Zarafa Contacts provider GUID */ +define('ZARAFA_CONTACTS_GUID' ,makeGuid("{30047F72-92E3-DA4F-B86A-E52A7FE46571}")); + +/* Permissions */ + +// Get permission type +define('ACCESS_TYPE_DENIED' ,1); +define('ACCESS_TYPE_GRANT' ,2); +define('ACCESS_TYPE_BOTH' ,3); + +define('ecRightsNone' ,0x00000000); +define('ecRightsReadAny' ,0x00000001); +define('ecRightsCreate' ,0x00000002); +define('ecRightsEditOwned' ,0x00000008); +define('ecRightsDeleteOwned' ,0x00000010); +define('ecRightsEditAny' ,0x00000020); +define('ecRightsDeleteAny' ,0x00000040); +define('ecRightsCreateSubfolder' ,0x00000080); +define('ecRightsFolderAccess' ,0x00000100); +//define('ecrightsContact' ,0x00000200); +define('ecRightsFolderVisible' ,0x00000400); + +define('ecRightsAll' ,ecRightsReadAny | ecRightsCreate | ecRightsEditOwned | ecRightsDeleteOwned | ecRightsEditAny | ecRightsDeleteAny | ecRightsCreateSubfolder | ecRightsFolderAccess | ecRightsFolderVisible); +define('ecRightsFullControl' ,ecRightsReadAny | ecRightsCreate | ecRightsEditOwned | ecRightsDeleteOwned | ecRightsEditAny | ecRightsDeleteAny | ecRightsCreateSubfolder | ecRightsFolderVisible); +define('ecRightsDefault' ,ecRightsNone | ecRightsFolderVisible); +define('ecRightsDefaultPublic' ,ecRightsReadAny | ecRightsFolderVisible); +define('ecRightsAdmin' ,0x00001000); +define('ecRightsAllMask' ,0x000015FB); + +// Right change indication +define('RIGHT_NORMAL' ,0x00); +define('RIGHT_NEW' ,0x01); +define('RIGHT_MODIFY' ,0x02); +define('RIGHT_DELETED' ,0x04); +define('RIGHT_AUTOUPDATE_DENIED' ,0x08); + +// IExchangeModifyTable: defines for rules +define('ROWLIST_REPLACE' ,0x0001); +define('ROW_ADD' ,0x0001); +define('ROW_MODIFY' ,0x0002); +define('ROW_REMOVE' ,0x0004); +define('ROW_EMPTY' ,(ROW_ADD|ROW_REMOVE)); + +// new property types +define('PT_SRESTRICTION' ,0x00FD); +define('PT_ACTIONS' ,0x00FE); +// unused, I believe +define('PT_FILE_HANDLE' ,0x0103); +define('PT_FILE_EA' ,0x0104); +define('PT_VIRTUAL' ,0x0105); + +// rules state +define('ST_DISABLED' ,0x0000); +define('ST_ENABLED' ,0x0001); +define('ST_ERROR' ,0x0002); +define('ST_ONLY_WHEN_OOF' ,0x0004); +define('ST_KEEP_OOF_HIST' ,0x0008); +define('ST_EXIT_LEVEL' ,0x0010); +define('ST_SKIP_IF_SCL_IS_SAFE' ,0x0020); +define('ST_RULE_PARSE_ERROR' ,0x0040); +define('ST_CLEAR_OOF_HIST' ,0x80000000); + +// action types +define('OP_MOVE' ,1); +define('OP_COPY' ,2); +define('OP_REPLY' ,3); +define('OP_OOF_REPLY' ,4); +define('OP_DEFER_ACTION' ,5); +define('OP_BOUNCE' ,6); +define('OP_FORWARD' ,7); +define('OP_DELEGATE' ,8); +define('OP_TAG' ,9); +define('OP_DELETE' ,10); +define('OP_MARK_AS_READ' ,11); + +// for OP_REPLY +define('DO_NOT_SEND_TO_ORIGINATOR' ,1); +define('STOCK_REPLY_TEMPLATE' ,2); + +// for OP_FORWARD +define('FWD_PRESERVE_SENDER' ,1); +define('FWD_DO_NOT_MUNGE_MSG' ,2); +define('FWD_AS_ATTACHMENT' ,4); + +// scBounceCodevalues +define('BOUNCE_MESSAGE_SIZE_TOO_LARGE' ,13); +define('BOUNCE_FORMS_MISMATCH' ,31); +define('BOUNCE_ACCESS_DENIED' ,38); + +// Free/busystatus +define('fbFree' ,0); +define('fbTentative' ,1); +define('fbBusy' ,2); +define('fbOutOfOffice' ,3); + +/* ICS flags */ + +// For Synchronize() +define('SYNC_UNICODE' ,0x01); +define('SYNC_NO_DELETIONS' ,0x02); +define('SYNC_NO_SOFT_DELETIONS' ,0x04); +define('SYNC_READ_STATE' ,0x08); +define('SYNC_ASSOCIATED' ,0x10); +define('SYNC_NORMAL' ,0x20); +define('SYNC_NO_CONFLICTS' ,0x40); +define('SYNC_ONLY_SPECIFIED_PROPS' ,0x80); +define('SYNC_NO_FOREIGN_KEYS' ,0x100); +define('SYNC_LIMITED_IMESSAGE' ,0x200); +define('SYNC_CATCHUP' ,0x400); +define('SYNC_NEW_MESSAGE' ,0x800); // only applicable to ImportMessageChange() +define('SYNC_MSG_SELECTIVE' ,0x1000); // Used internally. Will reject if used by clients. +define('SYNC_BEST_BODY' ,0x2000); +define('SYNC_IGNORE_SPECIFIED_ON_ASSOCIATED' ,0x4000); +define('SYNC_PROGRESS_MODE' ,0x8000); // AirMapi progress mode +define('SYNC_FXRECOVERMODE' ,0x10000); +define('SYNC_DEFER_CONFIG' ,0x20000); +define('SYNC_FORCE_UNICODE' ,0x40000); // Forces server to return Unicode properties + +define('EMS_AB_ADDRESS_LOOKUP' ,0x00000001); // Flag for resolvename to resolve only exact matches + +define('TBL_BATCH' ,0x00000002); // Batch multiple table commands + +/* Flags for recipients in exceptions */ +define('recipSendable' ,0x00000001); // sendable attendee. +define('recipOrganizer' ,0x00000002); // meeting organizer +define('recipExceptionalResponse' ,0x00000010); // attendee gave a response for the exception +define('recipExceptionalDeleted' ,0x00000020); // recipientRow exists, but it is treated as if the corresponding recipient is deleted from meeting +define('recipOriginal' ,0x00000100); // recipient is an original Attendee +define('recipReserved' ,0x00000200); + +/* Flags which indicates type of Meeting Object */ +define('mtgEmpty' ,0x00000000); // Unspecified. +define('mtgRequest' ,0x00000001); // Initial meeting request. +define('mtgFull' ,0x00010000); // Full update. +define('mtgInfo' ,0x00020000); // Informational update. +define('mtgOutOfDate' ,0x00080000); // A newer Meeting Request object or Meeting Update object was received after this one. +define('mtgDelegatorCopy' ,0x00100000); // This is set on the delegator's copy when a delegate will handle meeting-related objects. + +define('MAPI_ONE_OFF_UNICODE' ,0x8000); // the flag that defines whether the embedded strings are Unicode in one off entryids. +define('MAPI_ONE_OFF_NO_RICH_INFO' ,0x0001); // the flag that specifies whether the recipient gets TNEF or not. + +/* Mask flags for mapi_msgstore_advise */ +define('fnevCriticalError' ,0x00000001); +define('fnevNewMail' ,0x00000002); +define('fnevObjectCreated' ,0x00000004); +define('fnevObjectDeleted' ,0x00000008); +define('fnevObjectModified' ,0x00000010); +define('fnevObjectMoved' ,0x00000020); +define('fnevObjectCopied' ,0x00000040); +define('fnevSearchComplete' ,0x00000080); +define('fnevTableModified' ,0x00000100); +define('fnevStatusObjectModified' ,0x00000200); +define('fnevReservedForMapi' ,0x40000000); +define('fnevExtended' ,0x80000000); + +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapi/mapiguid.php b/sources/backend/zarafa/mapi/mapiguid.php new file mode 100644 index 0000000..5dd4a67 --- /dev/null +++ b/sources/backend/zarafa/mapi/mapiguid.php @@ -0,0 +1,74 @@ +. + * + */ + +define('IID_IStream', makeguid("{0000000c-0000-0000-c000-000000000046}")); +define('IID_IMAPITable', makeguid("{00020301-0000-0000-c000-000000000046}")); +define('IID_IMessage', makeguid("{00020307-0000-0000-c000-000000000046}")); +define('IID_IExchangeExportChanges', makeguid("{a3ea9cc0-d1b2-11cd-80fc-00aa004bba0b}")); +define('IID_IExchangeImportContentsChanges', makeguid("{f75abfa0-d0e0-11cd-80fc-00aa004bba0b}")); +define('IID_IExchangeImportHierarchyChanges', makeguid("{85a66cf0-d0e0-11cd-80fc-00aa004bba0b}")); + +define('PSETID_Appointment', makeguid("{00062002-0000-0000-C000-000000000046}")); +define('PSETID_Task', makeguid("{00062003-0000-0000-C000-000000000046}")); +define('PSETID_Address', makeguid("{00062004-0000-0000-C000-000000000046}")); +define('PSETID_Common', makeguid("{00062008-0000-0000-C000-000000000046}")); +define('PSETID_Log', makeguid("{0006200A-0000-0000-C000-000000000046}")); +define('PSETID_Note', makeguid("{0006200E-0000-0000-C000-000000000046}")); +define('PSETID_Meeting', makeguid("{6ED8DA90-450B-101B-98DA-00AA003F1305}")); +define('PSETID_Archive', makeguid("{72E98EBC-57D2-4AB5-B0AA-D50A7B531CB9}")); + +define('PS_MAPI', makeguid("{00020328-0000-0000-C000-000000000046}")); +define('PS_PUBLIC_STRINGS', makeguid("{00020329-0000-0000-C000-000000000046}")); +define('PS_INTERNET_HEADERS', makeguid("{00020386-0000-0000-c000-000000000046}")); + +// sk added for Z-Push +define ('PSETID_AirSync', makeguid("{71035549-0739-4DCB-9163-00F0580DBBDF}")); + +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapi/mapitags.php b/sources/backend/zarafa/mapi/mapitags.php new file mode 100644 index 0000000..af8f1a8 --- /dev/null +++ b/sources/backend/zarafa/mapi/mapitags.php @@ -0,0 +1,1247 @@ +. + * + */ + +if (!function_exists("mapi_prop_tag")) + throw new FatalMisconfigurationException("PHP-MAPI extension is not available"); + +define('PR_ACKNOWLEDGEMENT_MODE' ,mapi_prop_tag(PT_LONG, 0x0001)); +define('PR_ALTERNATE_RECIPIENT_ALLOWED' ,mapi_prop_tag(PT_BOOLEAN, 0x0002)); +define('PR_AUTHORIZING_USERS' ,mapi_prop_tag(PT_BINARY, 0x0003)); +define('PR_AUTO_FORWARD_COMMENT' ,mapi_prop_tag(PT_TSTRING, 0x0004)); +define('PR_AUTO_FORWARD_COMMENT_W' ,mapi_prop_tag(PT_UNICODE, 0x0004)); +define('PR_AUTO_FORWARD_COMMENT_A' ,mapi_prop_tag(PT_STRING8, 0x0004)); +define('PR_AUTO_FORWARDED' ,mapi_prop_tag(PT_BOOLEAN, 0x0005)); +define('PR_CONTENT_CONFIDENTIALITY_ALGORITHM_ID' ,mapi_prop_tag(PT_BINARY, 0x0006)); +define('PR_CONTENT_CORRELATOR' ,mapi_prop_tag(PT_BINARY, 0x0007)); +define('PR_CONTENT_IDENTIFIER' ,mapi_prop_tag(PT_TSTRING, 0x0008)); +define('PR_CONTENT_IDENTIFIER_W' ,mapi_prop_tag(PT_UNICODE, 0x0008)); +define('PR_CONTENT_IDENTIFIER_A' ,mapi_prop_tag(PT_STRING8, 0x0008)); +define('PR_CONTENT_LENGTH' ,mapi_prop_tag(PT_LONG, 0x0009)); +define('PR_CONTENT_RETURN_REQUESTED' ,mapi_prop_tag(PT_BOOLEAN, 0x000A)); + + + +define('PR_CONVERSATION_KEY' ,mapi_prop_tag(PT_BINARY, 0x000B)); + +define('PR_CONVERSION_EITS' ,mapi_prop_tag(PT_BINARY, 0x000C)); +define('PR_CONVERSION_WITH_LOSS_PROHIBITED' ,mapi_prop_tag(PT_BOOLEAN, 0x000D)); +define('PR_CONVERTED_EITS' ,mapi_prop_tag(PT_BINARY, 0x000E)); +define('PR_DEFERRED_DELIVERY_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x000F)); +define('PR_DELIVER_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x0010)); +define('PR_DISCARD_REASON' ,mapi_prop_tag(PT_LONG, 0x0011)); +define('PR_DISCLOSURE_OF_RECIPIENTS' ,mapi_prop_tag(PT_BOOLEAN, 0x0012)); +define('PR_DL_EXPANSION_HISTORY' ,mapi_prop_tag(PT_BINARY, 0x0013)); +define('PR_DL_EXPANSION_PROHIBITED' ,mapi_prop_tag(PT_BOOLEAN, 0x0014)); +define('PR_EXPIRY_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x0015)); +define('PR_IMPLICIT_CONVERSION_PROHIBITED' ,mapi_prop_tag(PT_BOOLEAN, 0x0016)); +define('PR_IMPORTANCE' ,mapi_prop_tag(PT_LONG, 0x0017)); +define('PR_IPM_ID' ,mapi_prop_tag(PT_BINARY, 0x0018)); +define('PR_LATEST_DELIVERY_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x0019)); +define('PR_MESSAGE_CLASS' ,mapi_prop_tag(PT_TSTRING, 0x001A)); +define('PR_MESSAGE_CLASS_W' ,mapi_prop_tag(PT_UNICODE, 0x001A)); +define('PR_MESSAGE_CLASS_A' ,mapi_prop_tag(PT_STRING8, 0x001A)); +define('PR_MESSAGE_DELIVERY_ID' ,mapi_prop_tag(PT_BINARY, 0x001B)); + + + + + +define('PR_MESSAGE_SECURITY_LABEL' ,mapi_prop_tag(PT_BINARY, 0x001E)); +define('PR_OBSOLETED_IPMS' ,mapi_prop_tag(PT_BINARY, 0x001F)); +define('PR_ORIGINALLY_INTENDED_RECIPIENT_NAME' ,mapi_prop_tag(PT_BINARY, 0x0020)); +define('PR_ORIGINAL_EITS' ,mapi_prop_tag(PT_BINARY, 0x0021)); +define('PR_ORIGINATOR_CERTIFICATE' ,mapi_prop_tag(PT_BINARY, 0x0022)); +define('PR_ORIGINATOR_DELIVERY_REPORT_REQUESTED' ,mapi_prop_tag(PT_BOOLEAN, 0x0023)); +define('PR_ORIGINATOR_RETURN_ADDRESS' ,mapi_prop_tag(PT_BINARY, 0x0024)); + +define('PR_PARENT_KEY' ,mapi_prop_tag(PT_BINARY, 0x0025)); +define('PR_PRIORITY' ,mapi_prop_tag(PT_LONG, 0x0026)); + +define('PR_ORIGIN_CHECK' ,mapi_prop_tag(PT_BINARY, 0x0027)); +define('PR_PROOF_OF_SUBMISSION_REQUESTED' ,mapi_prop_tag(PT_BOOLEAN, 0x0028)); +define('PR_READ_RECEIPT_REQUESTED' ,mapi_prop_tag(PT_BOOLEAN, 0x0029)); +define('PR_RECEIPT_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x002A)); +define('PR_RECIPIENT_REASSIGNMENT_PROHIBITED' ,mapi_prop_tag(PT_BOOLEAN, 0x002B)); +define('PR_REDIRECTION_HISTORY' ,mapi_prop_tag(PT_BINARY, 0x002C)); +define('PR_RELATED_IPMS' ,mapi_prop_tag(PT_BINARY, 0x002D)); +define('PR_ORIGINAL_SENSITIVITY' ,mapi_prop_tag(PT_LONG, 0x002E)); +define('PR_LANGUAGES' ,mapi_prop_tag(PT_TSTRING, 0x002F)); +define('PR_LANGUAGES_W' ,mapi_prop_tag(PT_UNICODE, 0x002F)); +define('PR_LANGUAGES_A' ,mapi_prop_tag(PT_STRING8, 0x002F)); +define('PR_REPLY_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x0030)); +define('PR_REPORT_TAG' ,mapi_prop_tag(PT_BINARY, 0x0031)); +define('PR_REPORT_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x0032)); +define('PR_RETURNED_IPM' ,mapi_prop_tag(PT_BOOLEAN, 0x0033)); +define('PR_SECURITY' ,mapi_prop_tag(PT_LONG, 0x0034)); +define('PR_INCOMPLETE_COPY' ,mapi_prop_tag(PT_BOOLEAN, 0x0035)); +define('PR_SENSITIVITY' ,mapi_prop_tag(PT_LONG, 0x0036)); +define('PR_SUBJECT' ,mapi_prop_tag(PT_TSTRING, 0x0037)); +define('PR_SUBJECT_W' ,mapi_prop_tag(PT_UNICODE, 0x0037)); +define('PR_SUBJECT_A' ,mapi_prop_tag(PT_STRING8, 0x0037)); +define('PR_SUBJECT_IPM' ,mapi_prop_tag(PT_BINARY, 0x0038)); +define('PR_CLIENT_SUBMIT_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x0039)); +define('PR_REPORT_NAME' ,mapi_prop_tag(PT_TSTRING, 0x003A)); +define('PR_REPORT_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x003A)); +define('PR_REPORT_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x003A)); +define('PR_SENT_REPRESENTING_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x003B)); +define('PR_X400_CONTENT_TYPE' ,mapi_prop_tag(PT_BINARY, 0x003C)); +define('PR_SUBJECT_PREFIX' ,mapi_prop_tag(PT_TSTRING, 0x003D)); +define('PR_SUBJECT_PREFIX_W' ,mapi_prop_tag(PT_UNICODE, 0x003D)); +define('PR_SUBJECT_PREFIX_A' ,mapi_prop_tag(PT_STRING8, 0x003D)); +define('PR_NON_RECEIPT_REASON' ,mapi_prop_tag(PT_LONG, 0x003E)); +define('PR_RECEIVED_BY_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x003F)); +define('PR_RECEIVED_BY_NAME' ,mapi_prop_tag(PT_TSTRING, 0x0040)); +define('PR_RECEIVED_BY_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x0040)); +define('PR_RECEIVED_BY_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x0040)); +define('PR_SENT_REPRESENTING_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x0041)); +define('PR_SENT_REPRESENTING_NAME' ,mapi_prop_tag(PT_TSTRING, 0x0042)); +define('PR_SENT_REPRESENTING_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x0042)); +define('PR_SENT_REPRESENTING_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x0042)); +define('PR_RCVD_REPRESENTING_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x0043)); +define('PR_RCVD_REPRESENTING_NAME' ,mapi_prop_tag(PT_TSTRING, 0x0044)); +define('PR_RCVD_REPRESENTING_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x0044)); +define('PR_RCVD_REPRESENTING_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x0044)); +define('PR_REPORT_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x0045)); +define('PR_READ_RECEIPT_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x0046)); +define('PR_MESSAGE_SUBMISSION_ID' ,mapi_prop_tag(PT_BINARY, 0x0047)); +define('PR_PROVIDER_SUBMIT_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x0048)); +define('PR_ORIGINAL_SUBJECT' ,mapi_prop_tag(PT_TSTRING, 0x0049)); +define('PR_ORIGINAL_SUBJECT_W' ,mapi_prop_tag(PT_UNICODE, 0x0049)); +define('PR_ORIGINAL_SUBJECT_A' ,mapi_prop_tag(PT_STRING8, 0x0049)); +define('PR_DISC_VAL' ,mapi_prop_tag(PT_BOOLEAN, 0x004A)); +define('PR_ORIG_MESSAGE_CLASS' ,mapi_prop_tag(PT_TSTRING, 0x004B)); +define('PR_ORIG_MESSAGE_CLASS_W' ,mapi_prop_tag(PT_UNICODE, 0x004B)); +define('PR_ORIG_MESSAGE_CLASS_A' ,mapi_prop_tag(PT_STRING8, 0x004B)); +define('PR_ORIGINAL_AUTHOR_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x004C)); +define('PR_ORIGINAL_AUTHOR_NAME' ,mapi_prop_tag(PT_TSTRING, 0x004D)); +define('PR_ORIGINAL_AUTHOR_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x004D)); +define('PR_ORIGINAL_AUTHOR_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x004D)); +define('PR_ORIGINAL_SUBMIT_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x004E)); +define('PR_REPLY_RECIPIENT_ENTRIES' ,mapi_prop_tag(PT_BINARY, 0x004F)); +define('PR_REPLY_RECIPIENT_NAMES' ,mapi_prop_tag(PT_TSTRING, 0x0050)); +define('PR_REPLY_RECIPIENT_NAMES_W' ,mapi_prop_tag(PT_UNICODE, 0x0050)); +define('PR_REPLY_RECIPIENT_NAMES_A' ,mapi_prop_tag(PT_STRING8, 0x0050)); + +define('PR_RECEIVED_BY_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x0051)); +define('PR_RCVD_REPRESENTING_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x0052)); +define('PR_READ_RECEIPT_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x0053)); +define('PR_REPORT_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x0054)); +define('PR_ORIGINAL_DELIVERY_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x0055)); +define('PR_ORIGINAL_AUTHOR_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x0056)); + +define('PR_MESSAGE_TO_ME' ,mapi_prop_tag(PT_BOOLEAN, 0x0057)); +define('PR_MESSAGE_CC_ME' ,mapi_prop_tag(PT_BOOLEAN, 0x0058)); +define('PR_MESSAGE_RECIP_ME' ,mapi_prop_tag(PT_BOOLEAN, 0x0059)); + +define('PR_ORIGINAL_SENDER_NAME' ,mapi_prop_tag(PT_TSTRING, 0x005A)); +define('PR_ORIGINAL_SENDER_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x005A)); +define('PR_ORIGINAL_SENDER_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x005A)); +define('PR_ORIGINAL_SENDER_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x005B)); +define('PR_ORIGINAL_SENDER_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x005C)); +define('PR_ORIGINAL_SENT_REPRESENTING_NAME' ,mapi_prop_tag(PT_TSTRING, 0x005D)); +define('PR_ORIGINAL_SENT_REPRESENTING_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x005D)); +define('PR_ORIGINAL_SENT_REPRESENTING_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x005D)); +define('PR_ORIGINAL_SENT_REPRESENTING_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x005E)); +define('PR_ORIGINAL_SENT_REPRESENTING_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x005F)); + +define('PR_START_DATE' ,mapi_prop_tag(PT_SYSTIME, 0x0060)); +define('PR_END_DATE' ,mapi_prop_tag(PT_SYSTIME, 0x0061)); +define('PR_OWNER_APPT_ID' ,mapi_prop_tag(PT_LONG, 0x0062)); +define('PR_RESPONSE_REQUESTED' ,mapi_prop_tag(PT_BOOLEAN, 0x0063)); + +define('PR_SENT_REPRESENTING_ADDRTYPE' ,mapi_prop_tag(PT_TSTRING, 0x0064)); +define('PR_SENT_REPRESENTING_ADDRTYPE_W' ,mapi_prop_tag(PT_UNICODE, 0x0064)); +define('PR_SENT_REPRESENTING_ADDRTYPE_A' ,mapi_prop_tag(PT_STRING8, 0x0064)); +define('PR_SENT_REPRESENTING_EMAIL_ADDRESS' ,mapi_prop_tag(PT_TSTRING, 0x0065)); +define('PR_SENT_REPRESENTING_EMAIL_ADDRESS_W' ,mapi_prop_tag(PT_UNICODE, 0x0065)); +define('PR_SENT_REPRESENTING_EMAIL_ADDRESS_A' ,mapi_prop_tag(PT_STRING8, 0x0065)); + +define('PR_ORIGINAL_SENDER_ADDRTYPE' ,mapi_prop_tag(PT_TSTRING, 0x0066)); +define('PR_ORIGINAL_SENDER_ADDRTYPE_W' ,mapi_prop_tag(PT_UNICODE, 0x0066)); +define('PR_ORIGINAL_SENDER_ADDRTYPE_A' ,mapi_prop_tag(PT_STRING8, 0x0066)); +define('PR_ORIGINAL_SENDER_EMAIL_ADDRESS' ,mapi_prop_tag(PT_TSTRING, 0x0067)); +define('PR_ORIGINAL_SENDER_EMAIL_ADDRESS_W' ,mapi_prop_tag(PT_UNICODE, 0x0067)); +define('PR_ORIGINAL_SENDER_EMAIL_ADDRESS_A' ,mapi_prop_tag(PT_STRING8, 0x0067)); + +define('PR_ORIGINAL_SENT_REPRESENTING_ADDRTYPE' ,mapi_prop_tag(PT_TSTRING, 0x0068)); +define('PR_ORIGINAL_SENT_REPRESENTING_ADDRTYPE_W' ,mapi_prop_tag(PT_UNICODE, 0x0068)); +define('PR_ORIGINAL_SENT_REPRESENTING_ADDRTYPE_A' ,mapi_prop_tag(PT_STRING8, 0x0068)); +define('PR_ORIGINAL_SENT_REPRESENTING_EMAIL_ADDRESS' ,mapi_prop_tag(PT_TSTRING, 0x0069)); +define('PR_ORIGINAL_SENT_REPRESENTING_EMAIL_ADDRESS_W',mapi_prop_tag(PT_UNICODE, 0x0069)); +define('PR_ORIGINAL_SENT_REPRESENTING_EMAIL_ADDRESS_A',mapi_prop_tag(PT_STRING8, 0x0069)); + +define('PR_CONVERSATION_TOPIC' ,mapi_prop_tag(PT_TSTRING, 0x0070)); +define('PR_CONVERSATION_TOPIC_W' ,mapi_prop_tag(PT_UNICODE, 0x0070)); +define('PR_CONVERSATION_TOPIC_A' ,mapi_prop_tag(PT_STRING8, 0x0070)); +define('PR_CONVERSATION_INDEX' ,mapi_prop_tag(PT_BINARY, 0x0071)); + +define('PR_ORIGINAL_DISPLAY_BCC' ,mapi_prop_tag(PT_TSTRING, 0x0072)); +define('PR_ORIGINAL_DISPLAY_BCC_W' ,mapi_prop_tag(PT_UNICODE, 0x0072)); +define('PR_ORIGINAL_DISPLAY_BCC_A' ,mapi_prop_tag(PT_STRING8, 0x0072)); +define('PR_ORIGINAL_DISPLAY_CC' ,mapi_prop_tag(PT_TSTRING, 0x0073)); +define('PR_ORIGINAL_DISPLAY_CC_W' ,mapi_prop_tag(PT_UNICODE, 0x0073)); +define('PR_ORIGINAL_DISPLAY_CC_A' ,mapi_prop_tag(PT_STRING8, 0x0073)); +define('PR_ORIGINAL_DISPLAY_TO' ,mapi_prop_tag(PT_TSTRING, 0x0074)); +define('PR_ORIGINAL_DISPLAY_TO_W' ,mapi_prop_tag(PT_UNICODE, 0x0074)); +define('PR_ORIGINAL_DISPLAY_TO_A' ,mapi_prop_tag(PT_STRING8, 0x0074)); + +define('PR_RECEIVED_BY_ADDRTYPE' ,mapi_prop_tag(PT_TSTRING, 0x0075)); +define('PR_RECEIVED_BY_ADDRTYPE_W' ,mapi_prop_tag(PT_UNICODE, 0x0075)); +define('PR_RECEIVED_BY_ADDRTYPE_A' ,mapi_prop_tag(PT_STRING8, 0x0075)); +define('PR_RECEIVED_BY_EMAIL_ADDRESS' ,mapi_prop_tag(PT_TSTRING, 0x0076)); +define('PR_RECEIVED_BY_EMAIL_ADDRESS_W' ,mapi_prop_tag(PT_UNICODE, 0x0076)); +define('PR_RECEIVED_BY_EMAIL_ADDRESS_A' ,mapi_prop_tag(PT_STRING8, 0x0076)); + +define('PR_RCVD_REPRESENTING_ADDRTYPE' ,mapi_prop_tag(PT_TSTRING, 0x0077)); +define('PR_RCVD_REPRESENTING_ADDRTYPE_W' ,mapi_prop_tag(PT_UNICODE, 0x0077)); +define('PR_RCVD_REPRESENTING_ADDRTYPE_A' ,mapi_prop_tag(PT_STRING8, 0x0077)); +define('PR_RCVD_REPRESENTING_EMAIL_ADDRESS' ,mapi_prop_tag(PT_TSTRING, 0x0078)); +define('PR_RCVD_REPRESENTING_EMAIL_ADDRESS_W' ,mapi_prop_tag(PT_UNICODE, 0x0078)); +define('PR_RCVD_REPRESENTING_EMAIL_ADDRESS_A' ,mapi_prop_tag(PT_STRING8, 0x0078)); + +define('PR_ORIGINAL_AUTHOR_ADDRTYPE' ,mapi_prop_tag(PT_TSTRING, 0x0079)); +define('PR_ORIGINAL_AUTHOR_ADDRTYPE_W' ,mapi_prop_tag(PT_UNICODE, 0x0079)); +define('PR_ORIGINAL_AUTHOR_ADDRTYPE_A' ,mapi_prop_tag(PT_STRING8, 0x0079)); +define('PR_ORIGINAL_AUTHOR_EMAIL_ADDRESS' ,mapi_prop_tag(PT_TSTRING, 0x007A)); +define('PR_ORIGINAL_AUTHOR_EMAIL_ADDRESS_W' ,mapi_prop_tag(PT_UNICODE, 0x007A)); +define('PR_ORIGINAL_AUTHOR_EMAIL_ADDRESS_A' ,mapi_prop_tag(PT_STRING8, 0x007A)); + +define('PR_ORIGINALLY_INTENDED_RECIP_ADDRTYPE' ,mapi_prop_tag(PT_TSTRING, 0x007B)); +define('PR_ORIGINALLY_INTENDED_RECIP_ADDRTYPE_W' ,mapi_prop_tag(PT_UNICODE, 0x007B)); +define('PR_ORIGINALLY_INTENDED_RECIP_ADDRTYPE_A' ,mapi_prop_tag(PT_STRING8, 0x007B)); +define('PR_ORIGINALLY_INTENDED_RECIP_EMAIL_ADDRESS' ,mapi_prop_tag(PT_TSTRING, 0x007C)); +define('PR_ORIGINALLY_INTENDED_RECIP_EMAIL_ADDRESS_W' ,mapi_prop_tag(PT_UNICODE, 0x007C)); +define('PR_ORIGINALLY_INTENDED_RECIP_EMAIL_ADDRESS_A' ,mapi_prop_tag(PT_STRING8, 0x007C)); + +define('PR_TRANSPORT_MESSAGE_HEADERS' ,mapi_prop_tag(PT_TSTRING, 0x007D)); +define('PR_TRANSPORT_MESSAGE_HEADERS_W' ,mapi_prop_tag(PT_UNICODE, 0x007D)); +define('PR_TRANSPORT_MESSAGE_HEADERS_A' ,mapi_prop_tag(PT_STRING8, 0x007D)); + +define('PR_DELEGATION' ,mapi_prop_tag(PT_BINARY, 0x007E)); + +define('PR_TNEF_CORRELATION_KEY' ,mapi_prop_tag(PT_BINARY, 0x007F)); + +define('PR_MDN_DISPOSITION_TYPE' ,mapi_prop_tag(PT_STRING8, 0x0080)); +define('PR_MDN_DISPOSITION_SENDINGMODE' ,mapi_prop_tag(PT_STRING8, 0x0081)); + +define('PR_USER_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x6618+0x01)); +define('PR_USER_NAME' ,mapi_prop_tag(PT_STRING8, 0x6618+0x02)); +define('PR_MAILBOX_OWNER_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x6618+0x03)); +define('PR_MAILBOX_OWNER_NAME' ,mapi_prop_tag(PT_STRING8, 0x6618+0x04)); + +define('PR_HIERARCHY_SYNCHRONIZER' ,mapi_prop_tag(PT_OBJECT, 0x6618+0x14)); +define('PR_CONTENTS_SYNCHRONIZER' ,mapi_prop_tag(PT_OBJECT, 0x6618+0x15)); +define('PR_COLLECTOR' ,mapi_prop_tag(PT_OBJECT, 0x6618+0x16)); + +define('PR_SMTP_ADDRESS' ,mapi_prop_tag(PT_TSTRING, 0x39FE)); + + +/* + * Message content properties + */ + +define('PR_BODY' ,mapi_prop_tag(PT_TSTRING, 0x1000)); +define('PR_HTML' ,mapi_prop_tag(PT_BINARY, 0x1013)); +define('PR_BODY_W' ,mapi_prop_tag(PT_UNICODE, 0x1000)); +define('PR_BODY_A' ,mapi_prop_tag(PT_STRING8, 0x1000)); +define('PR_REPORT_TEXT' ,mapi_prop_tag(PT_TSTRING, 0x1001)); +define('PR_REPORT_TEXT_W' ,mapi_prop_tag(PT_UNICODE, 0x1001)); +define('PR_REPORT_TEXT_A' ,mapi_prop_tag(PT_STRING8, 0x1001)); +define('PR_ORIGINATOR_AND_DL_EXPANSION_HISTORY' ,mapi_prop_tag(PT_BINARY, 0x1002)); +define('PR_REPORTING_DL_NAME' ,mapi_prop_tag(PT_BINARY, 0x1003)); +define('PR_REPORTING_MTA_CERTIFICATE' ,mapi_prop_tag(PT_BINARY, 0x1004)); + +/* Removed 'PR_REPORT_ORIGIN_AUTHENTICATION_CHECK with DCR 3865, use 'PR_ORIGIN_CHECK */ + +define('PR_RTF_SYNC_BODY_CRC' ,mapi_prop_tag(PT_LONG, 0x1006)); +define('PR_RTF_SYNC_BODY_COUNT' ,mapi_prop_tag(PT_LONG, 0x1007)); +define('PR_RTF_SYNC_BODY_TAG' ,mapi_prop_tag(PT_TSTRING, 0x1008)); +define('PR_RTF_SYNC_BODY_TAG_W' ,mapi_prop_tag(PT_UNICODE, 0x1008)); +define('PR_RTF_SYNC_BODY_TAG_A' ,mapi_prop_tag(PT_STRING8, 0x1008)); +define('PR_RTF_COMPRESSED' ,mapi_prop_tag(PT_BINARY, 0x1009)); +define('PR_RTF_SYNC_PREFIX_COUNT' ,mapi_prop_tag(PT_LONG, 0x1010)); +define('PR_RTF_SYNC_TRAILING_COUNT' ,mapi_prop_tag(PT_LONG, 0x1011)); +define('PR_ORIGINALLY_INTENDED_RECIP_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x1012)); +define('PR_NATIVE_BODY_INFO' ,mapi_prop_tag(PT_LONG, 0x1016)); + +define('PR_CONFLICT_ITEMS' ,mapi_prop_tag(PT_MV_BINARY, 0x1098)); + +/* + * Reserved 0x1100-0x1200 + */ + + +/* + * Message recipient properties + */ + +define('PR_CONTENT_INTEGRITY_CHECK' ,mapi_prop_tag(PT_BINARY, 0x0C00)); +define('PR_EXPLICIT_CONVERSION' ,mapi_prop_tag(PT_LONG, 0x0C01)); +define('PR_IPM_RETURN_REQUESTED' ,mapi_prop_tag(PT_BOOLEAN, 0x0C02)); +define('PR_MESSAGE_TOKEN' ,mapi_prop_tag(PT_BINARY, 0x0C03)); +define('PR_NDR_REASON_CODE' ,mapi_prop_tag(PT_LONG, 0x0C04)); +define('PR_NDR_DIAG_CODE' ,mapi_prop_tag(PT_LONG, 0x0C05)); +define('PR_NON_RECEIPT_NOTIFICATION_REQUESTED' ,mapi_prop_tag(PT_BOOLEAN, 0x0C06)); +define('PR_DELIVERY_POINT' ,mapi_prop_tag(PT_LONG, 0x0C07)); + +define('PR_ORIGINATOR_NON_DELIVERY_REPORT_REQUESTED' ,mapi_prop_tag(PT_BOOLEAN, 0x0C08)); +define('PR_ORIGINATOR_REQUESTED_ALTERNATE_RECIPIENT' ,mapi_prop_tag(PT_BINARY, 0x0C09)); +define('PR_PHYSICAL_DELIVERY_BUREAU_FAX_DELIVERY' ,mapi_prop_tag(PT_BOOLEAN, 0x0C0A)); +define('PR_PHYSICAL_DELIVERY_MODE' ,mapi_prop_tag(PT_LONG, 0x0C0B)); +define('PR_PHYSICAL_DELIVERY_REPORT_REQUEST' ,mapi_prop_tag(PT_LONG, 0x0C0C)); +define('PR_PHYSICAL_FORWARDING_ADDRESS' ,mapi_prop_tag(PT_BINARY, 0x0C0D)); +define('PR_PHYSICAL_FORWARDING_ADDRESS_REQUESTED' ,mapi_prop_tag(PT_BOOLEAN, 0x0C0E)); +define('PR_PHYSICAL_FORWARDING_PROHIBITED' ,mapi_prop_tag(PT_BOOLEAN, 0x0C0F)); +define('PR_PHYSICAL_RENDITION_ATTRIBUTES' ,mapi_prop_tag(PT_BINARY, 0x0C10)); +define('PR_PROOF_OF_DELIVERY' ,mapi_prop_tag(PT_BINARY, 0x0C11)); +define('PR_PROOF_OF_DELIVERY_REQUESTED' ,mapi_prop_tag(PT_BOOLEAN, 0x0C12)); +define('PR_RECIPIENT_CERTIFICATE' ,mapi_prop_tag(PT_BINARY, 0x0C13)); +define('PR_RECIPIENT_NUMBER_FOR_ADVICE' ,mapi_prop_tag(PT_TSTRING, 0x0C14)); +define('PR_RECIPIENT_NUMBER_FOR_ADVICE_W' ,mapi_prop_tag(PT_UNICODE, 0x0C14)); +define('PR_RECIPIENT_NUMBER_FOR_ADVICE_A' ,mapi_prop_tag(PT_STRING8, 0x0C14)); +define('PR_RECIPIENT_TYPE' ,mapi_prop_tag(PT_LONG, 0x0C15)); +define('PR_REGISTERED_MAIL_TYPE' ,mapi_prop_tag(PT_LONG, 0x0C16)); +define('PR_REPLY_REQUESTED' ,mapi_prop_tag(PT_BOOLEAN, 0x0C17)); +define('PR_REQUESTED_DELIVERY_METHOD' ,mapi_prop_tag(PT_LONG, 0x0C18)); +define('PR_SENDER_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x0C19)); +define('PR_SENDER_NAME' ,mapi_prop_tag(PT_TSTRING, 0x0C1A)); +define('PR_SENDER_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x0C1A)); +define('PR_SENDER_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x0C1A)); +define('PR_SUPPLEMENTARY_INFO' ,mapi_prop_tag(PT_TSTRING, 0x0C1B)); +define('PR_SUPPLEMENTARY_INFO_W' ,mapi_prop_tag(PT_UNICODE, 0x0C1B)); +define('PR_SUPPLEMENTARY_INFO_A' ,mapi_prop_tag(PT_STRING8, 0x0C1B)); +define('PR_TYPE_OF_MTS_USER' ,mapi_prop_tag(PT_LONG, 0x0C1C)); +define('PR_SENDER_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x0C1D)); +define('PR_SENDER_ADDRTYPE' ,mapi_prop_tag(PT_TSTRING, 0x0C1E)); +define('PR_SENDER_ADDRTYPE_W' ,mapi_prop_tag(PT_UNICODE, 0x0C1E)); +define('PR_SENDER_ADDRTYPE_A' ,mapi_prop_tag(PT_STRING8, 0x0C1E)); +define('PR_SENDER_EMAIL_ADDRESS' ,mapi_prop_tag(PT_TSTRING, 0x0C1F)); +define('PR_SENDER_EMAIL_ADDRESS_W' ,mapi_prop_tag(PT_UNICODE, 0x0C1F)); +define('PR_SENDER_EMAIL_ADDRESS_A' ,mapi_prop_tag(PT_STRING8, 0x0C1F)); + +/* + * Message non-transmittable properties + */ + +/* + * The two tags, 'PR_MESSAGE_RECIPIENTS and 'PR_MESSAGE_ATTACHMENTS, + * are to be used in the exclude list passed to + * IMessage::CopyTo when the caller wants either the recipients or attachments + * of the message to not get copied. It is also used in the ProblemArray + * return from IMessage::CopyTo when an error is encountered copying them + */ + +define('PR_CURRENT_VERSION' ,mapi_prop_tag(PT_I8, 0x0E00)); +define('PR_DELETE_AFTER_SUBMIT' ,mapi_prop_tag(PT_BOOLEAN, 0x0E01)); +define('PR_DISPLAY_BCC' ,mapi_prop_tag(PT_TSTRING, 0x0E02)); +define('PR_DISPLAY_BCC_W' ,mapi_prop_tag(PT_UNICODE, 0x0E02)); +define('PR_DISPLAY_BCC_A' ,mapi_prop_tag(PT_STRING8, 0x0E02)); +define('PR_DISPLAY_CC' ,mapi_prop_tag(PT_TSTRING, 0x0E03)); +define('PR_DISPLAY_CC_W' ,mapi_prop_tag(PT_UNICODE, 0x0E03)); +define('PR_DISPLAY_CC_A' ,mapi_prop_tag(PT_STRING8, 0x0E03)); +define('PR_DISPLAY_TO' ,mapi_prop_tag(PT_TSTRING, 0x0E04)); +define('PR_DISPLAY_TO_W' ,mapi_prop_tag(PT_UNICODE, 0x0E04)); +define('PR_DISPLAY_TO_A' ,mapi_prop_tag(PT_STRING8, 0x0E04)); +define('PR_PARENT_DISPLAY' ,mapi_prop_tag(PT_TSTRING, 0x0E05)); +define('PR_PARENT_DISPLAY_W' ,mapi_prop_tag(PT_UNICODE, 0x0E05)); +define('PR_PARENT_DISPLAY_A' ,mapi_prop_tag(PT_STRING8, 0x0E05)); +define('PR_MESSAGE_DELIVERY_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x0E06)); +define('PR_MESSAGE_FLAGS' ,mapi_prop_tag(PT_LONG, 0x0E07)); +define('PR_MESSAGE_SIZE' ,mapi_prop_tag(PT_LONG, 0x0E08)); +define('PR_MESSAGE_SIZE_EXTENDED' ,mapi_prop_tag(PT_LONGLONG, 0x0E08)); +define('PR_PARENT_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x0E09)); +define('PR_SENTMAIL_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x0E0A)); +define('PR_CORRELATE' ,mapi_prop_tag(PT_BOOLEAN, 0x0E0C)); +define('PR_CORRELATE_MTSID' ,mapi_prop_tag(PT_BINARY, 0x0E0D)); +define('PR_DISCRETE_VALUES' ,mapi_prop_tag(PT_BOOLEAN, 0x0E0E)); +define('PR_RESPONSIBILITY' ,mapi_prop_tag(PT_BOOLEAN, 0x0E0F)); +define('PR_SPOOLER_STATUS' ,mapi_prop_tag(PT_LONG, 0x0E10)); +define('PR_TRANSPORT_STATUS' ,mapi_prop_tag(PT_LONG, 0x0E11)); +define('PR_MESSAGE_RECIPIENTS' ,mapi_prop_tag(PT_OBJECT, 0x0E12)); +define('PR_MESSAGE_ATTACHMENTS' ,mapi_prop_tag(PT_OBJECT, 0x0E13)); +define('PR_SUBMIT_FLAGS' ,mapi_prop_tag(PT_LONG, 0x0E14)); +define('PR_RECIPIENT_STATUS' ,mapi_prop_tag(PT_LONG, 0x0E15)); +define('PR_TRANSPORT_KEY' ,mapi_prop_tag(PT_LONG, 0x0E16)); +define('PR_MSG_STATUS' ,mapi_prop_tag(PT_LONG, 0x0E17)); +define('PR_MESSAGE_DOWNLOAD_TIME' ,mapi_prop_tag(PT_LONG, 0x0E18)); +define('PR_CREATION_VERSION' ,mapi_prop_tag(PT_I8, 0x0E19)); +define('PR_MODIFY_VERSION' ,mapi_prop_tag(PT_I8, 0x0E1A)); +define('PR_HASATTACH' ,mapi_prop_tag(PT_BOOLEAN, 0x0E1B)); +define('PR_BODY_CRC' ,mapi_prop_tag(PT_LONG, 0x0E1C)); +define('PR_NORMALIZED_SUBJECT' ,mapi_prop_tag(PT_TSTRING, 0x0E1D)); +define('PR_NORMALIZED_SUBJECT_W' ,mapi_prop_tag(PT_UNICODE, 0x0E1D)); +define('PR_NORMALIZED_SUBJECT_A' ,mapi_prop_tag(PT_STRING8, 0x0E1D)); +define('PR_RTF_IN_SYNC' ,mapi_prop_tag(PT_BOOLEAN, 0x0E1F)); +define('PR_ATTACH_SIZE' ,mapi_prop_tag(PT_LONG, 0x0E20)); +define('PR_ATTACH_NUM' ,mapi_prop_tag(PT_LONG, 0x0E21)); +define('PR_PREPROCESS' ,mapi_prop_tag(PT_BOOLEAN, 0x0E22)); + +/* 'PR_ORIGINAL_DISPLAY_TO, _CC, and _BCC moved to transmittible range 03/09/95 */ + +define('PR_ORIGINATING_MTA_CERTIFICATE' ,mapi_prop_tag(PT_BINARY, 0x0E25)); +define('PR_PROOF_OF_SUBMISSION' ,mapi_prop_tag(PT_BINARY, 0x0E26)); + + +/* + * The range of non-message and non-recipient property IDs (0x3000 - 0x3FFF)); is + * further broken down into ranges to make assigning new property IDs easier. + * + * From To Kind of property + * -------------------------------- + * 3000 32FF MAPI_defined common property + * 3200 33FF MAPI_defined form property + * 3400 35FF MAPI_defined message store property + * 3600 36FF MAPI_defined Folder or AB Container property + * 3700 38FF MAPI_defined attachment property + * 3900 39FF MAPI_defined address book property + * 3A00 3BFF MAPI_defined mailuser property + * 3C00 3CFF MAPI_defined DistList property + * 3D00 3DFF MAPI_defined Profile Section property + * 3E00 3EFF MAPI_defined Status property + * 3F00 3FFF MAPI_defined display table property + */ + +/* + * Properties common to numerous MAPI objects. + * + * Those properties that can appear on messages are in the + * non-transmittable range for messages. They start at the high + * end of that range and work down. + * + * Properties that never appear on messages are defined in the common + * property range (see above));. + */ + +/* + * properties that are common to multiple objects (including message objects)); + * -- these ids are in the non-transmittable range + */ + +define('PR_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x0FFF)); +define('PR_OBJECT_TYPE' ,mapi_prop_tag(PT_LONG, 0x0FFE)); +define('PR_ICON' ,mapi_prop_tag(PT_BINARY, 0x0FFD)); +define('PR_MINI_ICON' ,mapi_prop_tag(PT_BINARY, 0x0FFC)); +define('PR_STORE_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x0FFB)); +define('PR_STORE_RECORD_KEY' ,mapi_prop_tag(PT_BINARY, 0x0FFA)); +define('PR_RECORD_KEY' ,mapi_prop_tag(PT_BINARY, 0x0FF9)); +define('PR_MAPPING_SIGNATURE' ,mapi_prop_tag(PT_BINARY, 0x0FF8)); +define('PR_ACCESS_LEVEL' ,mapi_prop_tag(PT_LONG, 0x0FF7)); +define('PR_INSTANCE_KEY' ,mapi_prop_tag(PT_BINARY, 0x0FF6)); +define('PR_ROW_TYPE' ,mapi_prop_tag(PT_LONG, 0x0FF5)); +define('PR_ACCESS' ,mapi_prop_tag(PT_LONG, 0x0FF4)); + +/* + * properties that are common to multiple objects (usually not including message objects)); + * -- these ids are in the transmittable range + */ + +define('PR_ROWID' ,mapi_prop_tag(PT_LONG, 0x3000)); +define('PR_DISPLAY_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3001)); +define('PR_DISPLAY_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3001)); +define('PR_DISPLAY_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3001)); +define('PR_ADDRTYPE' ,mapi_prop_tag(PT_TSTRING, 0x3002)); +define('PR_ADDRTYPE_W' ,mapi_prop_tag(PT_UNICODE, 0x3002)); +define('PR_ADDRTYPE_A' ,mapi_prop_tag(PT_STRING8, 0x3002)); +define('PR_EMAIL_ADDRESS' ,mapi_prop_tag(PT_TSTRING, 0x3003)); +define('PR_EMAIL_ADDRESS_W' ,mapi_prop_tag(PT_UNICODE, 0x3003)); +define('PR_EMAIL_ADDRESS_A' ,mapi_prop_tag(PT_STRING8, 0x3003)); +define('PR_COMMENT' ,mapi_prop_tag(PT_TSTRING, 0x3004)); +define('PR_COMMENT_W' ,mapi_prop_tag(PT_UNICODE, 0x3004)); +define('PR_COMMENT_A' ,mapi_prop_tag(PT_STRING8, 0x3004)); +define('PR_DEPTH' ,mapi_prop_tag(PT_LONG, 0x3005)); +define('PR_PROVIDER_DISPLAY' ,mapi_prop_tag(PT_TSTRING, 0x3006)); +define('PR_PROVIDER_DISPLAY_W' ,mapi_prop_tag(PT_UNICODE, 0x3006)); +define('PR_PROVIDER_DISPLAY_A' ,mapi_prop_tag(PT_STRING8, 0x3006)); +define('PR_CREATION_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x3007)); +define('PR_LAST_MODIFICATION_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x3008)); +define('PR_RESOURCE_FLAGS' ,mapi_prop_tag(PT_LONG, 0x3009)); +define('PR_PROVIDER_DLL_NAME' ,mapi_prop_tag(PT_TSTRING, 0x300A)); +define('PR_PROVIDER_DLL_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x300A)); +define('PR_PROVIDER_DLL_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x300A)); +define('PR_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x300B)); +define('PR_PROVIDER_UID' ,mapi_prop_tag(PT_BINARY, 0x300C)); +define('PR_PROVIDER_ORDINAL' ,mapi_prop_tag(PT_LONG, 0x300D)); + +/* + * MAPI Form properties + */ +define('PR_FORM_VERSION' ,mapi_prop_tag(PT_TSTRING, 0x3301)); +define('PR_FORM_VERSION_W' ,mapi_prop_tag(PT_UNICODE, 0x3301)); +define('PR_FORM_VERSION_A' ,mapi_prop_tag(PT_STRING8, 0x3301)); +define('PR_FORM_CLSID' ,mapi_prop_tag(PT_CLSID, 0x3302)); +define('PR_FORM_CONTACT_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3303)); +define('PR_FORM_CONTACT_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3303)); +define('PR_FORM_CONTACT_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3303)); +define('PR_FORM_CATEGORY' ,mapi_prop_tag(PT_TSTRING, 0x3304)); +define('PR_FORM_CATEGORY_W' ,mapi_prop_tag(PT_UNICODE, 0x3304)); +define('PR_FORM_CATEGORY_A' ,mapi_prop_tag(PT_STRING8, 0x3304)); +define('PR_FORM_CATEGORY_SUB' ,mapi_prop_tag(PT_TSTRING, 0x3305)); +define('PR_FORM_CATEGORY_SUB_W' ,mapi_prop_tag(PT_UNICODE, 0x3305)); +define('PR_FORM_CATEGORY_SUB_A' ,mapi_prop_tag(PT_STRING8, 0x3305)); +define('PR_FORM_HOST_MAP' ,mapi_prop_tag(PT_MV_LONG, 0x3306)); +define('PR_FORM_HIDDEN' ,mapi_prop_tag(PT_BOOLEAN, 0x3307)); +define('PR_FORM_DESIGNER_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3308)); +define('PR_FORM_DESIGNER_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3308)); +define('PR_FORM_DESIGNER_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3308)); +define('PR_FORM_DESIGNER_GUID' ,mapi_prop_tag(PT_CLSID, 0x3309)); +define('PR_FORM_MESSAGE_BEHAVIOR' ,mapi_prop_tag(PT_LONG, 0x330A)); + +/* + * Message store properties + */ + +define('PR_DEFAULT_STORE' ,mapi_prop_tag(PT_BOOLEAN, 0x3400)); +define('PR_STORE_SUPPORT_MASK' ,mapi_prop_tag(PT_LONG, 0x340D)); +define('PR_STORE_STATE' ,mapi_prop_tag(PT_LONG, 0x340E)); + +define('PR_IPM_SUBTREE_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x3410)); +define('PR_IPM_OUTBOX_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x3411)); +define('PR_IPM_WASTEBASKET_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x3412)); +define('PR_IPM_SENTMAIL_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x3413)); +define('PR_MDB_PROVIDER' ,mapi_prop_tag(PT_BINARY, 0x3414)); +define('PR_RECEIVE_FOLDER_SETTINGS' ,mapi_prop_tag(PT_OBJECT, 0x3415)); + +define('PR_VALID_FOLDER_MASK' ,mapi_prop_tag(PT_LONG, 0x35DF)); +define('PR_IPM_SUBTREE_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x35E0)); + +define('PR_IPM_OUTBOX_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x35E2)); +define('PR_IPM_WASTEBASKET_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x35E3)); +define('PR_IPM_SENTMAIL_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x35E4)); +define('PR_VIEWS_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x35E5)); +define('PR_COMMON_VIEWS_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x35E6)); +define('PR_FINDER_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x35E7)); +define('PR_IPM_FAVORITES_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x6630)); +define('PR_IPM_PUBLIC_FOLDERS_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x6631)); + + +/* Proptags 0x35E8-0x35FF reserved for folders "guaranteed" by 'PR_VALID_FOLDER_MASK */ + + +/* + * Folder and AB Container properties + */ + +define('PR_CONTAINER_FLAGS' ,mapi_prop_tag(PT_LONG, 0x3600)); +define('PR_FOLDER_TYPE' ,mapi_prop_tag(PT_LONG, 0x3601)); +define('PR_CONTENT_COUNT' ,mapi_prop_tag(PT_LONG, 0x3602)); +define('PR_CONTENT_UNREAD' ,mapi_prop_tag(PT_LONG, 0x3603)); +define('PR_CREATE_TEMPLATES' ,mapi_prop_tag(PT_OBJECT, 0x3604)); +define('PR_DETAILS_TABLE' ,mapi_prop_tag(PT_OBJECT, 0x3605)); +define('PR_SEARCH' ,mapi_prop_tag(PT_OBJECT, 0x3607)); +define('PR_SELECTABLE' ,mapi_prop_tag(PT_BOOLEAN, 0x3609)); +define('PR_SUBFOLDERS' ,mapi_prop_tag(PT_BOOLEAN, 0x360A)); +define('PR_STATUS' ,mapi_prop_tag(PT_LONG, 0x360B)); +define('PR_ANR' ,mapi_prop_tag(PT_TSTRING, 0x360C)); +define('PR_ANR_W' ,mapi_prop_tag(PT_UNICODE, 0x360C)); +define('PR_ANR_A' ,mapi_prop_tag(PT_STRING8, 0x360C)); +define('PR_CONTENTS_SORT_ORDER' ,mapi_prop_tag(PT_MV_LONG, 0x360D)); +define('PR_CONTAINER_HIERARCHY' ,mapi_prop_tag(PT_OBJECT, 0x360E)); +define('PR_CONTAINER_CONTENTS' ,mapi_prop_tag(PT_OBJECT, 0x360F)); +define('PR_FOLDER_ASSOCIATED_CONTENTS' ,mapi_prop_tag(PT_OBJECT, 0x3610)); +define('PR_DEF_CREATE_DL' ,mapi_prop_tag(PT_BINARY, 0x3611)); +define('PR_DEF_CREATE_MAILUSER' ,mapi_prop_tag(PT_BINARY, 0x3612)); +define('PR_CONTAINER_CLASS' ,mapi_prop_tag(PT_TSTRING, 0x3613)); +define('PR_CONTAINER_CLASS_W' ,mapi_prop_tag(PT_UNICODE, 0x3613)); +define('PR_CONTAINER_CLASS_A' ,mapi_prop_tag(PT_STRING8, 0x3613)); +define('PR_CONTAINER_MODIFY_VERSION' ,mapi_prop_tag(PT_I8, 0x3614)); +define('PR_AB_PROVIDER_ID' ,mapi_prop_tag(PT_BINARY, 0x3615)); +define('PR_DEFAULT_VIEW_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x3616)); +define('PR_ASSOC_CONTENT_COUNT' ,mapi_prop_tag(PT_LONG, 0x3617)); +define('PR_EXTENDED_FOLDER_FLAGS' ,mapi_prop_tag(PT_BINARY, 0x36DA)); + +define('PR_RIGHTS' ,mapi_prop_tag(PT_LONG, 0x6639)); + +/* Reserved 0x36C0-0x36FF */ + +/* + * Attachment properties + */ + +define('PR_ATTACHMENT_X400_PARAMETERS' ,mapi_prop_tag(PT_BINARY, 0x3700)); +define('PR_ATTACH_DATA_OBJ' ,mapi_prop_tag(PT_OBJECT, 0x3701)); +define('PR_ATTACH_DATA_BIN' ,mapi_prop_tag(PT_BINARY, 0x3701)); +define('PR_ATTACH_CONTENT_ID' ,mapi_prop_tag(PT_STRING8, 0x3712)); +define('PR_ATTACH_CONTENT_ID_W' ,mapi_prop_tag(PT_UNICODE, 0x3712)); +define('PR_ATTACH_CONTENT_LOCATION' ,mapi_prop_tag(PT_STRING8, 0x3713)); +define('PR_ATTACH_ENCODING' ,mapi_prop_tag(PT_BINARY, 0x3702)); +define('PR_ATTACH_EXTENSION' ,mapi_prop_tag(PT_TSTRING, 0x3703)); +define('PR_ATTACH_EXTENSION_W' ,mapi_prop_tag(PT_UNICODE, 0x3703)); +define('PR_ATTACH_EXTENSION_A' ,mapi_prop_tag(PT_STRING8, 0x3703)); +define('PR_ATTACH_FILENAME' ,mapi_prop_tag(PT_TSTRING, 0x3704)); +define('PR_ATTACH_FILENAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3704)); +define('PR_ATTACH_FILENAME_A' ,mapi_prop_tag(PT_STRING8, 0x3704)); +define('PR_ATTACH_METHOD' ,mapi_prop_tag(PT_LONG, 0x3705)); +define('PR_ATTACH_LONG_FILENAME' ,mapi_prop_tag(PT_TSTRING, 0x3707)); +define('PR_ATTACH_LONG_FILENAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3707)); +define('PR_ATTACH_LONG_FILENAME_A' ,mapi_prop_tag(PT_STRING8, 0x3707)); +define('PR_ATTACH_PATHNAME' ,mapi_prop_tag(PT_TSTRING, 0x3708)); +define('PR_ATTACH_PATHNAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3708)); +define('PR_ATTACH_PATHNAME_A' ,mapi_prop_tag(PT_STRING8, 0x3708)); +define('PR_ATTACH_RENDERING' ,mapi_prop_tag(PT_BINARY, 0x3709)); +define('PR_ATTACH_TAG' ,mapi_prop_tag(PT_BINARY, 0x370A)); +define('PR_RENDERING_POSITION' ,mapi_prop_tag(PT_LONG, 0x370B)); +define('PR_ATTACH_TRANSPORT_NAME' ,mapi_prop_tag(PT_TSTRING, 0x370C)); +define('PR_ATTACH_TRANSPORT_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x370C)); +define('PR_ATTACH_TRANSPORT_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x370C)); +define('PR_ATTACH_LONG_PATHNAME' ,mapi_prop_tag(PT_TSTRING, 0x370D)); +define('PR_ATTACH_LONG_PATHNAME_W' ,mapi_prop_tag(PT_UNICODE, 0x370D)); +define('PR_ATTACH_LONG_PATHNAME_A' ,mapi_prop_tag(PT_STRING8, 0x370D)); +define('PR_ATTACH_MIME_TAG' ,mapi_prop_tag(PT_TSTRING, 0x370E)); +define('PR_ATTACH_MIME_TAG_W' ,mapi_prop_tag(PT_UNICODE, 0x370E)); +define('PR_ATTACH_MIME_TAG_A' ,mapi_prop_tag(PT_STRING8, 0x370E)); +define('PR_ATTACH_ADDITIONAL_INFO' ,mapi_prop_tag(PT_BINARY, 0x370F)); +define('PR_ATTACHMENT_FLAGS' ,mapi_prop_tag(PT_LONG, 0x7FFD)); +define('PR_ATTACHMENT_HIDDEN' ,mapi_prop_tag(PT_BOOLEAN, 0x7FFE)); +define('PR_ATTACHMENT_LINKID' ,mapi_prop_tag(PT_LONG, 0x7FFA)); +define('PR_ATTACH_FLAGS' ,mapi_prop_tag(PT_LONG, 0x3714)); +define('PR_EXCEPTION_STARTTIME' ,mapi_prop_tag(PT_SYSTIME, 0x7FFB)); +define('PR_EXCEPTION_ENDTIME' ,mapi_prop_tag(PT_SYSTIME, 0x7FFC)); + +/* + * AB Object properties + */ + +define('PR_DISPLAY_TYPE' ,mapi_prop_tag(PT_LONG, 0x3900)); +define('PR_DISPLAY_TYPE_EX' ,mapi_prop_tag(PT_LONG, 0x3905)); +define('PR_TEMPLATEID' ,mapi_prop_tag(PT_BINARY, 0x3902)); +define('PR_PRIMARY_CAPABILITY' ,mapi_prop_tag(PT_BINARY, 0x3904)); + + +/* + * Mail user properties + */ +define('PR_7BIT_DISPLAY_NAME' ,mapi_prop_tag(PT_STRING8, 0x39FF)); +define('PR_ACCOUNT' ,mapi_prop_tag(PT_TSTRING, 0x3A00)); +define('PR_ACCOUNT_W' ,mapi_prop_tag(PT_UNICODE, 0x3A00)); +define('PR_ACCOUNT_A' ,mapi_prop_tag(PT_STRING8, 0x3A00)); +define('PR_ALTERNATE_RECIPIENT' ,mapi_prop_tag(PT_BINARY, 0x3A01)); +define('PR_CALLBACK_TELEPHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A02)); +define('PR_CALLBACK_TELEPHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A02)); +define('PR_CALLBACK_TELEPHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A02)); +define('PR_CONVERSION_PROHIBITED' ,mapi_prop_tag(PT_BOOLEAN, 0x3A03)); +define('PR_DISCLOSE_RECIPIENTS' ,mapi_prop_tag(PT_BOOLEAN, 0x3A04)); +define('PR_GENERATION' ,mapi_prop_tag(PT_TSTRING, 0x3A05)); +define('PR_GENERATION_W' ,mapi_prop_tag(PT_UNICODE, 0x3A05)); +define('PR_GENERATION_A' ,mapi_prop_tag(PT_STRING8, 0x3A05)); +define('PR_GIVEN_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3A06)); +define('PR_GIVEN_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3A06)); +define('PR_GIVEN_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3A06)); +define('PR_GOVERNMENT_ID_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A07)); +define('PR_GOVERNMENT_ID_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A07)); +define('PR_GOVERNMENT_ID_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A07)); +define('PR_BUSINESS_TELEPHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A08)); +define('PR_BUSINESS_TELEPHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A08)); +define('PR_BUSINESS_TELEPHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A08)); +define('PR_OFFICE_TELEPHONE_NUMBER' ,PR_BUSINESS_TELEPHONE_NUMBER); +define('PR_OFFICE_TELEPHONE_NUMBER_W' ,PR_BUSINESS_TELEPHONE_NUMBER_W); +define('PR_OFFICE_TELEPHONE_NUMBER_A' ,PR_BUSINESS_TELEPHONE_NUMBER_A); +define('PR_HOME_TELEPHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A09)); +define('PR_HOME_TELEPHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A09)); +define('PR_HOME_TELEPHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A09)); +define('PR_INITIALS' ,mapi_prop_tag(PT_TSTRING, 0x3A0A)); +define('PR_INITIALS_W' ,mapi_prop_tag(PT_UNICODE, 0x3A0A)); +define('PR_INITIALS_A' ,mapi_prop_tag(PT_STRING8, 0x3A0A)); +define('PR_KEYWORD' ,mapi_prop_tag(PT_TSTRING, 0x3A0B)); +define('PR_KEYWORD_W' ,mapi_prop_tag(PT_UNICODE, 0x3A0B)); +define('PR_KEYWORD_A' ,mapi_prop_tag(PT_STRING8, 0x3A0B)); +define('PR_LANGUAGE' ,mapi_prop_tag(PT_TSTRING, 0x3A0C)); +define('PR_LANGUAGE_W' ,mapi_prop_tag(PT_UNICODE, 0x3A0C)); +define('PR_LANGUAGE_A' ,mapi_prop_tag(PT_STRING8, 0x3A0C)); +define('PR_LOCATION' ,mapi_prop_tag(PT_TSTRING, 0x3A0D)); +define('PR_LOCATION_W' ,mapi_prop_tag(PT_UNICODE, 0x3A0D)); +define('PR_LOCATION_A' ,mapi_prop_tag(PT_STRING8, 0x3A0D)); +define('PR_MAIL_PERMISSION' ,mapi_prop_tag(PT_BOOLEAN, 0x3A0E)); +define('PR_MHS_COMMON_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3A0F)); +define('PR_MHS_COMMON_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3A0F)); +define('PR_MHS_COMMON_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3A0F)); +define('PR_ORGANIZATIONAL_ID_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A10)); +define('PR_ORGANIZATIONAL_ID_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A10)); +define('PR_ORGANIZATIONAL_ID_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A10)); +define('PR_SURNAME' ,mapi_prop_tag(PT_TSTRING, 0x3A11)); +define('PR_SURNAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3A11)); +define('PR_SURNAME_A' ,mapi_prop_tag(PT_STRING8, 0x3A11)); +define('PR_ORIGINAL_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x3A12)); +define('PR_ORIGINAL_DISPLAY_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3A13)); +define('PR_ORIGINAL_DISPLAY_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3A13)); +define('PR_ORIGINAL_DISPLAY_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3A13)); +define('PR_ORIGINAL_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x3A14)); +define('PR_POSTAL_ADDRESS' ,mapi_prop_tag(PT_TSTRING, 0x3A15)); +define('PR_POSTAL_ADDRESS_W' ,mapi_prop_tag(PT_UNICODE, 0x3A15)); +define('PR_POSTAL_ADDRESS_A' ,mapi_prop_tag(PT_STRING8, 0x3A15)); +define('PR_COMPANY_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3A16)); +define('PR_COMPANY_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3A16)); +define('PR_COMPANY_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3A16)); +define('PR_TITLE' ,mapi_prop_tag(PT_TSTRING, 0x3A17)); +define('PR_TITLE_W' ,mapi_prop_tag(PT_UNICODE, 0x3A17)); +define('PR_TITLE_A' ,mapi_prop_tag(PT_STRING8, 0x3A17)); +define('PR_DEPARTMENT_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3A18)); +define('PR_DEPARTMENT_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3A18)); +define('PR_DEPARTMENT_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3A18)); +define('PR_OFFICE_LOCATION' ,mapi_prop_tag(PT_TSTRING, 0x3A19)); +define('PR_OFFICE_LOCATION_W' ,mapi_prop_tag(PT_UNICODE, 0x3A19)); +define('PR_OFFICE_LOCATION_A' ,mapi_prop_tag(PT_STRING8, 0x3A19)); +define('PR_PRIMARY_TELEPHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A1A)); +define('PR_PRIMARY_TELEPHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A1A)); +define('PR_PRIMARY_TELEPHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A1A)); +define('PR_BUSINESS2_TELEPHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A1B)); +define('PR_BUSINESS2_TELEPHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A1B)); +define('PR_BUSINESS2_TELEPHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A1B)); +define('PR_OFFICE2_TELEPHONE_NUMBER' ,PR_BUSINESS2_TELEPHONE_NUMBER); +define('PR_OFFICE2_TELEPHONE_NUMBER_W' ,PR_BUSINESS2_TELEPHONE_NUMBER_W); +define('PR_OFFICE2_TELEPHONE_NUMBER_A' ,PR_BUSINESS2_TELEPHONE_NUMBER_A); +define('PR_MOBILE_TELEPHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A1C)); +define('PR_MOBILE_TELEPHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A1C)); +define('PR_MOBILE_TELEPHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A1C)); +define('PR_CELLULAR_TELEPHONE_NUMBER' ,PR_MOBILE_TELEPHONE_NUMBER); +define('PR_CELLULAR_TELEPHONE_NUMBER_W' ,PR_MOBILE_TELEPHONE_NUMBER_W); +define('PR_CELLULAR_TELEPHONE_NUMBER_A' ,PR_MOBILE_TELEPHONE_NUMBER_A); +define('PR_RADIO_TELEPHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A1D)); +define('PR_RADIO_TELEPHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A1D)); +define('PR_RADIO_TELEPHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A1D)); +define('PR_CAR_TELEPHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A1E)); +define('PR_CAR_TELEPHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A1E)); +define('PR_CAR_TELEPHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A1E)); +define('PR_OTHER_TELEPHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A1F)); +define('PR_OTHER_TELEPHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A1F)); +define('PR_OTHER_TELEPHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A1F)); +define('PR_TRANSMITABLE_DISPLAY_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3A20)); +define('PR_TRANSMITABLE_DISPLAY_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3A20)); +define('PR_TRANSMITABLE_DISPLAY_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3A20)); +define('PR_PAGER_TELEPHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A21)); +define('PR_PAGER_TELEPHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A21)); +define('PR_PAGER_TELEPHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A21)); +define('PR_BEEPER_TELEPHONE_NUMBER' ,PR_PAGER_TELEPHONE_NUMBER); +define('PR_BEEPER_TELEPHONE_NUMBER_W' ,PR_PAGER_TELEPHONE_NUMBER_W); +define('PR_BEEPER_TELEPHONE_NUMBER_A' ,PR_PAGER_TELEPHONE_NUMBER_A); +define('PR_USER_CERTIFICATE' ,mapi_prop_tag(PT_BINARY, 0x3A22)); +define('PR_PRIMARY_FAX_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A23)); +define('PR_PRIMARY_FAX_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A23)); +define('PR_PRIMARY_FAX_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A23)); +define('PR_BUSINESS_FAX_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A24)); +define('PR_BUSINESS_FAX_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A24)); +define('PR_BUSINESS_FAX_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A24)); +define('PR_HOME_FAX_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A25)); +define('PR_HOME_FAX_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A25)); +define('PR_HOME_FAX_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A25)); +define('PR_COUNTRY' ,mapi_prop_tag(PT_TSTRING, 0x3A26)); +define('PR_COUNTRY_W' ,mapi_prop_tag(PT_UNICODE, 0x3A26)); +define('PR_COUNTRY_A' ,mapi_prop_tag(PT_STRING8, 0x3A26)); +define('PR_BUSINESS_ADDRESS_COUNTRY' ,PR_COUNTRY); +define('PR_BUSINESS_ADDRESS_COUNTRY_W' ,PR_COUNTRY_W); +define('PR_BUSINESS_ADDRESS_COUNTRY_A' ,PR_COUNTRY_A); + +define('PR_FLAG_STATUS' ,mapi_prop_tag(PT_LONG, 0x1090)); +define('PR_FLAG_COMPLETE_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x1091)); +define('PR_FLAG_ICON' ,mapi_prop_tag(PT_LONG, 0x1095)); +define('PR_BLOCK_STATUS' ,mapi_prop_tag(PT_LONG, 0x1096)); + +define('PR_LOCALITY' ,mapi_prop_tag(PT_TSTRING, 0x3A27)); +define('PR_LOCALITY_W' ,mapi_prop_tag(PT_UNICODE, 0x3A27)); +define('PR_LOCALITY_A' ,mapi_prop_tag(PT_STRING8, 0x3A27)); +define('PR_BUSINESS_ADDRESS_CITY' ,PR_LOCALITY); +define('PR_BUSINESS_ADDRESS_CITY_W' ,PR_LOCALITY_W); +define('PR_BUSINESS_ADDRESS_CITY_A' ,PR_LOCALITY_A); + +define('PR_STATE_OR_PROVINCE' ,mapi_prop_tag(PT_TSTRING, 0x3A28)); +define('PR_STATE_OR_PROVINCE_W' ,mapi_prop_tag(PT_UNICODE, 0x3A28)); +define('PR_STATE_OR_PROVINCE_A' ,mapi_prop_tag(PT_STRING8, 0x3A28)); +define('PR_BUSINESS_ADDRESS_STATE_OR_PROVINCE' ,PR_STATE_OR_PROVINCE); +define('PR_BUSINESS_ADDRESS_STATE_OR_PROVINCE_W' ,PR_STATE_OR_PROVINCE_W); +define('PR_BUSINESS_ADDRESS_STATE_OR_PROVINCE_A' ,PR_STATE_OR_PROVINCE_A); + +define('PR_STREET_ADDRESS' ,mapi_prop_tag(PT_TSTRING, 0x3A29)); +define('PR_STREET_ADDRESS_W' ,mapi_prop_tag(PT_UNICODE, 0x3A29)); +define('PR_STREET_ADDRESS_A' ,mapi_prop_tag(PT_STRING8, 0x3A29)); +define('PR_BUSINESS_ADDRESS_STREET' ,PR_STREET_ADDRESS); +define('PR_BUSINESS_ADDRESS_STREET_W' ,PR_STREET_ADDRESS_W); +define('PR_BUSINESS_ADDRESS_STREET_A' ,PR_STREET_ADDRESS_A); + +define('PR_POSTAL_CODE' ,mapi_prop_tag(PT_TSTRING, 0x3A2A)); +define('PR_POSTAL_CODE_W' ,mapi_prop_tag(PT_UNICODE, 0x3A2A)); +define('PR_POSTAL_CODE_A' ,mapi_prop_tag(PT_STRING8, 0x3A2A)); +define('PR_BUSINESS_ADDRESS_POSTAL_CODE' ,PR_POSTAL_CODE); +define('PR_BUSINESS_ADDRESS_POSTAL_CODE_W' ,PR_POSTAL_CODE_W); +define('PR_BUSINESS_ADDRESS_POSTAL_CODE_A' ,PR_POSTAL_CODE_A); + + +define('PR_POST_OFFICE_BOX' ,mapi_prop_tag(PT_TSTRING, 0x3A2B)); +define('PR_POST_OFFICE_BOX_W' ,mapi_prop_tag(PT_UNICODE, 0x3A2B)); +define('PR_POST_OFFICE_BOX_A' ,mapi_prop_tag(PT_STRING8, 0x3A2B)); +define('PR_BUSINESS_ADDRESS_POST_OFFICE_BOX' ,PR_POST_OFFICE_BOX); +define('PR_BUSINESS_ADDRESS_POST_OFFICE_BOX_W' ,PR_POST_OFFICE_BOX_W); +define('PR_BUSINESS_ADDRESS_POST_OFFICE_BOX_A' ,PR_POST_OFFICE_BOX_A); + + +define('PR_TELEX_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A2C)); +define('PR_TELEX_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A2C)); +define('PR_TELEX_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A2C)); +define('PR_ISDN_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A2D)); +define('PR_ISDN_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A2D)); +define('PR_ISDN_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A2D)); +define('PR_ASSISTANT_TELEPHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A2E)); +define('PR_ASSISTANT_TELEPHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A2E)); +define('PR_ASSISTANT_TELEPHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A2E)); +define('PR_HOME2_TELEPHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A2F)); +define('PR_HOME2_TELEPHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A2F)); +define('PR_HOME2_TELEPHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A2F)); +define('PR_ASSISTANT' ,mapi_prop_tag(PT_TSTRING, 0x3A30)); +define('PR_ASSISTANT_W' ,mapi_prop_tag(PT_UNICODE, 0x3A30)); +define('PR_ASSISTANT_A' ,mapi_prop_tag(PT_STRING8, 0x3A30)); +define('PR_SEND_RICH_INFO' ,mapi_prop_tag(PT_BOOLEAN, 0x3A40)); +define('PR_WEDDING_ANNIVERSARY' ,mapi_prop_tag(PT_SYSTIME, 0x3A41)); +define('PR_BIRTHDAY' ,mapi_prop_tag(PT_SYSTIME, 0x3A42)); + + +define('PR_HOBBIES' ,mapi_prop_tag(PT_TSTRING, 0x3A43)); +define('PR_HOBBIES_W' ,mapi_prop_tag(PT_UNICODE, 0x3A43)); +define('PR_HOBBIES_A' ,mapi_prop_tag(PT_STRING8, 0x3A43)); + +define('PR_MIDDLE_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3A44)); +define('PR_MIDDLE_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3A44)); +define('PR_MIDDLE_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3A44)); + +define('PR_DISPLAY_NAME_PREFIX' ,mapi_prop_tag(PT_TSTRING, 0x3A45)); +define('PR_DISPLAY_NAME_PREFIX_W' ,mapi_prop_tag(PT_UNICODE, 0x3A45)); +define('PR_DISPLAY_NAME_PREFIX_A' ,mapi_prop_tag(PT_STRING8, 0x3A45)); + +define('PR_PROFESSION' ,mapi_prop_tag(PT_TSTRING, 0x3A46)); +define('PR_PROFESSION_W' ,mapi_prop_tag(PT_UNICODE, 0x3A46)); +define('PR_PROFESSION_A' ,mapi_prop_tag(PT_STRING8, 0x3A46)); + +define('PR_PREFERRED_BY_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3A47)); +define('PR_PREFERRED_BY_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3A47)); +define('PR_PREFERRED_BY_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3A47)); + +define('PR_SPOUSE_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3A48)); +define('PR_SPOUSE_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3A48)); +define('PR_SPOUSE_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3A48)); + +define('PR_COMPUTER_NETWORK_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3A49)); +define('PR_COMPUTER_NETWORK_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3A49)); +define('PR_COMPUTER_NETWORK_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3A49)); + +define('PR_CUSTOMER_ID' ,mapi_prop_tag(PT_TSTRING, 0x3A4A)); +define('PR_CUSTOMER_ID_W' ,mapi_prop_tag(PT_UNICODE, 0x3A4A)); +define('PR_CUSTOMER_ID_A' ,mapi_prop_tag(PT_STRING8, 0x3A4A)); + +define('PR_TTYTDD_PHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A4B)); +define('PR_TTYTDD_PHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A4B)); +define('PR_TTYTDD_PHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A4B)); + +define('PR_FTP_SITE' ,mapi_prop_tag(PT_TSTRING, 0x3A4C)); +define('PR_FTP_SITE_W' ,mapi_prop_tag(PT_UNICODE, 0x3A4C)); +define('PR_FTP_SITE_A' ,mapi_prop_tag(PT_STRING8, 0x3A4C)); + +define('PR_GENDER' ,mapi_prop_tag(PT_SHORT, 0x3A4D)); + +define('PR_MANAGER_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3A4E)); +define('PR_MANAGER_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3A4E)); +define('PR_MANAGER_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3A4E)); + +define('PR_NICKNAME' ,mapi_prop_tag(PT_TSTRING, 0x3A4F)); +define('PR_NICKNAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3A4F)); +define('PR_NICKNAME_A' ,mapi_prop_tag(PT_STRING8, 0x3A4F)); + +define('PR_PERSONAL_HOME_PAGE' ,mapi_prop_tag(PT_TSTRING, 0x3A50)); +define('PR_PERSONAL_HOME_PAGE_W' ,mapi_prop_tag(PT_UNICODE, 0x3A50)); +define('PR_PERSONAL_HOME_PAGE_A' ,mapi_prop_tag(PT_STRING8, 0x3A50)); + + +define('PR_BUSINESS_HOME_PAGE' ,mapi_prop_tag(PT_TSTRING, 0x3A51)); +define('PR_BUSINESS_HOME_PAGE_W' ,mapi_prop_tag(PT_UNICODE, 0x3A51)); +define('PR_BUSINESS_HOME_PAGE_A' ,mapi_prop_tag(PT_STRING8, 0x3A51)); + +define('PR_CONTACT_VERSION' ,mapi_prop_tag(PT_CLSID, 0x3A52)); +define('PR_CONTACT_ENTRYIDS' ,mapi_prop_tag(PT_MV_BINARY, 0x3A53)); + +define('PR_CONTACT_ADDRTYPES' ,mapi_prop_tag(PT_MV_TSTRING, 0x3A54)); +define('PR_CONTACT_ADDRTYPES_W' ,mapi_prop_tag(PT_MV_UNICODE, 0x3A54)); +define('PR_CONTACT_ADDRTYPES_A' ,mapi_prop_tag(PT_MV_STRING8, 0x3A54)); + +define('PR_CONTACT_DEFAULT_ADDRESS_INDEX' ,mapi_prop_tag(PT_LONG, 0x3A55)); + +define('PR_CONTACT_EMAIL_ADDRESSES' ,mapi_prop_tag(PT_MV_TSTRING, 0x3A56)); +define('PR_CONTACT_EMAIL_ADDRESSES_W' ,mapi_prop_tag(PT_MV_UNICODE, 0x3A56)); +define('PR_CONTACT_EMAIL_ADDRESSES_A' ,mapi_prop_tag(PT_MV_STRING8, 0x3A56)); +define('PR_ATTACHMENT_CONTACTPHOTO' ,mapi_prop_tag(PT_BOOLEAN, 0x7FFF)); + + +define('PR_COMPANY_MAIN_PHONE_NUMBER' ,mapi_prop_tag(PT_TSTRING, 0x3A57)); +define('PR_COMPANY_MAIN_PHONE_NUMBER_W' ,mapi_prop_tag(PT_UNICODE, 0x3A57)); +define('PR_COMPANY_MAIN_PHONE_NUMBER_A' ,mapi_prop_tag(PT_STRING8, 0x3A57)); + +define('PR_CHILDRENS_NAMES' ,mapi_prop_tag(PT_MV_TSTRING, 0x3A58)); +define('PR_CHILDRENS_NAMES_W' ,mapi_prop_tag(PT_MV_UNICODE, 0x3A58)); +define('PR_CHILDRENS_NAMES_A' ,mapi_prop_tag(PT_MV_STRING8, 0x3A58)); + + + +define('PR_HOME_ADDRESS_CITY' ,mapi_prop_tag(PT_TSTRING, 0x3A59)); +define('PR_HOME_ADDRESS_CITY_W' ,mapi_prop_tag(PT_UNICODE, 0x3A59)); +define('PR_HOME_ADDRESS_CITY_A' ,mapi_prop_tag(PT_STRING8, 0x3A59)); + +define('PR_HOME_ADDRESS_COUNTRY' ,mapi_prop_tag(PT_TSTRING, 0x3A5A)); +define('PR_HOME_ADDRESS_COUNTRY_W' ,mapi_prop_tag(PT_UNICODE, 0x3A5A)); +define('PR_HOME_ADDRESS_COUNTRY_A' ,mapi_prop_tag(PT_STRING8, 0x3A5A)); + +define('PR_HOME_ADDRESS_POSTAL_CODE' ,mapi_prop_tag(PT_TSTRING, 0x3A5B)); +define('PR_HOME_ADDRESS_POSTAL_CODE_W' ,mapi_prop_tag(PT_UNICODE, 0x3A5B)); +define('PR_HOME_ADDRESS_POSTAL_CODE_A' ,mapi_prop_tag(PT_STRING8, 0x3A5B)); + +define('PR_HOME_ADDRESS_STATE_OR_PROVINCE' ,mapi_prop_tag(PT_TSTRING, 0x3A5C)); +define('PR_HOME_ADDRESS_STATE_OR_PROVINCE_W' ,mapi_prop_tag(PT_UNICODE, 0x3A5C)); +define('PR_HOME_ADDRESS_STATE_OR_PROVINCE_A' ,mapi_prop_tag(PT_STRING8, 0x3A5C)); + +define('PR_HOME_ADDRESS_STREET' ,mapi_prop_tag(PT_TSTRING, 0x3A5D)); +define('PR_HOME_ADDRESS_STREET_W' ,mapi_prop_tag(PT_UNICODE, 0x3A5D)); +define('PR_HOME_ADDRESS_STREET_A' ,mapi_prop_tag(PT_STRING8, 0x3A5D)); + +define('PR_HOME_ADDRESS_POST_OFFICE_BOX' ,mapi_prop_tag(PT_TSTRING, 0x3A5E)); +define('PR_HOME_ADDRESS_POST_OFFICE_BOX_W' ,mapi_prop_tag(PT_UNICODE, 0x3A5E)); +define('PR_HOME_ADDRESS_POST_OFFICE_BOX_A' ,mapi_prop_tag(PT_STRING8, 0x3A5E)); + +define('PR_OTHER_ADDRESS_CITY' ,mapi_prop_tag(PT_TSTRING, 0x3A5F)); +define('PR_OTHER_ADDRESS_CITY_W' ,mapi_prop_tag(PT_UNICODE, 0x3A5F)); +define('PR_OTHER_ADDRESS_CITY_A' ,mapi_prop_tag(PT_STRING8, 0x3A5F)); + +define('PR_OTHER_ADDRESS_COUNTRY' ,mapi_prop_tag(PT_TSTRING, 0x3A60)); +define('PR_OTHER_ADDRESS_COUNTRY_W' ,mapi_prop_tag(PT_UNICODE, 0x3A60)); +define('PR_OTHER_ADDRESS_COUNTRY_A' ,mapi_prop_tag(PT_STRING8, 0x3A60)); + +define('PR_OTHER_ADDRESS_POSTAL_CODE' ,mapi_prop_tag(PT_TSTRING, 0x3A61)); +define('PR_OTHER_ADDRESS_POSTAL_CODE_W' ,mapi_prop_tag(PT_UNICODE, 0x3A61)); +define('PR_OTHER_ADDRESS_POSTAL_CODE_A' ,mapi_prop_tag(PT_STRING8, 0x3A61)); + +define('PR_OTHER_ADDRESS_STATE_OR_PROVINCE' ,mapi_prop_tag(PT_TSTRING, 0x3A62)); +define('PR_OTHER_ADDRESS_STATE_OR_PROVINCE_W' ,mapi_prop_tag(PT_UNICODE, 0x3A62)); +define('PR_OTHER_ADDRESS_STATE_OR_PROVINCE_A' ,mapi_prop_tag(PT_STRING8, 0x3A62)); + +define('PR_OTHER_ADDRESS_STREET' ,mapi_prop_tag(PT_TSTRING, 0x3A63)); +define('PR_OTHER_ADDRESS_STREET_W' ,mapi_prop_tag(PT_UNICODE, 0x3A63)); +define('PR_OTHER_ADDRESS_STREET_A' ,mapi_prop_tag(PT_STRING8, 0x3A63)); + +define('PR_OTHER_ADDRESS_POST_OFFICE_BOX' ,mapi_prop_tag(PT_TSTRING, 0x3A64)); +define('PR_OTHER_ADDRESS_POST_OFFICE_BOX_W' ,mapi_prop_tag(PT_UNICODE, 0x3A64)); +define('PR_OTHER_ADDRESS_POST_OFFICE_BOX_A' ,mapi_prop_tag(PT_STRING8, 0x3A64)); + +define('PR_USER_X509_CERTIFICATE' ,mapi_prop_tag(PT_MV_BINARY, 0x3A70)); + +/* + * Profile section properties + */ + +define('PR_STORE_PROVIDERS' ,mapi_prop_tag(PT_BINARY, 0x3D00)); +define('PR_AB_PROVIDERS' ,mapi_prop_tag(PT_BINARY, 0x3D01)); +define('PR_TRANSPORT_PROVIDERS' ,mapi_prop_tag(PT_BINARY, 0x3D02)); + +define('PR_DEFAULT_PROFILE' ,mapi_prop_tag(PT_BOOLEAN, 0x3D04)); +define('PR_AB_SEARCH_PATH' ,mapi_prop_tag(PT_MV_BINARY, 0x3D05)); +define('PR_AB_DEFAULT_DIR' ,mapi_prop_tag(PT_BINARY, 0x3D06)); +define('PR_AB_DEFAULT_PAB' ,mapi_prop_tag(PT_BINARY, 0x3D07)); + +define('PR_FILTERING_HOOKS' ,mapi_prop_tag(PT_BINARY, 0x3D08)); +define('PR_SERVICE_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3D09)); +define('PR_SERVICE_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3D09)); +define('PR_SERVICE_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3D09)); +define('PR_SERVICE_DLL_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3D0A)); +define('PR_SERVICE_DLL_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3D0A)); +define('PR_SERVICE_DLL_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3D0A)); +define('PR_SERVICE_ENTRY_NAME' ,mapi_prop_tag(PT_STRING8, 0x3D0B)); +define('PR_SERVICE_UID' ,mapi_prop_tag(PT_BINARY, 0x3D0C)); +define('PR_SERVICE_EXTRA_UIDS' ,mapi_prop_tag(PT_BINARY, 0x3D0D)); +define('PR_SERVICES' ,mapi_prop_tag(PT_BINARY, 0x3D0E)); +define('PR_SERVICE_SUPPORT_FILES' ,mapi_prop_tag(PT_MV_TSTRING, 0x3D0F)); +define('PR_SERVICE_SUPPORT_FILES_W' ,mapi_prop_tag(PT_MV_UNICODE, 0x3D0F)); +define('PR_SERVICE_SUPPORT_FILES_A' ,mapi_prop_tag(PT_MV_STRING8, 0x3D0F)); +define('PR_SERVICE_DELETE_FILES' ,mapi_prop_tag(PT_MV_TSTRING, 0x3D10)); +define('PR_SERVICE_DELETE_FILES_W' ,mapi_prop_tag(PT_MV_UNICODE, 0x3D10)); +define('PR_SERVICE_DELETE_FILES_A' ,mapi_prop_tag(PT_MV_STRING8, 0x3D10)); +define('PR_AB_SEARCH_PATH_UPDATE' ,mapi_prop_tag(PT_BINARY, 0x3D11)); +define('PR_PROFILE_NAME' ,mapi_prop_tag(PT_TSTRING, 0x3D12)); +define('PR_PROFILE_NAME_A' ,mapi_prop_tag(PT_STRING8, 0x3D12)); +define('PR_PROFILE_NAME_W' ,mapi_prop_tag(PT_UNICODE, 0x3D12)); + +/* + * Status object properties + */ + +define('PR_IDENTITY_DISPLAY' ,mapi_prop_tag(PT_TSTRING, 0x3E00)); +define('PR_IDENTITY_DISPLAY_W' ,mapi_prop_tag(PT_UNICODE, 0x3E00)); +define('PR_IDENTITY_DISPLAY_A' ,mapi_prop_tag(PT_STRING8, 0x3E00)); +define('PR_IDENTITY_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x3E01)); +define('PR_RESOURCE_METHODS' ,mapi_prop_tag(PT_LONG, 0x3E02)); +define('PR_RESOURCE_TYPE' ,mapi_prop_tag(PT_LONG, 0x3E03)); +define('PR_STATUS_CODE' ,mapi_prop_tag(PT_LONG, 0x3E04)); +define('PR_IDENTITY_SEARCH_KEY' ,mapi_prop_tag(PT_BINARY, 0x3E05)); +define('PR_OWN_STORE_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x3E06)); +define('PR_RESOURCE_PATH' ,mapi_prop_tag(PT_TSTRING, 0x3E07)); +define('PR_RESOURCE_PATH_W' ,mapi_prop_tag(PT_UNICODE, 0x3E07)); +define('PR_RESOURCE_PATH_A' ,mapi_prop_tag(PT_STRING8, 0x3E07)); +define('PR_STATUS_STRING' ,mapi_prop_tag(PT_TSTRING, 0x3E08)); +define('PR_STATUS_STRING_W' ,mapi_prop_tag(PT_UNICODE, 0x3E08)); +define('PR_STATUS_STRING_A' ,mapi_prop_tag(PT_STRING8, 0x3E08)); +define('PR_X400_DEFERRED_DELIVERY_CANCEL' ,mapi_prop_tag(PT_BOOLEAN, 0x3E09)); +define('PR_HEADER_FOLDER_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x3E0A)); +define('PR_REMOTE_PROGRESS' ,mapi_prop_tag(PT_LONG, 0x3E0B)); +define('PR_REMOTE_PROGRESS_TEXT' ,mapi_prop_tag(PT_TSTRING, 0x3E0C)); +define('PR_REMOTE_PROGRESS_TEXT_W' ,mapi_prop_tag(PT_UNICODE, 0x3E0C)); +define('PR_REMOTE_PROGRESS_TEXT_A' ,mapi_prop_tag(PT_STRING8, 0x3E0C)); +define('PR_REMOTE_VALIDATE_OK' ,mapi_prop_tag(PT_BOOLEAN, 0x3E0D)); + +/* + * Display table properties + */ + +define('PR_CONTROL_FLAGS' ,mapi_prop_tag(PT_LONG, 0x3F00)); +define('PR_CONTROL_STRUCTURE' ,mapi_prop_tag(PT_BINARY, 0x3F01)); +define('PR_CONTROL_TYPE' ,mapi_prop_tag(PT_LONG, 0x3F02)); +define('PR_DELTAX' ,mapi_prop_tag(PT_LONG, 0x3F03)); +define('PR_DELTAY' ,mapi_prop_tag(PT_LONG, 0x3F04)); +define('PR_XPOS' ,mapi_prop_tag(PT_LONG, 0x3F05)); +define('PR_YPOS' ,mapi_prop_tag(PT_LONG, 0x3F06)); +define('PR_CONTROL_ID' ,mapi_prop_tag(PT_BINARY, 0x3F07)); +define('PR_INITIAL_DETAILS_PANE' ,mapi_prop_tag(PT_LONG, 0x3F08)); + +/* + * Secure property id range + */ + +define('PROP_ID_SECURE_MIN' ,0x67F0); +define('PROP_ID_SECURE_MAX' ,0x67FF); + +/* + * Extra properties + */ + +define('PR_IPM_APPOINTMENT_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x36D0)); +define('PR_IPM_CONTACT_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x36D1)); +define('PR_IPM_JOURNAL_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x36D2)); +define('PR_IPM_NOTE_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x36D3)); +define('PR_IPM_TASK_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x36D4)); +define('PR_IPM_DRAFTS_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x36D7)); +/* +PR_ADDITIONAL_REN_ENTRYIDS: + This is a multivalued property which contains entry IDs for certain special folders. + The first 5 (0-4) entries in this multivalued property are as follows: + 0 - Conflicts folder + 1 - Sync Issues folder + 2 - Local Failures folder + 3 - Server Failures folder + 4 - Junk E-mail Folder + 5 - sfSpamTagDontUse (unknown what this is, disable olk spam stuff?) +*/ +define('PR_ADDITIONAL_REN_ENTRYIDS' ,mapi_prop_tag(PT_MV_BINARY, 0x36D8)); +define('PR_FREEBUSY_ENTRYIDS' ,mapi_prop_tag(PT_MV_BINARY, 0x36E4)); +define('PR_REM_ONLINE_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x36D5)); +define('PR_REM_OFFLINE_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x36D6)); +/* +PR_IPM_OL2007_ENTRYIDS: + This is a single binary property containing the entryids for: + - 'Rss feeds' folder + - The searchfolder 'Tracked mail processing' + - The searchfolder 'To-do list' + + However, it is encoded something like the following: + + 01803200 (type: rss feeds ?) + 0100 + 2E00 + 00000000B774162F0098C84182DE9E4358E4249D01000B41FF66083D464EA7E34D6026C9B143000000006DDA0000 (entryid) + 04803200 (type: tracked mail processing ?) + 0100 + 2E00 + 00000000B774162F0098C84182DE9E4358E4249D01000B41FF66083D464EA7E34D6026C9B143000000006DDB0000 (entryid) + 02803200 (type: todo list ?) + 0100 + 2E00 + 00000000B774162F0098C84182DE9E4358E4249D01000B41FF66083D464EA7E34D6026C9B143000000006DE40000 (entryid) + 00000000 (terminator?) + + It may also only contain the rss feeds entryid, and then have the 00000000 terminator directly after the entryid: + + 01803200 (type: rss feeds ?) + 0100 + 2E00 + 00000000B774162F0098C84182DE9E4358E4249D01000B41FF66083D464EA7E34D6026C9B143000000006DDA0000 (entryid) + 00000000 (terminator?) +*/ +define('PR_IPM_OL2007_ENTRYIDS' ,mapi_prop_tag(PT_BINARY, 0x36D9)); + + + +/* + * Don't know where to put these + */ + +define('PR_ICON_INDEX' ,mapi_prop_tag(PT_LONG, 0x1080)); +define('PR_LAST_VERB_EXECUTED' ,mapi_prop_tag(PT_LONG, 0x1081)); +define('PR_LAST_VERB_EXECUTION_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x1082)); +define('PR_INTERNET_CPID' ,mapi_prop_tag(PT_LONG, 0x3FDE)); +define('PR_RECIPIENT_ENTRYID' ,mapi_prop_tag(PT_BINARY, 0x5FF7)); +define('PR_SEND_INTERNET_ENCODING' ,mapi_prop_tag(PT_LONG, 0x3FDE)); +define('PR_RECIPIENT_DISPLAY_NAME' ,mapi_prop_tag(PT_STRING8, 0x5FF6)); +define('PR_RECIPIENT_TRACKSTATUS' ,mapi_prop_tag(PT_LONG, 0x5FFF)); +define('PR_RECIPIENT_FLAGS' ,mapi_prop_tag(PT_LONG, 0x5FFD)); +define('PR_RECIPIENT_TRACKSTATUS_TIME' ,mapi_prop_tag(PT_SYSTIME, 0x5FFB)); + +define('PR_EC_BASE' , 0x6700); +define('PR_EC_OUTOFOFFICE' ,mapi_prop_tag(PT_BOOLEAN, PR_EC_BASE+0x60)); +define('PR_EC_OUTOFOFFICE_MSG' ,mapi_prop_tag(PT_STRING8, PR_EC_BASE+0x61)); +define('PR_EC_OUTOFOFFICE_SUBJECT' ,mapi_prop_tag(PT_STRING8, PR_EC_BASE+0x62)); + +/* quota support */ +define('PR_QUOTA_WARNING_THRESHOLD' ,mapi_prop_tag(PT_LONG, PR_EC_BASE+0x21)); +define('PR_QUOTA_SEND_THRESHOLD' ,mapi_prop_tag(PT_LONG, PR_EC_BASE+0x22)); +define('PR_QUOTA_RECEIVE_THRESHOLD' ,mapi_prop_tag(PT_LONG, PR_EC_BASE+0x23)); + +/* storage for the settings for the webaccess 6.xx */ +define('PR_EC_WEBACCESS_SETTINGS' ,mapi_prop_tag(PT_STRING8, PR_EC_BASE+0x70)); +define('PR_EC_RECIPIENT_HISTORY' ,mapi_prop_tag(PT_STRING8, PR_EC_BASE+0x71)); + +/* storage for the settings for the webaccess 7.xx */ +define('PR_EC_WEBACCESS_SETTINGS_JSON' ,mapi_prop_tag(PT_STRING8, PR_EC_BASE+0x72)); +define('PR_EC_RECIPIENT_HISTORY_JSON' ,mapi_prop_tag(PT_STRING8, PR_EC_BASE+0x73)); + +/* statistics properties */ +define('PR_EC_STATSTABLE_SYSTEM' ,mapi_prop_tag(PT_OBJECT, PR_EC_BASE+0x30)); +define('PR_EC_STATSTABLE_SESSIONS' ,mapi_prop_tag(PT_OBJECT, PR_EC_BASE+0x31)); +define('PR_EC_STATSTABLE_USERS' ,mapi_prop_tag(PT_OBJECT, PR_EC_BASE+0x32)); +define('PR_EC_STATSTABLE_COMPANY' ,mapi_prop_tag(PT_OBJECT, PR_EC_BASE+0x33)); + +define('PR_EC_STATS_SYSTEM_DESCRIPTION' ,mapi_prop_tag(PT_STRING8, PR_EC_BASE+0x40)); +define('PR_EC_STATS_SYSTEM_VALUE' ,mapi_prop_tag(PT_STRING8, PR_EC_BASE+0x41)); +define('PR_EC_STATS_SESSION_ID' ,mapi_prop_tag(PT_LONG, PR_EC_BASE+0x42)); +define('PR_EC_STATS_SESSION_IPADDRESS' ,mapi_prop_tag(PT_STRING8, PR_EC_BASE+0x43)); +define('PR_EC_STATS_SESSION_IDLETIME' ,mapi_prop_tag(PT_LONG, PR_EC_BASE+0x44)); +define('PR_EC_STATS_SESSION_CAPABILITY' ,mapi_prop_tag(PT_LONG, PR_EC_BASE+0x45)); +define('PR_EC_STATS_SESSION_LOCKED' ,mapi_prop_tag(PT_BOOLEAN, PR_EC_BASE+0x46)); +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)); + +/* WA properties */ +define('PR_EC_WA_ATTACHMENT_HIDDEN_OVERRIDE' ,mapi_prop_tag(PT_BOOLEAN, PR_EC_BASE+0xE0)); + +// edkmdb, rules properties +#define pidSpecialMin 0x6670 +define('PR_RULE_ID' ,mapi_prop_tag(PT_I8, 0x6670+0x04)); // only lower 32bits are used. +define('PR_RULE_IDS' ,mapi_prop_tag(PT_BINARY, 0x6670+0x05)); +define('PR_RULE_SEQUENCE' ,mapi_prop_tag(PT_LONG, 0x6670+0x06)); +define('PR_RULE_STATE' ,mapi_prop_tag(PT_LONG, 0x6670+0x07)); +define('PR_RULE_USER_FLAGS' ,mapi_prop_tag(PT_LONG, 0x6670+0x08)); +define('PR_RULE_CONDITION' ,mapi_prop_tag(PT_SRESTRICTION,0x6670+0x09)); +define('PR_RULE_ACTIONS' ,mapi_prop_tag(PT_ACTIONS, 0x6670+0x10)); +define('PR_RULE_PROVIDER' ,mapi_prop_tag(PT_STRING8, 0x6670+0x11)); +define('PR_RULE_NAME' ,mapi_prop_tag(PT_TSTRING, 0x6670+0x12)); +define('PR_RULE_LEVEL' ,mapi_prop_tag(PT_LONG, 0x6670+0x13)); +define('PR_RULE_PROVIDER_DATA' ,mapi_prop_tag(PT_BINARY, 0x6670+0x14)); + +// edkmdb, ICS properties +define('PR_SOURCE_KEY' ,mapi_prop_tag(PT_BINARY, 0x65E0+0x00)); +define('PR_PARENT_SOURCE_KEY' ,mapi_prop_tag(PT_BINARY, 0x65E0+0x01)); +define('PR_CHANGE_KEY' ,mapi_prop_tag(PT_BINARY, 0x65E0+0x02)); +define('PR_PREDECESSOR_CHANGE_LIST' ,mapi_prop_tag(PT_BINARY, 0x65E0+0x03)); + + +define('PR_PROCESS_MEETING_REQUESTS' ,mapi_prop_tag(PT_BOOLEAN, 0x686D)); +define('PR_DECLINE_RECURRING_MEETING_REQUESTS' ,mapi_prop_tag(PT_BOOLEAN, 0x686E)); +define('PR_DECLINE_CONFLICTING_MEETING_REQUESTS' ,mapi_prop_tag(PT_BOOLEAN, 0x686F)); + + +define('PR_PROPOSEDNEWTIME' ,mapi_prop_tag(PT_BOOLEAN, 0x5FE1)); +define('PR_PROPOSENEWTIME_START' ,mapi_prop_tag(PT_SYSTIME, 0x5FE3)); +define('PR_PROPOSENEWTIME_END' ,mapi_prop_tag(PT_SYSTIME, 0x5FE4)); + +// property for sort the recoverable items. +define('PR_DELETED_ON' ,mapi_prop_tag(PT_SYSTIME, 0x668F)); + +define('PR_PROCESSED' ,mapi_prop_tag(PT_BOOLEAN, 0x7D01)); + +// Delegates properties +define('PR_DELEGATES_SEE_PRIVATE' ,mapi_prop_tag(PT_MV_LONG, 0x686B)); +define('PR_SCHDINFO_DELEGATE_ENTRYIDS' ,mapi_prop_tag(PT_MV_BINARY, 0x6845)); +define('PR_SCHDINFO_DELEGATE_NAMES' ,mapi_prop_tag(PT_MV_STRING8, 0x6844)); +define('PR_DELEGATED_BY_RULE' ,mapi_prop_tag(PT_BOOLEAN, 0x3FE3)); + +// properties required in Reply mail. +define('PR_INTERNET_REFERENCES' ,mapi_prop_tag(PT_STRING8, 0x1039)); +define('PR_IN_REPLY_TO_ID' ,mapi_prop_tag(PT_STRING8, 0x1042)); +define('PR_INTERNET_MESSAGE_ID' ,mapi_prop_tag(PT_STRING8, 0x1035)); + +// for hidden folders +define('PR_ATTR_HIDDEN' ,mapi_prop_tag(PT_BOOLEAN, 0x10F4)); + +/** + * Addressbook detail properties. + * It is not defined by MAPI, but to keep in sync with the interface of outlook we have to use these + * properties. Outlook actually uses these properties for it's addressbook details. + */ +define('PR_HOME2_TELEPHONE_NUMBER_MV' ,mapi_prop_tag(PT_MV_TSTRING, 0x3A2F)); +define('PR_BUSINESS2_TELEPHONE_NUMBER_MV' ,mapi_prop_tag(PT_MV_TSTRING, 0x3A1B)); +define('PR_EMS_AB_PROXY_ADDRESSES' ,mapi_prop_tag(PT_TSTRING, 0x800F)); +define('PR_EMS_AB_PROXY_ADDRESSES_MV' ,mapi_prop_tag(PT_MV_TSTRING, 0x800F)); +define('PR_EMS_AB_MANAGER' ,mapi_prop_tag(PT_BINARY, 0x8005)); +define('PR_EMS_AB_REPORTS' ,mapi_prop_tag(PT_BINARY, 0x800E)); +define('PR_EMS_AB_REPORTS_MV' ,mapi_prop_tag(PT_MV_BINARY, 0x800E)); +define('PR_EMS_AB_IS_MEMBER_OF_DL' ,mapi_prop_tag(PT_MV_BINARY, 0x8008)); +define('PR_EMS_AB_OWNER' ,mapi_prop_tag(PT_BINARY, 0x800C)); +define('PR_EMS_AB_ROOM_CAPACITY' ,mapi_prop_tag(PT_LONG, 0x0807)); +define('PR_EMS_AB_TAGGED_X509_CERT' ,mapi_prop_tag(PT_MV_BINARY, 0x8C6A)); + +define('PR_EC_ARCHIVE_SERVERS' ,mapi_prop_tag(PT_MV_TSTRING, 0x67c4)); + +/* zarafa contacts provider properties */ +define('PR_ZC_CONTACT_STORE_ENTRYIDS' ,mapi_prop_tag(PT_MV_BINARY, PR_EC_BASE+0x11)); +define('PR_ZC_CONTACT_FOLDER_ENTRYIDS' ,mapi_prop_tag(PT_MV_BINARY, PR_EC_BASE+0x12)); +define('PR_ZC_CONTACT_FOLDER_NAMES' ,mapi_prop_tag(PT_MV_TSTRING, PR_EC_BASE+0x13)); + +//Properties defined for Z-Push +define('PR_TODO_ITEM_FLAGS' ,mapi_prop_tag(PT_LONG, 0x0E2B)); + +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapimapping.php b/sources/backend/zarafa/mapimapping.php new file mode 100644 index 0000000..8d85586 --- /dev/null +++ b/sources/backend/zarafa/mapimapping.php @@ -0,0 +1,524 @@ +. +* +* Consult LICENSE file for details +************************************************/ +/** + * + * MAPI to AS mapping class + * + * + */ +class MAPIMapping { + /** + * Returns the MAPI to AS mapping for contacts + * + * @return array + */ + public static function GetContactMapping() { + return array ( + "anniversary" => PR_WEDDING_ANNIVERSARY, + "assistantname" => PR_ASSISTANT, + "assistnamephonenumber" => PR_ASSISTANT_TELEPHONE_NUMBER, + "birthday" => PR_BIRTHDAY, + "body" => PR_BODY, + "business2phonenumber" => PR_BUSINESS2_TELEPHONE_NUMBER, + "businesscity" => "PT_STRING8:PSETID_Address:0x8046", + "businesscountry" => "PT_STRING8:PSETID_Address:0x8049", + "businesspostalcode" => "PT_STRING8:PSETID_Address:0x8048", + "businessstate" => "PT_STRING8:PSETID_Address:0x8047", + "businessstreet" => "PT_STRING8:PSETID_Address:0x8045", + "businessfaxnumber" => PR_BUSINESS_FAX_NUMBER, + "businessphonenumber" => PR_OFFICE_TELEPHONE_NUMBER, + "carphonenumber" => PR_CAR_TELEPHONE_NUMBER, + "categories" => "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords", + "children" => PR_CHILDRENS_NAMES, + "companyname" => PR_COMPANY_NAME, + "department" => PR_DEPARTMENT_NAME, + "email1address" => "PT_STRING8:PSETID_Address:0x8083", + "email2address" => "PT_STRING8:PSETID_Address:0x8093", + "email3address" => "PT_STRING8:PSETID_Address:0x80A3", + "fileas" => "PT_STRING8:PSETID_Address:0x8005", + "firstname" => PR_GIVEN_NAME, + "home2phonenumber" => PR_HOME2_TELEPHONE_NUMBER, + "homecity" => PR_HOME_ADDRESS_CITY, + "homecountry" => PR_HOME_ADDRESS_COUNTRY, + "homepostalcode" => PR_HOME_ADDRESS_POSTAL_CODE, + "homestate" => PR_HOME_ADDRESS_STATE_OR_PROVINCE, + "homestreet" => PR_HOME_ADDRESS_STREET, + "homefaxnumber" => PR_HOME_FAX_NUMBER, + "homephonenumber" => PR_HOME_TELEPHONE_NUMBER, + "jobtitle" => PR_TITLE, + "lastname" => PR_SURNAME, + "middlename" => PR_MIDDLE_NAME, + "mobilephonenumber" => PR_CELLULAR_TELEPHONE_NUMBER, + "officelocation" => PR_OFFICE_LOCATION, + "othercity" => PR_OTHER_ADDRESS_CITY, + "othercountry" => PR_OTHER_ADDRESS_COUNTRY, + "otherpostalcode" => PR_OTHER_ADDRESS_POSTAL_CODE, + "otherstate" => PR_OTHER_ADDRESS_STATE_OR_PROVINCE, + "otherstreet" => PR_OTHER_ADDRESS_STREET, + "pagernumber" => PR_PAGER_TELEPHONE_NUMBER, + "radiophonenumber" => PR_RADIO_TELEPHONE_NUMBER, + "spouse" => PR_SPOUSE_NAME, + "suffix" => PR_GENERATION, + "title" => PR_DISPLAY_NAME_PREFIX, + "webpage" => "PT_STRING8:PSETID_Address:0x802b", + "yomicompanyname" => "PT_STRING8:PSETID_Address:0x802e", + "yomifirstname" => "PT_STRING8:PSETID_Address:0x802c", + "yomilastname" => "PT_STRING8:PSETID_Address:0x802d", + "rtf" => PR_RTF_COMPRESSED, + // picture + "customerid" => PR_CUSTOMER_ID, + "governmentid" => PR_GOVERNMENT_ID_NUMBER, + "imaddress" => "PT_STRING8:PSETID_Address:0x8062", + "imaddress2" => "PT_STRING8:PSETID_AirSync:IMAddress2", + "imaddress3" => "PT_STRING8:PSETID_AirSync:IMAddress3", + "managername" => PR_MANAGER_NAME, + "companymainphone" => PR_COMPANY_MAIN_PHONE_NUMBER, + "accountname" => PR_ACCOUNT, + "nickname" => PR_NICKNAME, + // mms + ); + } + + + /** + * + * Returns contact specific MAPI properties + * + * @access public + * + * @return array + */ + public static function GetContactProperties() { + return array ( + "haspic" => "PT_BOOLEAN:PSETID_Address:0x8015", + "emailaddress1" => "PT_STRING8:PSETID_Address:0x8083", + "emailaddressdname1" => "PT_STRING8:PSETID_Address:0x8080", + "emailaddressdemail1" => "PT_STRING8:PSETID_Address:0x8084", + "emailaddresstype1" => "PT_STRING8:PSETID_Address:0x8082", + "emailaddressentryid1" => "PT_BINARY:PSETID_Address:0x8085", + "emailaddress2" => "PT_STRING8:PSETID_Address:0x8093", + "emailaddressdname2" => "PT_STRING8:PSETID_Address:0x8090", + "emailaddressdemail2" => "PT_STRING8:PSETID_Address:0x8094", + "emailaddresstype2" => "PT_STRING8:PSETID_Address:0x8092", + "emailaddressentryid2" => "PT_BINARY:PSETID_Address:0x8095", + "emailaddress3" => "PT_STRING8:PSETID_Address:0x80a3", + "emailaddressdname3" => "PT_STRING8:PSETID_Address:0x80a0", + "emailaddressdemail3" => "PT_STRING8:PSETID_Address:0x80a4", + "emailaddresstype3" => "PT_STRING8:PSETID_Address:0x80a2", + "emailaddressentryid3" => "PT_BINARY:PSETID_Address:0x80a5", + "addressbookmv" => "PT_MV_LONG:PSETID_Address:0x8028", + "addressbooklong" => "PT_LONG:PSETID_Address:0x8029", + "displayname" => PR_DISPLAY_NAME, + "subject" => PR_SUBJECT, + "country" => PR_COUNTRY, + "city" => PR_LOCALITY, + "postaladdress" => PR_POSTAL_ADDRESS, + "postalcode" => PR_POSTAL_CODE, + "state" => PR_STATE_OR_PROVINCE, + "street" => PR_STREET_ADDRESS, + "homeaddress" => "PT_STRING8:PSETID_Address:0x801a", + "businessaddress" => "PT_STRING8:PSETID_Address:0x801b", + "otheraddress" => "PT_STRING8:PSETID_Address:0x801c", + "mailingaddress" => "PT_LONG:PSETID_Address:0x8022", + ); + } + + + /** + * Returns the MAPI to AS mapping for emails + * + * @return array + */ + public static function GetEmailMapping() { + return array ( + // from + "datereceived" => PR_MESSAGE_DELIVERY_TIME, + "displayname" => PR_SUBJECT, + "displayto" => PR_DISPLAY_TO, + "importance" => PR_IMPORTANCE, + "messageclass" => PR_MESSAGE_CLASS, + "subject" => PR_SUBJECT, + "read" => PR_MESSAGE_FLAGS, + // "to" // need to be generated with SMTP addresses + // "cc" + // "threadtopic" => PR_CONVERSATION_TOPIC, + "internetcpid" => PR_INTERNET_CPID, + "nativebodytype" => PR_NATIVE_BODY_INFO, + "lastverbexecuted" => PR_LAST_VERB_EXECUTED, + "lastverbexectime" => PR_LAST_VERB_EXECUTION_TIME, + ); + } + + + /** + * + * Returns email specific MAPI properties + * + * @access public + * + * @return array + */ + public static function GetEmailProperties() { + return array ( + // Override 'From' to show "Full Name " + "representingname" => PR_SENT_REPRESENTING_NAME, + "representingentryid" => PR_SENT_REPRESENTING_ENTRYID, + "sourcekey" => PR_SOURCE_KEY, + "entryid" => PR_ENTRYID, + "body" => PR_BODY, + "rtfcompressed" => PR_RTF_COMPRESSED, + "html" => PR_HTML, + "rtfinsync" => PR_RTF_IN_SYNC, + "processed" => PR_PROCESSED, + ); + } + + + /** + * Returns the MAPI to AS mapping for meeting requests + * + * @return array + */ + public static function GetMeetingRequestMapping() { + return array ( + "responserequested" => PR_RESPONSE_REQUESTED, + // timezone + "alldayevent" => "PT_BOOLEAN:PSETID_Appointment:0x8215", + "busystatus" => "PT_LONG:PSETID_Appointment:0x8205", + "rtf" => PR_RTF_COMPRESSED, + "dtstamp" => PR_LAST_MODIFICATION_TIME, + "endtime" => "PT_SYSTIME:PSETID_Appointment:0x820e", + "location" => "PT_STRING8:PSETID_Appointment:0x8208", + // recurrences + "reminder" => "PT_LONG:PSETID_Common:0x8501", + "starttime" => "PT_SYSTIME:PSETID_Appointment:0x820d", + "sensitivity" => PR_SENSITIVITY, + ); + } + + + public static function GetMeetingRequestProperties() { + return array ( + "goidtag" => "PT_BINARY:PSETID_Meeting:0x3", + "timezonetag" => "PT_BINARY:PSETID_Appointment:0x8233", + "recReplTime" => "PT_SYSTIME:PSETID_Appointment:0x8228", + "isrecurringtag" => "PT_BOOLEAN:PSETID_Appointment:0x8223", + "recurringstate" => "PT_BINARY:PSETID_Appointment:0x8216", + "appSeqNr" => "PT_LONG:PSETID_Appointment:0x8201", + "lidIsException" => "PT_BOOLEAN:PSETID_Appointment:0xA", + "recurStartTime" => "PT_LONG:PSETID_Meeting:0xE", + "reminderset" => "PT_BOOLEAN:PSETID_Common:0x8503", + "remindertime" => "PT_LONG:PSETID_Common:0x8501", + "recurrenceend" => "PT_SYSTIME:PSETID_Appointment:0x8236", + ); + } + + + public static function GetTnefAndIcalProperties() { + return array( + "starttime" => "PT_SYSTIME:PSETID_Appointment:0x820d", + "endtime" => "PT_SYSTIME:PSETID_Appointment:0x820e", + "commonstart" => "PT_SYSTIME:PSETID_Common:0x8516", + "commonend" => "PT_SYSTIME:PSETID_Common:0x8517", + "clipstart" => "PT_SYSTIME:PSETID_Appointment:0x8235", //ical only + "recurrenceend" => "PT_SYSTIME:PSETID_Appointment:0x8236", //ical only + "isrecurringtag" => "PT_BOOLEAN:PSETID_Appointment:0x8223", + "goidtag" => "PT_BINARY:PSETID_Meeting:0x3", + "goid2tag" => "PT_BINARY:PSETID_Meeting:0x23", + "usetnef" => "PT_LONG:PSETID_Meeting:0x8582", + "tneflocation" => "PT_STRING8:PSETID_Meeting:0x2", //ical only + "location" => "PT_STRING8:PSETID_Appointment:0x8208", + "tnefrecurr" => "PT_BOOLEAN:PSETID_Meeting:0x5", + "sideeffects" => "PT_LONG:PSETID_Common:0x8510", + "type" => "PT_STRING8:PSETID_Meeting:0x24", + "busystatus" => "PT_LONG:PSETID_Appointment:0x8205", + "meetingstatus" => "PT_LONG:PSETID_Appointment:0x8217", + "responsestatus" => "PT_LONG:PSETID_Meeting:0x8218", + //the properties below are currently not used + "dayinterval" => "PT_I2:PSETID_Meeting:0x11", + "weekinterval" => "PT_I2:PSETID_Meeting:0x12", + "monthinterval" => "PT_I2:PSETID_Meeting:0x13", + "yearinterval" => "PT_I2:PSETID_Meeting:0x14", + ); + } + + + /** + * Returns the MAPI to AS mapping for appointments + * + * @return array + */ + public static function GetAppointmentMapping() { + return array ( + "alldayevent" => "PT_BOOLEAN:PSETID_Appointment:0x8215", + "body" => PR_BODY, + "busystatus" => "PT_LONG:PSETID_Appointment:0x8205", + "categories" => "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords", + "rtf" => PR_RTF_COMPRESSED, + "dtstamp" => PR_LAST_MODIFICATION_TIME, + "endtime" => "PT_SYSTIME:PSETID_Appointment:0x820e", + "location" => "PT_STRING8:PSETID_Appointment:0x8208", + "meetingstatus" => "PT_LONG:PSETID_Appointment:0x8217", + "sensitivity" => PR_SENSITIVITY, + "subject" => PR_SUBJECT, + "starttime" => "PT_SYSTIME:PSETID_Appointment:0x820d", + "uid" => "PT_BINARY:PSETID_Meeting:0x3", + "nativebodytype" => PR_NATIVE_BODY_INFO, + ); + } + + + /** + * + * Returns appointment specific MAPI properties + * + * @access public + * + * @return array + */ + public static function GetAppointmentProperties() { + return array( + "sourcekey" => PR_SOURCE_KEY, + "representingentryid" => PR_SENT_REPRESENTING_ENTRYID, + "representingname" => PR_SENT_REPRESENTING_NAME, + "sentrepresentingemail" => PR_SENT_REPRESENTING_EMAIL_ADDRESS, + "sentrepresentingaddt" => PR_SENT_REPRESENTING_ADDRTYPE, + "sentrepresentinsrchk" => PR_SENT_REPRESENTING_SEARCH_KEY, + "reminderset" => "PT_BOOLEAN:PSETID_Common:0x8503", + "remindertime" => "PT_LONG:PSETID_Common:0x8501", + "meetingstatus" => "PT_LONG:PSETID_Appointment:0x8217", + "isrecurring" => "PT_BOOLEAN:PSETID_Appointment:0x8223", + "recurringstate" => "PT_BINARY:PSETID_Appointment:0x8216", + "timezonetag" => "PT_BINARY:PSETID_Appointment:0x8233", + "recurrenceend" => "PT_SYSTIME:PSETID_Appointment:0x8236", + "responsestatus" => "PT_LONG:PSETID_Appointment:0x8218", + "commonstart" => "PT_SYSTIME:PSETID_Common:0x8516", + "commonend" => "PT_SYSTIME:PSETID_Common:0x8517", + "reminderstart" => "PT_SYSTIME:PSETID_Common:0x8502", + "duration" => "PT_LONG:PSETID_Appointment:0x8213", + "private" => "PT_BOOLEAN:PSETID_Common:0x8506", + "uid" => "PT_BINARY:PSETID_Meeting:0x23", + "sideeffects" => "PT_LONG:PSETID_Common:0x8510", + "flagdueby" => "PT_SYSTIME:PSETID_Common:0x8560", + "icon" => PR_ICON_INDEX, + "mrwassent" => "PT_BOOLEAN:PSETID_Appointment:0x8229", + "endtime" => "PT_SYSTIME:PSETID_Appointment:0x820e",//this is here for calendar restriction, tnef and ical + "starttime" => "PT_SYSTIME:PSETID_Appointment:0x820d",//this is here for calendar restriction, tnef and ical + "clipstart" => "PT_SYSTIME:PSETID_Appointment:0x8235", //ical only + "recurrencetype" => "PT_LONG:PSETID_Appointment:0x8231", + "body" => PR_BODY, + "rtfcompressed" => PR_RTF_COMPRESSED, + "html" => PR_HTML, + "rtfinsync" => PR_RTF_IN_SYNC, + ); + } + + + /** + * Returns the MAPI to AS mapping for tasks + * + * @return array + */ + public static function GetTaskMapping() { + return array ( + "body" => PR_BODY, + "categories" => "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords", + "complete" => "PT_BOOLEAN:PSETID_Task:0x811C", + "datecompleted" => "PT_SYSTIME:PSETID_Task:0x810F", + "duedate" => "PT_SYSTIME:PSETID_Task:0x8105", + "utcduedate" => "PT_SYSTIME:PSETID_Common:0x8517", + "utcstartdate" => "PT_SYSTIME:PSETID_Common:0x8516", + "importance" => PR_IMPORTANCE, + // recurrence + // regenerate + // deadoccur + "reminderset" => "PT_BOOLEAN:PSETID_Common:0x8503", + "remindertime" => "PT_SYSTIME:PSETID_Common:0x8502", + "sensitivity" => PR_SENSITIVITY, + "startdate" => "PT_SYSTIME:PSETID_Task:0x8104", + "subject" => PR_SUBJECT, + "rtf" => PR_RTF_COMPRESSED, + ); + } + + + /** + * Returns task specific MAPI properties + * + * @access public + * + * @return array + */ + public static function GetTaskProperties() { + return array ( + "isrecurringtag" => "PT_BOOLEAN:PSETID_Task:0x8126", + "recurringstate" => "PT_BINARY:PSETID_Task:0x8116", + "deadoccur" => "PT_BOOLEAN:PSETID_Task:0x8109", + "completion" => "PT_DOUBLE:PSETID_Task:0x8102", + "status" => "PT_LONG:PSETID_Task:0x8101", + "icon" => PR_ICON_INDEX, + "owner" => "PT_STRING8:PSETID_Task:0x811F", + ); + } + + + /** + * Returns the MAPI to AS mapping for email todo flags + * + * @return array + */ + public static function GetMailFlagsMapping() { + return array ( + "flagstatus" => PR_FLAG_STATUS, + "flagtype" => "PT_STRING8:PSETID_Common:0x8530", + "datecompleted" => "PT_SYSTIME:PSETID_Common:0x810F", + "completetime" => PR_FLAG_COMPLETE_TIME, + "startdate" => "PT_SYSTIME:PSETID_Task:0x8104", + "duedate" => "PT_SYSTIME:PSETID_Task:0x8105", + "utcstartdate" => "PT_SYSTIME:PSETID_Common:0x8516", + "utcduedate" => "PT_SYSTIME:PSETID_Common:0x8517", + "reminderset" => "PT_BOOLEAN:PSETID_Common:0x8503", + "remindertime" => "PT_SYSTIME:PSETID_Common:0x8502", + "ordinaldate" => "PT_SYSTIME:PSETID_Common:0x85A0", + "subordinaldate" => "PT_STRING8:PSETID_Common:0x85A1", + + ); + } + + + /** + * Returns email todo flags' specific MAPI properties + * + * @access public + * + * @return array + */ + public static function GetMailFlagsProperties() { + return array( + "todoitemsflags" => PR_TODO_ITEM_FLAGS, + "todotitle" => "PT_STRING8:PSETID_Common:0x85A4", + "flagicon" => PR_FLAG_ICON, + "replyrequested" => PR_REPLY_REQUESTED, + "responserequested" => PR_RESPONSE_REQUESTED, + "status" => "PT_LONG:PSETID_Task:0x8101", + "completion" => "PT_DOUBLE:PSETID_Task:0x8102", + "complete" => "PT_BOOLEAN:PSETID_Task:0x811C", + ); + } + + + /** + * Returns the MAPI to AS mapping for notes + * + * @access public + * + * @return array + */ + public static function GetNoteMapping() { + return array( + "categories" => "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords", + "lastmodificationtime" => PR_LAST_MODIFICATION_TIME, + "messageclass" => PR_MESSAGE_CLASS, + "subject" => PR_SUBJECT, + ); + } + + + /** + * Returns note specific MAPI properties + * + * @access public + * + * @return array + */ + public static function GetNoteProperties() { + return array( + "body" => PR_BODY, + "messageclass" => PR_MESSAGE_CLASS, + "html" => PR_HTML, + "internetcpid" => PR_INTERNET_CPID, + + ); + } + + + /** + * Returns properties for sending an email + * + * @access public + * + * @return array + */ + public static function GetSendMailProperties() { + return array( + "outboxentryid" => PR_IPM_OUTBOX_ENTRYID, + "ipmsentmailentryid" => PR_IPM_SENTMAIL_ENTRYID, + "sentmailentryid" => PR_SENTMAIL_ENTRYID, + "subject" => PR_SUBJECT, + "messageclass" => PR_MESSAGE_CLASS, + "deliverytime" => PR_MESSAGE_DELIVERY_TIME, + "importance" => PR_IMPORTANCE, + "priority" => PR_PRIORITY, + "addrtype" => PR_ADDRTYPE, + "emailaddress" => PR_EMAIL_ADDRESS, + "displayname" => PR_DISPLAY_NAME, + "recipienttype" => PR_RECIPIENT_TYPE, + "entryid" => PR_ENTRYID, + "iconindex" => PR_ICON_INDEX, + "body" => PR_BODY, + "html" => PR_HTML, + "sentrepresentingname" => PR_SENT_REPRESENTING_NAME, + "sentrepresentingemail" => PR_SENT_REPRESENTING_EMAIL_ADDRESS, + "representingentryid" => PR_SENT_REPRESENTING_ENTRYID, + "sentrepresentingaddt" => PR_SENT_REPRESENTING_ADDRTYPE, + "sentrepresentinsrchk" => PR_SENT_REPRESENTING_SEARCH_KEY, + "displayto" => PR_DISPLAY_TO, + "displaycc" => PR_DISPLAY_CC, + "clientsubmittime" => PR_CLIENT_SUBMIT_TIME, + "attachnum" => PR_ATTACH_NUM, + "attachdatabin" => PR_ATTACH_DATA_BIN, + "internetcpid" => PR_INTERNET_CPID, + ); + } +} +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapiphpwrapper.php b/sources/backend/zarafa/mapiphpwrapper.php new file mode 100644 index 0000000..0b27c6a --- /dev/null +++ b/sources/backend/zarafa/mapiphpwrapper.php @@ -0,0 +1,228 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +/** + * This is the PHP wrapper which strips MAPI information from + * the import interface of ICS. We get all the information about messages + * from MAPI here which are sent to the next importer, which will + * convert the data into WBXML which is streamed to the PDA + */ + +class PHPWrapper { + private $importer; + private $mapiprovider; + private $store; + private $contentparameters; + + + /** + * Constructor of the PHPWrapper + * + * @param ressource $session + * @param ressource $store + * @param IImportChanges $importer incoming changes from ICS are forwarded here + * + * @access public + * @return + */ + public function PHPWrapper($session, $store, $importer) { + $this->importer = &$importer; + $this->store = $store; + $this->mapiprovider = new MAPIProvider($session, $this->store); + } + + /** + * Configures additional parameters used for content synchronization + * + * @param ContentParameters $contentparameters + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ConfigContentParameters($contentparameters) { + $this->contentparameters = $contentparameters; + } + + /** + * Implement MAPI interface + */ + public function Config($stream, $flags = 0) {} + public function GetLastError($hresult, $ulflags, &$lpmapierror) {} + public function UpdateState($stream) { } + + /** + * Imports a single message + * + * @param array $props + * @param long $flags + * @param object $retmapimessage + * + * @access public + * @return long + */ + public function ImportMessageChange($props, $flags, &$retmapimessage) { + $sourcekey = $props[PR_SOURCE_KEY]; + $parentsourcekey = $props[PR_PARENT_SOURCE_KEY]; + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $parentsourcekey, $sourcekey); + + if(!$entryid) + return SYNC_E_IGNORE; + + $mapimessage = mapi_msgstore_openentry($this->store, $entryid); + try { + $message = $this->mapiprovider->GetMessage($mapimessage, $this->contentparameters); + } + catch (SyncObjectBrokenException $mbe) { + $brokenSO = $mbe->GetSyncObject(); + if (!$brokenSO) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("PHPWrapper->ImportMessageChange(): Catched SyncObjectBrokenException but broken SyncObject available")); + } + else { + if (!isset($brokenSO->id)) { + $brokenSO->id = "Unknown ID"; + ZLog::Write(LOGLEVEL_ERROR, sprintf("PHPWrapper->ImportMessageChange(): Catched SyncObjectBrokenException but no ID of object set")); + } + ZPush::GetDeviceManager()->AnnounceIgnoredMessage(false, $brokenSO->id, $brokenSO); + } + // tell MAPI to ignore the message + return SYNC_E_IGNORE; + } + + + // substitute the MAPI SYNC_NEW_MESSAGE flag by a z-push proprietary flag + if ($flags == SYNC_NEW_MESSAGE) $message->flags = SYNC_NEWMESSAGE; + else $message->flags = $flags; + + $this->importer->ImportMessageChange(bin2hex($sourcekey), $message); + + // Tell MAPI it doesn't need to do anything itself, as we've done all the work already. + return SYNC_E_IGNORE; + } + + /** + * Imports a list of messages to be deleted + * + * @param long $flags + * @param array $sourcekeys array with sourcekeys + * + * @access public + * @return + */ + public function ImportMessageDeletion($flags, $sourcekeys) { + foreach($sourcekeys as $sourcekey) { + $this->importer->ImportMessageDeletion(bin2hex($sourcekey)); + } + } + + /** + * Imports a list of messages to be deleted + * + * @param mixed $readstates sourcekeys and message flags + * + * @access public + * @return + */ + public function ImportPerUserReadStateChange($readstates) { + foreach($readstates as $readstate) { + $this->importer->ImportMessageReadFlag(bin2hex($readstate["sourcekey"]), $readstate["flags"] & MSGFLAG_READ); + } + } + + /** + * Imports a message move + * this is never called by ICS + * + * @access public + * @return + */ + public function ImportMessageMove($sourcekeysrcfolder, $sourcekeysrcmessage, $message, $sourcekeydestmessage, $changenumdestmessage) { + // Never called + } + + /** + * Imports a single folder change + * + * @param mixed $props sourcekey of the changed folder + * + * @access public + * @return + */ + function ImportFolderChange($props) { + $sourcekey = $props[PR_SOURCE_KEY]; + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $sourcekey); + $mapifolder = mapi_msgstore_openentry($this->store, $entryid); + $folder = $this->mapiprovider->GetFolder($mapifolder); + + // do not import folder if there is something "wrong" with it + if ($folder === false) + return 0; + + $this->importer->ImportFolderChange($folder); + return 0; + } + + /** + * Imports a list of folders which are to be deleted + * + * @param long $flags + * @param mixed $sourcekeys array with sourcekeys + * + * @access public + * @return + */ + function ImportFolderDeletion($flags, $sourcekeys) { + foreach ($sourcekeys as $sourcekey) { + $this->importer->ImportFolderDeletion(bin2hex($sourcekey)); + } + return 0; + } +} + +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapiprovider.php b/sources/backend/zarafa/mapiprovider.php new file mode 100644 index 0000000..56834ed --- /dev/null +++ b/sources/backend/zarafa/mapiprovider.php @@ -0,0 +1,2623 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class MAPIProvider { + private $session; + private $store; + private $zRFC822; + private $addressbook; + + /** + * Constructor of the MAPI Provider + * Almost all methods of this class require a MAPI session and/or store + * + * @param ressource $session + * @param ressource $store + * + * @access public + */ + function MAPIProvider($session, $store) { + $this->session = $session; + $this->store = $store; + } + + + /**---------------------------------------------------------------------------------------------------------- + * GETTER + */ + + /** + * Reads a message from MAPI + * Depending on the message class, a contact, appointment, task or email is read + * + * @param mixed $mapimessage + * @param ContentParameters $contentparameters + * + * @access public + * @return SyncObject + */ + public function GetMessage($mapimessage, $contentparameters) { + // Gets the Sync object from a MAPI object according to its message class + + $props = mapi_getprops($mapimessage, array(PR_MESSAGE_CLASS)); + if(isset($props[PR_MESSAGE_CLASS])) + $messageclass = $props[PR_MESSAGE_CLASS]; + else + $messageclass = "IPM"; + + if(strpos($messageclass,"IPM.Contact") === 0) + return $this->getContact($mapimessage, $contentparameters); + else if(strpos($messageclass,"IPM.Appointment") === 0) + return $this->getAppointment($mapimessage, $contentparameters); + else if(strpos($messageclass,"IPM.Task") === 0) + return $this->getTask($mapimessage, $contentparameters); + else if(strpos($messageclass,"IPM.StickyNote") === 0) + return $this->getNote($mapimessage, $contentparameters); + else + return $this->getEmail($mapimessage, $contentparameters); + } + + /** + * Reads a contact object from MAPI + * + * @param mixed $mapimessage + * @param ContentParameters $contentparameters + * + * @access private + * @return SyncContact + */ + private function getContact($mapimessage, $contentparameters) { + $message = new SyncContact(); + + // Standard one-to-one mappings first + $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetContactMapping()); + + // Contact specific props + $contactproperties = MAPIMapping::GetContactProperties(); + $messageprops = $this->getProps($mapimessage, $contactproperties); + + //set the body according to contentparameters and supported AS version + $this->setMessageBody($mapimessage, $contentparameters, $message); + + //check the picture + if (isset($messageprops[$contactproperties["haspic"]]) && $messageprops[$contactproperties["haspic"]]) { + // Add attachments + $attachtable = mapi_message_getattachmenttable($mapimessage); + mapi_table_restrict($attachtable, MAPIUtils::GetContactPicRestriction()); + $rows = mapi_table_queryallrows($attachtable, array(PR_ATTACH_NUM, PR_ATTACH_SIZE)); + + foreach($rows as $row) { + if(isset($row[PR_ATTACH_NUM])) { + $mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]); + $message->picture = base64_encode(mapi_attach_openbin($mapiattach, PR_ATTACH_DATA_BIN)); + } + } + } + + return $message; + } + + /** + * Reads a task object from MAPI + * + * @param mixed $mapimessage + * @param ContentParameters $contentparameters + * + * @access private + * @return SyncTask + */ + private function getTask($mapimessage, $contentparameters) { + $message = new SyncTask(); + + // Standard one-to-one mappings first + $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetTaskMapping()); + + // Task specific props + $taskproperties = MAPIMapping::GetTaskProperties(); + $messageprops = $this->getProps($mapimessage, $taskproperties); + + //set the body according to contentparameters and supported AS version + $this->setMessageBody($mapimessage, $contentparameters, $message); + + //task with deadoccur is an occurrence of a recurring task and does not need to be handled as recurring + //webaccess does not set deadoccur for the initial recurring task + if(isset($messageprops[$taskproperties["isrecurringtag"]]) && + $messageprops[$taskproperties["isrecurringtag"]] && + (!isset($messageprops[$taskproperties["deadoccur"]]) || + (isset($messageprops[$taskproperties["deadoccur"]]) && + !$messageprops[$taskproperties["deadoccur"]]))) { + // Process recurrence + $message->recurrence = new SyncTaskRecurrence(); + $this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, false); + } + + // when set the task to complete using the WebAccess, the dateComplete property is not set correctly + if ($message->complete == 1 && !isset($message->datecompleted)) + $message->datecompleted = time(); + + // if no reminder is set, announce that to the mobile + if (!isset($message->reminderset)) + $message->reminderset = 0; + + return $message; + } + + /** + * Reads an appointment object from MAPI + * + * @param mixed $mapimessage + * @param ContentParameters $contentparameters + * + * @access private + * @return SyncAppointment + */ + private function getAppointment($mapimessage, $contentparameters) { + $message = new SyncAppointment(); + + // Standard one-to-one mappings first + $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetAppointmentMapping()); + + // Appointment specific props + $appointmentprops = MAPIMapping::GetAppointmentProperties(); + $messageprops = $this->getProps($mapimessage, $appointmentprops); + + //set the body according to contentparameters and supported AS version + $this->setMessageBody($mapimessage, $contentparameters, $message); + + // Set reminder time if reminderset is true + if(isset($messageprops[$appointmentprops["reminderset"]]) && $messageprops[$appointmentprops["reminderset"]] == true) { + if ($messageprops[$appointmentprops["remindertime"]] == 0x5AE980E1) + $message->reminder = 15; + else + $message->reminder = $messageprops[$appointmentprops["remindertime"]]; + } + + if(!isset($message->uid)) + $message->uid = bin2hex($messageprops[$appointmentprops["sourcekey"]]); + else + $message->uid = Utils::GetICalUidFromOLUid($message->uid); + + // Always set organizer information because some devices do not work properly without it + if( isset($messageprops[$appointmentprops["representingentryid"]]) && + isset($messageprops[$appointmentprops["representingname"]])) { + + $message->organizeremail = w2u($this->getSMTPAddressFromEntryID($messageprops[$appointmentprops["representingentryid"]])); + $message->organizername = w2u($messageprops[$appointmentprops["representingname"]]); + } + + if(isset($messageprops[$appointmentprops["timezonetag"]])) + $tz = $this->getTZFromMAPIBlob($messageprops[$appointmentprops["timezonetag"]]); + else { + // set server default timezone (correct timezone should be configured!) + $tz = TimezoneUtil::GetFullTZ(); + } + $message->timezone = base64_encode($this->getSyncBlobFromTZ($tz)); + + if(isset($messageprops[$appointmentprops["isrecurring"]]) && $messageprops[$appointmentprops["isrecurring"]]) { + // Process recurrence + $message->recurrence = new SyncRecurrence(); + $this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, $tz); + } + + // Do attendees + $reciptable = mapi_message_getrecipienttable($mapimessage); + // Only get first 256 recipients, to prevent possible load issues. + $rows = mapi_table_queryrows($reciptable, array(PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ADDRTYPE, PR_RECIPIENT_TRACKSTATUS, PR_RECIPIENT_TYPE), 0, 256); + + // Exception: we do not synchronize appointments with more than 250 attendees + if (count($rows) > 250) { + $message->id = bin2hex($messageprops[$appointmentprops["sourcekey"]]); + $mbe = new SyncObjectBrokenException("Appointment has too many attendees"); + $mbe->SetSyncObject($message); + throw $mbe; + } + + if(count($rows) > 0) + $message->attendees = array(); + + foreach($rows as $row) { + $attendee = new SyncAttendee(); + + $attendee->name = w2u($row[PR_DISPLAY_NAME]); + //smtp address is always a proper email address + if(isset($row[PR_SMTP_ADDRESS])) + $attendee->email = w2u($row[PR_SMTP_ADDRESS]); + elseif (isset($row[PR_ADDRTYPE]) && isset($row[PR_EMAIL_ADDRESS])) { + //if address type is SMTP, it's also a proper email address + if ($row[PR_ADDRTYPE] == "SMTP") + $attendee->email = w2u($row[PR_EMAIL_ADDRESS]); + //if address type is ZARAFA, the PR_EMAIL_ADDRESS contains username + elseif ($row[PR_ADDRTYPE] == "ZARAFA") { + $userinfo = @mapi_zarafa_getuser_by_name($this->store, $row[PR_EMAIL_ADDRESS]); + if (is_array($userinfo) && isset($userinfo["emailaddress"])) { + $attendee->email = w2u($userinfo["emailaddress"]); + } + else + ZLog::Write(LOGLEVEL_WARN, sprintf("MAPIProvider->getAppointment: The attendee '%s' of type ZARAFA can not be resolved. Code: 0x%X", $row[PR_EMAIL_ADDRESS], mapi_last_hresult())); + } + } + + //set attendee's status and type if they're available + if (isset($row[PR_RECIPIENT_TRACKSTATUS])) + $attendee->attendeestatus = $row[PR_RECIPIENT_TRACKSTATUS]; + if (isset($row[PR_RECIPIENT_TYPE])) + $attendee->attendeetype = $row[PR_RECIPIENT_TYPE]; + // Some attendees have no email or name (eg resources), and if you + // don't send one of those fields, the phone will give an error ... so + // we don't send it in that case. + // also ignore the "attendee" if the email is equal to the organizers' email + if(isset($attendee->name) && isset($attendee->email) && $attendee->email != "" && (!isset($message->organizeremail) || (isset($message->organizeremail) && $attendee->email != $message->organizeremail))) + array_push($message->attendees, $attendee); + } + + // Status 0 = no meeting, status 1 = organizer, status 2/3/4/5 = tentative/accepted/declined/notresponded + if(isset($messageprops[$appointmentprops["meetingstatus"]]) && $messageprops[$appointmentprops["meetingstatus"]] > 1) { + if (!isset($message->attendees) || !is_array($message->attendees)) + $message->attendees = array(); + // Work around iOS6 cancellation issue when there are no attendees for this meeting. Just add ourselves as the sole attendee. + if(count($message->attendees) == 0) { + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->getAppointment: adding ourself as an attendee for iOS6 workaround")); + $attendee = new SyncAttendee(); + + $meinfo = mapi_zarafa_getuser_by_name($this->store, Request::GetAuthUser()); + + if (is_array($meinfo)) { + $attendee->email = w2u($meinfo["emailaddress"]); + $attendee->name = w2u($meinfo["fullname"]); + $attendee->attendeetype = MAPI_TO; + + array_push($message->attendees, $attendee); + } + } + } + + if (!isset($message->nativebodytype)) $message->nativebodytype = $this->getNativeBodyType($messageprops); + + return $message; + } + + /** + * Reads recurrence information from MAPI + * + * @param mixed $mapimessage + * @param array $recurprops + * @param SyncObject &$syncMessage the message + * @param SyncObject &$syncRecurrence the recurrence message + * @param array $tz timezone information + * + * @access private + * @return + */ + private function getRecurrence($mapimessage, $recurprops, &$syncMessage, &$syncRecurrence, $tz) { + if ($syncRecurrence instanceof SyncTaskRecurrence) + $recurrence = new TaskRecurrence($this->store, $mapimessage); + else + $recurrence = new Recurrence($this->store, $mapimessage); + + switch($recurrence->recur["type"]) { + case 10: // daily + switch($recurrence->recur["subtype"]) { + default: + $syncRecurrence->type = 0; + break; + case 1: + $syncRecurrence->type = 0; + $syncRecurrence->dayofweek = 62; // mon-fri + $syncRecurrence->interval = 1; + break; + } + break; + case 11: // weekly + $syncRecurrence->type = 1; + break; + case 12: // monthly + switch($recurrence->recur["subtype"]) { + default: + $syncRecurrence->type = 2; + break; + case 3: + $syncRecurrence->type = 3; + break; + } + break; + case 13: // yearly + switch($recurrence->recur["subtype"]) { + default: + $syncRecurrence->type = 4; + break; + case 2: + $syncRecurrence->type = 5; + break; + case 3: + $syncRecurrence->type = 6; + } + } + // Termination + switch($recurrence->recur["term"]) { + case 0x21: + $syncRecurrence->until = $recurrence->recur["end"]; + // fixes Mantis #350 : recur-end does not consider timezones - use ClipEnd if available + if (isset($recurprops[$recurrence->proptags["enddate_recurring"]])) + $syncRecurrence->until = $recurprops[$recurrence->proptags["enddate_recurring"]]; + // add one day (minus 1 sec) to the end time to make sure the last occurrence is covered + $syncRecurrence->until += 86399; + break; + case 0x22: + $syncRecurrence->occurrences = $recurrence->recur["numoccur"]; break; + case 0x23: + // never ends + break; + } + + // Correct 'alldayevent' because outlook fails to set it on recurring items of 24 hours or longer + if(isset($recurrence->recur["endocc"], $recurrence->recur["startocc"]) && ($recurrence->recur["endocc"] - $recurrence->recur["startocc"] >= 1440)) + $syncMessage->alldayevent = true; + + // Interval is different according to the type/subtype + switch($recurrence->recur["type"]) { + case 10: + if($recurrence->recur["subtype"] == 0) + $syncRecurrence->interval = (int)($recurrence->recur["everyn"] / 1440); // minutes + break; + case 11: + case 12: + $syncRecurrence->interval = $recurrence->recur["everyn"]; + break; // months / weeks + case 13: + $syncRecurrence->interval = (int)($recurrence->recur["everyn"] / 12); + break; // months + } + + if(isset($recurrence->recur["weekdays"])) + $syncRecurrence->dayofweek = $recurrence->recur["weekdays"]; // bitmask of days (1 == sunday, 128 == saturday + if(isset($recurrence->recur["nday"])) + $syncRecurrence->weekofmonth = $recurrence->recur["nday"]; // N'th {DAY} of {X} (0-5) + if(isset($recurrence->recur["month"])) + $syncRecurrence->monthofyear = (int)($recurrence->recur["month"] / (60 * 24 * 29)) + 1; // works ok due to rounding. see also $monthminutes below (1-12) + if(isset($recurrence->recur["monthday"])) + $syncRecurrence->dayofmonth = $recurrence->recur["monthday"]; // day of month (1-31) + + // All changed exceptions are appointments within the 'exceptions' array. They contain the same items as a normal appointment + foreach($recurrence->recur["changed_occurences"] as $change) { + $exception = new SyncAppointmentException(); + + // start, end, basedate, subject, remind_before, reminderset, location, busystatus, alldayevent, label + if(isset($change["start"])) + $exception->starttime = $this->getGMTTimeByTZ($change["start"], $tz); + if(isset($change["end"])) + $exception->endtime = $this->getGMTTimeByTZ($change["end"], $tz); + if(isset($change["basedate"])) { + $exception->exceptionstarttime = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($change["basedate"]) + $recurrence->recur["startocc"] * 60, $tz); + + //open body because getting only property might not work because of memory limit + $exceptionatt = $recurrence->getExceptionAttachment($change["basedate"]); + if($exceptionatt) { + $exceptionobj = mapi_attach_openobj($exceptionatt, 0); + $this->setMessageBodyForType($exceptionobj, SYNC_BODYPREFERENCE_PLAIN, $exception); + } + } + if(isset($change["subject"])) + $exception->subject = w2u($change["subject"]); + if(isset($change["reminder_before"]) && $change["reminder_before"]) + $exception->reminder = $change["remind_before"]; + if(isset($change["location"])) + $exception->location = w2u($change["location"]); + if(isset($change["busystatus"])) + $exception->busystatus = $change["busystatus"]; + if(isset($change["alldayevent"])) + $exception->alldayevent = $change["alldayevent"]; + + // set some data from the original appointment + if (isset($syncMessage->uid)) + $exception->uid = $syncMessage->uid; + if (isset($syncMessage->organizername)) + $exception->organizername = $syncMessage->organizername; + if (isset($syncMessage->organizeremail)) + $exception->organizeremail = $syncMessage->organizeremail; + + if(!isset($syncMessage->exceptions)) + $syncMessage->exceptions = array(); + + array_push($syncMessage->exceptions, $exception); + } + + // Deleted appointments contain only the original date (basedate) and a 'deleted' tag + foreach($recurrence->recur["deleted_occurences"] as $deleted) { + $exception = new SyncAppointmentException(); + + $exception->exceptionstarttime = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($deleted) + $recurrence->recur["startocc"] * 60, $tz); + $exception->deleted = "1"; + + if(!isset($syncMessage->exceptions)) + $syncMessage->exceptions = array(); + + array_push($syncMessage->exceptions, $exception); + } + + if (isset($syncMessage->complete) && $syncMessage->complete) { + $syncRecurrence->complete = $syncMessage->complete; + } + } + + /** + * Reads an email object from MAPI + * + * @param mixed $mapimessage + * @param ContentParameters $contentparameters + * + * @access private + * @return SyncEmail + */ + private function getEmail($mapimessage, $contentparameters) { + $message = new SyncMail(); + + $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetEmailMapping()); + + $emailproperties = MAPIMapping::GetEmailProperties(); + $messageprops = $this->getProps($mapimessage, $emailproperties); + + if(isset($messageprops[PR_SOURCE_KEY])) + $sourcekey = $messageprops[PR_SOURCE_KEY]; + else + return false; + + //set the body according to contentparameters and supported AS version + $this->setMessageBody($mapimessage, $contentparameters, $message); + + $fromname = $fromaddr = ""; + + if(isset($messageprops[$emailproperties["representingname"]])) { + // remove encapsulating double quotes from the representingname + $fromname = preg_replace('/^\"(.*)\"$/',"\${1}", $messageprops[$emailproperties["representingname"]]); + } + if(isset($messageprops[$emailproperties["representingentryid"]])) + $fromaddr = $this->getSMTPAddressFromEntryID($messageprops[$emailproperties["representingentryid"]]); + + if($fromname == $fromaddr) + $fromname = ""; + + if($fromname) + $from = "\"" . w2u($fromname) . "\" <" . w2u($fromaddr) . ">"; + else + //START CHANGED dw2412 HTC shows "error" if sender name is unknown + $from = "\"" . w2u($fromaddr) . "\" <" . w2u($fromaddr) . ">"; + //END CHANGED dw2412 HTC shows "error" if sender name is unknown + + $message->from = $from; + + // process Meeting Requests + if(isset($message->messageclass) && strpos($message->messageclass, "IPM.Schedule.Meeting") === 0) { + $message->meetingrequest = new SyncMeetingRequest(); + $this->getPropsFromMAPI($message->meetingrequest, $mapimessage, MAPIMapping::GetMeetingRequestMapping()); + + $meetingrequestproperties = MAPIMapping::GetMeetingRequestProperties(); + $props = $this->getProps($mapimessage, $meetingrequestproperties); + + // Get the GOID + if(isset($props[$meetingrequestproperties["goidtag"]])) + $message->meetingrequest->globalobjid = base64_encode($props[$meetingrequestproperties["goidtag"]]); + + // Set Timezone + if(isset($props[$meetingrequestproperties["timezonetag"]])) + $tz = $this->getTZFromMAPIBlob($props[$meetingrequestproperties["timezonetag"]]); + else + $tz = $this->getGMTTZ(); + + $message->meetingrequest->timezone = base64_encode($this->getSyncBlobFromTZ($tz)); + + // send basedate if exception + if(isset($props[$meetingrequestproperties["recReplTime"]]) || + (isset($props[$meetingrequestproperties["lidIsException"]]) && $props[$meetingrequestproperties["lidIsException"]] == true)) { + if (isset($props[$meetingrequestproperties["recReplTime"]])){ + $basedate = $props[$meetingrequestproperties["recReplTime"]]; + $message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $this->getGMTTZ()); + } + else { + if (!isset($props[$meetingrequestproperties["goidtag"]]) || !isset($props[$meetingrequestproperties["recurStartTime"]]) || !isset($props[$meetingrequestproperties["timezonetag"]])) + ZLog::Write(LOGLEVEL_WARN, "Missing property to set correct basedate for exception"); + else { + $basedate = Utils::ExtractBaseDate($props[$meetingrequestproperties["goidtag"]], $props[$meetingrequestproperties["recurStartTime"]]); + $message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $tz); + } + } + } + + // Organizer is the sender + if (strpos($message->messageclass, "IPM.Schedule.Meeting.Resp") === 0) { + $message->meetingrequest->organizer = $message->to; + } + else { + $message->meetingrequest->organizer = $message->from; + } + + // Process recurrence + if(isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]]) { + $myrec = new SyncMeetingRequestRecurrence(); + // get recurrence -> put $message->meetingrequest as message so the 'alldayevent' is set correctly + $this->getRecurrence($mapimessage, $props, $message->meetingrequest, $myrec, $tz); + $message->meetingrequest->recurrences = array($myrec); + } + + // Force the 'alldayevent' in the object at all times. (non-existent == 0) + if(!isset($message->meetingrequest->alldayevent) || $message->meetingrequest->alldayevent == "") + $message->meetingrequest->alldayevent = 0; + + // Instancetype + // 0 = single appointment + // 1 = master recurring appointment + // 2 = single instance of recurring appointment + // 3 = exception of recurring appointment + $message->meetingrequest->instancetype = 0; + if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]] == 1) + $message->meetingrequest->instancetype = 1; + else if ((!isset($props[$meetingrequestproperties["isrecurringtag"]]) || $props[$meetingrequestproperties["isrecurringtag"]] == 0 )&& isset($message->meetingrequest->recurrenceid)) + if (isset($props[$meetingrequestproperties["appSeqNr"]]) && $props[$meetingrequestproperties["appSeqNr"]] == 0 ) + $message->meetingrequest->instancetype = 2; + else + $message->meetingrequest->instancetype = 3; + + // Disable reminder if it is off + if(!isset($props[$meetingrequestproperties["reminderset"]]) || $props[$meetingrequestproperties["reminderset"]] == false) + $message->meetingrequest->reminder = ""; + //the property saves reminder in minutes, but we need it in secs + else { + ///set the default reminder time to seconds + if ($props[$meetingrequestproperties["remindertime"]] == 0x5AE980E1) + $message->meetingrequest->reminder = 900; + else + $message->meetingrequest->reminder = $props[$meetingrequestproperties["remindertime"]] * 60; + } + + // Set sensitivity to 0 if missing + if(!isset($message->meetingrequest->sensitivity)) + $message->meetingrequest->sensitivity = 0; + + // if a meeting request response hasn't been processed yet, + // do it so that the attendee status is updated on the mobile + if(!isset($messageprops[$emailproperties["processed"]])) { + $req = new Meetingrequest($this->store, $mapimessage, $this->session); + if ($req->isMeetingRequestResponse()) { + $req->processMeetingRequestResponse(); + } + if ($req->isMeetingCancellation()) { + $req->processMeetingCancellation(); + } + } + } + + // Add attachments + $attachtable = mapi_message_getattachmenttable($mapimessage); + $rows = mapi_table_queryallrows($attachtable, array(PR_ATTACH_NUM)); + $entryid = bin2hex($messageprops[$emailproperties["entryid"]]); + + foreach($rows as $row) { + if(isset($row[PR_ATTACH_NUM])) { + if (Request::GetProtocolVersion() >= 12.0) { + $attach = new SyncBaseAttachment(); + } + else { + $attach = new SyncAttachment(); + } + + $mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]); + $attachprops = mapi_getprops($mapiattach, array(PR_ATTACH_LONG_FILENAME, PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_ATTACH_CONTENT_ID, PR_ATTACH_CONTENT_ID_W, PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W, PR_ATTACH_METHOD, PR_DISPLAY_NAME, PR_DISPLAY_NAME_W, PR_ATTACH_SIZE)); + if ((isset($attachprops[PR_ATTACH_MIME_TAG]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG]), 'signed') !== false) || + (isset($attachprops[PR_ATTACH_MIME_TAG_W]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG_W]), 'signed') !== false)) { + continue; + } + + // the displayname is handled equaly for all AS versions + $attach->displayname = w2u((isset($attachprops[PR_ATTACH_LONG_FILENAME])) ? $attachprops[PR_ATTACH_LONG_FILENAME] : ((isset($attachprops[PR_ATTACH_FILENAME])) ? $attachprops[PR_ATTACH_FILENAME] : ((isset($attachprops[PR_DISPLAY_NAME])) ? $attachprops[PR_DISPLAY_NAME] : "attachment.bin"))); + // fix attachment name in case of inline images + if ($attach->displayname == "inline.txt" && (isset($attachprops[PR_ATTACH_MIME_TAG]) || $attachprops[PR_ATTACH_MIME_TAG_W])) { + $mimetype = (isset($attachprops[PR_ATTACH_MIME_TAG])) ? $attachprops[PR_ATTACH_MIME_TAG]:$attachprops[PR_ATTACH_MIME_TAG_W]; + $mime = explode("/", $mimetype); + + if (count($mime) == 2 && $mime[0] == "image") { + $attach->displayname = "inline." . $mime[1]; + } + } + + // set AS version specific parameters + if (Request::GetProtocolVersion() >= 12.0) { + $attach->filereference = $entryid.":".$row[PR_ATTACH_NUM]; + $attach->method = (isset($attachprops[PR_ATTACH_METHOD])) ? $attachprops[PR_ATTACH_METHOD] : ATTACH_BY_VALUE; + + // if displayname does not have the eml extension for embedde messages, android and WP devices won't open it + if ($attach->method == ATTACH_EMBEDDED_MSG) { + if (strtolower(substr($attach->displayname, -4)) != '.eml') + $attach->displayname .= '.eml'; + } + $attach->estimatedDataSize = $attachprops[PR_ATTACH_SIZE]; + + if (isset($attachprops[PR_ATTACH_CONTENT_ID]) && $attachprops[PR_ATTACH_CONTENT_ID]) + $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID]; + + if (!isset($attach->contentid) && isset($attachprops[PR_ATTACH_CONTENT_ID_W]) && $attachprops[PR_ATTACH_CONTENT_ID_W]) + $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID_W]; + + if (isset($attachprops[PR_ATTACHMENT_HIDDEN]) && $attachprops[PR_ATTACHMENT_HIDDEN]) $attach->isinline = 1; + + if(!isset($message->asattachments)) + $message->asattachments = array(); + + array_push($message->asattachments, $attach); + } + else { + $attach->attsize = $attachprops[PR_ATTACH_SIZE]; + $attach->attname = $entryid.":".$row[PR_ATTACH_NUM]; + if(!isset($message->attachments)) + $message->attachments = array(); + + array_push($message->attachments, $attach); + } + } + } + + // Get To/Cc as SMTP addresses (this is different from displayto and displaycc because we are putting + // in the SMTP addresses as well, while displayto and displaycc could just contain the display names + $message->to = array(); + $message->cc = array(); + + $reciptable = mapi_message_getrecipienttable($mapimessage); + $rows = mapi_table_queryallrows($reciptable, array(PR_RECIPIENT_TYPE, PR_DISPLAY_NAME, PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ENTRYID)); + + foreach ($rows as $row) { + $address = ""; + $fulladdr = ""; + + $addrtype = isset($row[PR_ADDRTYPE]) ? $row[PR_ADDRTYPE] : ""; + + if (isset($row[PR_SMTP_ADDRESS])) + $address = $row[PR_SMTP_ADDRESS]; + elseif ($addrtype == "SMTP" && isset($row[PR_EMAIL_ADDRESS])) + $address = $row[PR_EMAIL_ADDRESS]; + elseif ($addrtype == "ZARAFA" && isset($row[PR_ENTRYID])) + $address = $this->getSMTPAddressFromEntryID($row[PR_ENTRYID]); + + $name = isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : ""; + + if($name == "" || $name == $address) + $fulladdr = w2u($address); + else { + if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') { + $fulladdr = "\"" . w2u($name) ."\" <" . w2u($address) . ">"; + } + else { + $fulladdr = w2u($name) ."<" . w2u($address) . ">"; + } + } + + if($row[PR_RECIPIENT_TYPE] == MAPI_TO) { + array_push($message->to, $fulladdr); + } else if($row[PR_RECIPIENT_TYPE] == MAPI_CC) { + array_push($message->cc, $fulladdr); + } + } + + if (is_array($message->to) && !empty($message->to)) $message->to = implode(", ", $message->to); + if (is_array($message->cc) && !empty($message->cc)) $message->cc = implode(", ", $message->cc); + + // without importance some mobiles assume "0" (low) - Mantis #439 + if (!isset($message->importance)) + $message->importance = IMPORTANCE_NORMAL; + + //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->nativebodytype)) $message->nativebodytype = $this->getNativeBodyType($messageprops); + + // reply, reply to all, forward flags + if (isset($message->lastverbexecuted) && $message->lastverbexecuted) { + $message->lastverbexecuted = Utils::GetLastVerbExecuted($message->lastverbexecuted); + } + + return $message; + } + + /** + * Reads a note object from MAPI + * + * @param mixed $mapimessage + * @param ContentParameters $contentparameters + * + * @access private + * @return SyncNote + */ + private function getNote($mapimessage, $contentparameters) { + $message = new SyncNote(); + + // Standard one-to-one mappings first + $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetNoteMapping()); + + //set the body according to contentparameters and supported AS version + $this->setMessageBody($mapimessage, $contentparameters, $message); + + return $message; + } + + /** + * Reads a folder object from MAPI + * + * @param mixed $mapimessage + * + * @access public + * @return SyncFolder + */ + public function GetFolder($mapifolder) { + $folder = new SyncFolder(); + + $folderprops = mapi_getprops($mapifolder, array(PR_DISPLAY_NAME, PR_PARENT_ENTRYID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_ENTRYID, PR_CONTAINER_CLASS, PR_ATTR_HIDDEN)); + $storeprops = mapi_getprops($this->store, array(PR_IPM_SUBTREE_ENTRYID)); + + if(!isset($folderprops[PR_DISPLAY_NAME]) || + !isset($folderprops[PR_PARENT_ENTRYID]) || + !isset($folderprops[PR_SOURCE_KEY]) || + !isset($folderprops[PR_ENTRYID]) || + !isset($folderprops[PR_PARENT_SOURCE_KEY]) || + !isset($storeprops[PR_IPM_SUBTREE_ENTRYID])) { + ZLog::Write(LOGLEVEL_ERROR, "MAPIProvider->GetFolder(): invalid folder. Missing properties"); + return false; + } + + // ignore hidden folders + if (isset($folderprops[PR_ATTR_HIDDEN]) && $folderprops[PR_ATTR_HIDDEN] != false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->GetFolder(): invalid folder '%s' as it is a hidden folder (PR_ATTR_HIDDEN)", $folderprops[PR_DISPLAY_NAME])); + return false; + } + + $folder->serverid = bin2hex($folderprops[PR_SOURCE_KEY]); + if($folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_SUBTREE_ENTRYID]) + $folder->parentid = "0"; + else + $folder->parentid = bin2hex($folderprops[PR_PARENT_SOURCE_KEY]); + $folder->displayname = w2u($folderprops[PR_DISPLAY_NAME]); + $folder->type = $this->GetFolderType($folderprops[PR_ENTRYID], isset($folderprops[PR_CONTAINER_CLASS])?$folderprops[PR_CONTAINER_CLASS]:false); + + return $folder; + } + + /** + * Returns the foldertype for an entryid + * Gets the folder type by checking the default folders in MAPI + * + * @param string $entryid + * @param string $class (opt) + * + * @access public + * @return long + */ + public function GetFolderType($entryid, $class = false) { + $storeprops = mapi_getprops($this->store, array(PR_IPM_OUTBOX_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID, PR_IPM_SENTMAIL_ENTRYID)); + $inbox = mapi_msgstore_getreceivefolder($this->store); + $inboxprops = mapi_getprops($inbox, array(PR_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_TASK_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_JOURNAL_ENTRYID)); + + if($entryid == $inboxprops[PR_ENTRYID]) + return SYNC_FOLDER_TYPE_INBOX; + if($entryid == $inboxprops[PR_IPM_DRAFTS_ENTRYID]) + return SYNC_FOLDER_TYPE_DRAFTS; + if($entryid == $storeprops[PR_IPM_WASTEBASKET_ENTRYID]) + return SYNC_FOLDER_TYPE_WASTEBASKET; + if($entryid == $storeprops[PR_IPM_SENTMAIL_ENTRYID]) + return SYNC_FOLDER_TYPE_SENTMAIL; + if($entryid == $storeprops[PR_IPM_OUTBOX_ENTRYID]) + return SYNC_FOLDER_TYPE_OUTBOX; + if($entryid == $inboxprops[PR_IPM_TASK_ENTRYID]) + return SYNC_FOLDER_TYPE_TASK; + if($entryid == $inboxprops[PR_IPM_APPOINTMENT_ENTRYID]) + return SYNC_FOLDER_TYPE_APPOINTMENT; + if($entryid == $inboxprops[PR_IPM_CONTACT_ENTRYID]) + return SYNC_FOLDER_TYPE_CONTACT; + if($entryid == $inboxprops[PR_IPM_NOTE_ENTRYID]) + return SYNC_FOLDER_TYPE_NOTE; + if($entryid == $inboxprops[PR_IPM_JOURNAL_ENTRYID]) + return SYNC_FOLDER_TYPE_JOURNAL; + + // user created folders + if ($class == "IPF.Note") + return SYNC_FOLDER_TYPE_USER_MAIL; + if ($class == "IPF.Task") + return SYNC_FOLDER_TYPE_USER_TASK; + if ($class == "IPF.Appointment") + return SYNC_FOLDER_TYPE_USER_APPOINTMENT; + if ($class == "IPF.Contact") + return SYNC_FOLDER_TYPE_USER_CONTACT; + if ($class == "IPF.StickyNote") + return SYNC_FOLDER_TYPE_USER_NOTE; + if ($class == "IPF.Journal") + return SYNC_FOLDER_TYPE_USER_JOURNAL; + + return SYNC_FOLDER_TYPE_OTHER; + } + + /** + * Indicates if the entry id is a default MAPI folder + * + * @param string $entryid + * + * @access public + * @return boolean + */ + public function IsMAPIDefaultFolder($entryid) { + $msgstore_props = mapi_getprops($this->store, array(PR_ENTRYID, PR_DISPLAY_NAME, PR_IPM_SUBTREE_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER, PR_IPM_PUBLIC_FOLDERS_ENTRYID, PR_IPM_FAVORITES_ENTRYID, PR_MAILBOX_OWNER_ENTRYID)); + + $inboxProps = array(); + $inbox = mapi_msgstore_getreceivefolder($this->store); + if(!mapi_last_hresult()) + $inboxProps = mapi_getprops($inbox, array(PR_ENTRYID)); + + $root = mapi_msgstore_openentry($this->store, null); + $rootProps = mapi_getprops($root, array(PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_JOURNAL_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_TASK_ENTRYID, PR_ADDITIONAL_REN_ENTRYIDS)); + + $additional_ren_entryids = array(); + if(isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) + $additional_ren_entryids = $rootProps[PR_ADDITIONAL_REN_ENTRYIDS]; + + $defaultfolders = array( + "inbox" => array("inbox"=>PR_ENTRYID), + "outbox" => array("store"=>PR_IPM_OUTBOX_ENTRYID), + "sent" => array("store"=>PR_IPM_SENTMAIL_ENTRYID), + "wastebasket" => array("store"=>PR_IPM_WASTEBASKET_ENTRYID), + "favorites" => array("store"=>PR_IPM_FAVORITES_ENTRYID), + "publicfolders" => array("store"=>PR_IPM_PUBLIC_FOLDERS_ENTRYID), + "calendar" => array("root" =>PR_IPM_APPOINTMENT_ENTRYID), + "contact" => array("root" =>PR_IPM_CONTACT_ENTRYID), + "drafts" => array("root" =>PR_IPM_DRAFTS_ENTRYID), + "journal" => array("root" =>PR_IPM_JOURNAL_ENTRYID), + "note" => array("root" =>PR_IPM_NOTE_ENTRYID), + "task" => array("root" =>PR_IPM_TASK_ENTRYID), + "junk" => array("additional" =>4), + "syncissues" => array("additional" =>1), + "conflicts" => array("additional" =>0), + "localfailures" => array("additional" =>2), + "serverfailures" => array("additional" =>3), + ); + + foreach($defaultfolders as $key=>$prop){ + $tag = reset($prop); + $from = key($prop); + switch($from){ + case "inbox": + if(isset($inboxProps[$tag]) && $entryid == $inboxProps[$tag]) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Inbox found, key '%s'", $key)); + return true; + } + break; + case "store": + if(isset($msgstore_props[$tag]) && $entryid == $msgstore_props[$tag]) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Store folder found, key '%s'", $key)); + return true; + } + break; + case "root": + if(isset($rootProps[$tag]) && $entryid == $rootProps[$tag]) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Root folder found, key '%s'", $key)); + return true; + } + break; + case "additional": + if(isset($additional_ren_entryids[$tag]) && $entryid == $additional_ren_entryids[$tag]) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Additional folder found, key '%s'", $key)); + return true; + } + } + } + return false; + } + + /**---------------------------------------------------------------------------------------------------------- + * SETTER + */ + + /** + * Writes a SyncObject to MAPI + * Depending on the message class, a contact, appointment, task or email is written + * + * @param mixed $mapimessage + * @param SyncObject $message + * + * @access public + * @return boolean + */ + public function SetMessage($mapimessage, $message) { + // TODO check with instanceof + switch(strtolower(get_class($message))) { + case "synccontact": + return $this->setContact($mapimessage, $message); + case "syncappointment": + return $this->setAppointment($mapimessage, $message); + case "synctask": + return $this->setTask($mapimessage, $message); + case "syncnote": + return $this->setNote($mapimessage, $message); + default: + //for emails only flag (read and todo) changes are possible + return $this->setEmail($mapimessage, $message); + } + } + + /** + * Writes SyncMail to MAPI (actually flags only) + * + * @param mixed $mapimessage + * @param SyncMail $message + */ + private function setEmail($mapimessage, $message) { + $flagmapping = MAPIMapping::GetMailFlagsMapping(); + $flagprops = MAPIMapping::GetMailFlagsProperties(); + $flagprops = array_merge($this->getPropIdsFromStrings($flagmapping), $this->getPropIdsFromStrings($flagprops)); + // flag specific properties to be set + $props = $delprops = array(); + // unset message flags if: + // flag is not set + if (empty($message->flag) || + // flag status is not set + !isset($message->flag->flagstatus) || + // flag status is 0 or empty + (isset($message->flag->flagstatus) && ($message->flag->flagstatus == 0 || $message->flag->flagstatus == "")) ) { + // if message flag is empty, some properties need to be deleted + // and some set to 0 or false + + $props[$flagprops["todoitemsflags"]] = 0; + $props[$flagprops["status"]] = 0; + $props[$flagprops["completion"]] = 0.0; + $props[$flagprops["flagtype"]] = ""; + $props[$flagprops["ordinaldate"]] = 0x7fffffff; // ordinal date is 12am 1.1.4501, set it to max possible value + $props[$flagprops["subordinaldate"]] = ""; + $props[$flagprops["replyrequested"]] = false; + $props[$flagprops["responserequested"]] = false; + $props[$flagprops["reminderset"]] = false; + $props[$flagprops["complete"]] = false; + + $delprops[] = $flagprops["todotitle"]; + $delprops[] = $flagprops["duedate"]; + $delprops[] = $flagprops["startdate"]; + $delprops[] = $flagprops["datecompleted"]; + $delprops[] = $flagprops["utcstartdate"]; + $delprops[] = $flagprops["utcduedate"]; + $delprops[] = $flagprops["completetime"]; + $delprops[] = $flagprops["flagstatus"]; + $delprops[] = $flagprops["flagicon"]; + } + else { + $this->setPropsInMAPI($mapimessage, $message->flag, $flagmapping); + $props[$flagprops["todoitemsflags"]] = 1; + if (isset($message->subject) && str_len($message->subject) > 0) + $props[$flagprops["todotitle"]] = $message->subject; + // ordinal date is utc current time + if (!isset($message->flag->ordinaldate) || empty($message->flag->ordinaldate)) { + $props[$flagprops["ordinaldate"]] = time(); + } + // the default value + if (!isset($message->flag->subordinaldate) || empty($message->flag->subordinaldate)) { + $props[$flagprops["subordinaldate"]] = "5555555"; + } + $props[$flagprops["flagicon"]] = 6; //red flag icon + $props[$flagprops["replyrequested"]] = true; + $props[$flagprops["responserequested"]] = true; + + if ($message->flag->flagstatus == SYNC_FLAGSTATUS_COMPLETE) { + $props[$flagprops["status"]] = olTaskComplete; + $props[$flagprops["completion"]] = 1.0; + $props[$flagprops["complete"]] = true; + $props[$flagprops["replyrequested"]] = false; + $props[$flagprops["responserequested"]] = false; + unset($props[$flagprops["flagicon"]]); + $delprops[] = $flagprops["flagicon"]; + } + } + + if (!empty($props)) { + mapi_setprops($mapimessage, $props); + } + if (!empty($delprops)) { + mapi_deleteprops($mapimessage, $delprops); + } + } + + /** + * Writes a SyncAppointment to MAPI + * + * @param mixed $mapimessage + * @param SyncAppointment $message + * + * @access private + * @return boolean + */ + private function setAppointment($mapimessage, $appointment) { + // Get timezone info + if(isset($appointment->timezone)) + $tz = $this->getTZFromSyncBlob(base64_decode($appointment->timezone)); + else + $tz = false; + + //calculate duration because without it some webaccess views are broken. duration is in min + $localstart = $this->getLocaltimeByTZ($appointment->starttime, $tz); + $localend = $this->getLocaltimeByTZ($appointment->endtime, $tz); + $duration = ($localend - $localstart)/60; + + //nokia sends an yearly event with 0 mins duration but as all day event, + //so make it end next day + if ($appointment->starttime == $appointment->endtime && isset($appointment->alldayevent) && $appointment->alldayevent) { + $duration = 1440; + $appointment->endtime = $appointment->starttime + 24 * 60 * 60; + $localend = $localstart + 24 * 60 * 60; + } + + // is the transmitted UID OL compatible? + // if not, encapsulate the transmitted uid + $appointment->uid = Utils::GetOLUidFromICalUid($appointment->uid); + + mapi_setprops($mapimessage, array(PR_MESSAGE_CLASS => "IPM.Appointment")); + + $appointmentmapping = MAPIMapping::GetAppointmentMapping(); + $this->setPropsInMAPI($mapimessage, $appointment, $appointmentmapping); + $appointmentprops = MAPIMapping::GetAppointmentProperties(); + $appointmentprops = array_merge($this->getPropIdsFromStrings($appointmentmapping), $this->getPropIdsFromStrings($appointmentprops)); + //appointment specific properties to be set + $props = array(); + + //we also have to set the responsestatus and not only meetingstatus, so we use another mapi tag + $props[$appointmentprops["responsestatus"]] = (isset($appointment->responsestatus)) ? $appointment->responsestatus : olResponseNone; + + //sensitivity is not enough to mark an appointment as private, so we use another mapi tag + $private = (isset($appointment->sensitivity) && $appointment->sensitivity == 0) ? false : true; + + // Set commonstart/commonend to start/end and remindertime to start, duration, private and cleanGlobalObjectId + $props[$appointmentprops["commonstart"]] = $appointment->starttime; + $props[$appointmentprops["commonend"]] = $appointment->endtime; + $props[$appointmentprops["reminderstart"]] = $appointment->starttime; + // Set reminder boolean to 'true' if reminder is set + $props[$appointmentprops["reminderset"]] = isset($appointment->reminder) ? true : false; + $props[$appointmentprops["duration"]] = $duration; + $props[$appointmentprops["private"]] = $private; + $props[$appointmentprops["uid"]] = $appointment->uid; + // Set named prop 8510, unknown property, but enables deleting a single occurrence of a recurring + // type in OLK2003. + $props[$appointmentprops["sideeffects"]] = 369; + + + if(isset($appointment->reminder) && $appointment->reminder >= 0) { + // Set 'flagdueby' to correct value (start - reminderminutes) + $props[$appointmentprops["flagdueby"]] = $appointment->starttime - $appointment->reminder * 60; + $props[$appointmentprops["remindertime"]] = $appointment->reminder; + } + // unset the reminder + else { + $props[$appointmentprops["reminderset"]] = false; + } + + if (isset($appointment->asbody)) { + $this->setASbody($appointment->asbody, $props, $appointmentprops); + } + + if(isset($appointment->recurrence)) { + // Set PR_ICON_INDEX to 1025 to show correct icon in category view + $props[$appointmentprops["icon"]] = 1025; + + //if there aren't any exceptions, use the 'old style' set recurrence + $noexceptions = true; + + $recurrence = new Recurrence($this->store, $mapimessage); + $recur = array(); + $this->setRecurrence($appointment, $recur); + + // set the recurrence type to that of the MAPI + $props[$appointmentprops["recurrencetype"]] = $recur["recurrencetype"]; + + $starttime = $this->gmtime($localstart); + $endtime = $this->gmtime($localend); + + //set recurrence start here because it's calculated differently for tasks and appointments + $recur["start"] = $this->getDayStartOfTimestamp($this->getGMTTimeByTZ($localstart, $tz)); + + $recur["startocc"] = $starttime["tm_hour"] * 60 + $starttime["tm_min"]; + $recur["endocc"] = $recur["startocc"] + $duration; // Note that this may be > 24*60 if multi-day + + //only tasks can regenerate + $recur["regen"] = false; + + // Process exceptions. The PDA will send all exceptions for this recurring item. + if(isset($appointment->exceptions)) { + foreach($appointment->exceptions as $exception) { + // we always need the base date + if(!isset($exception->exceptionstarttime)) + continue; + + if(isset($exception->deleted) && $exception->deleted) { + // Delete exception + if(!isset($recur["deleted_occurences"])) + $recur["deleted_occurences"] = array(); + + array_push($recur["deleted_occurences"], $this->getDayStartOfTimestamp($exception->exceptionstarttime)); + } else { + // Change exception + $basedate = $this->getDayStartOfTimestamp($exception->exceptionstarttime); + $mapiexception = array("basedate" => $basedate); + //other exception properties which are not handled in recurrence + $exceptionprops = array(); + + if(isset($exception->starttime)) { + $mapiexception["start"] = $this->getLocaltimeByTZ($exception->starttime, $tz); + $exceptionprops[$appointmentprops["starttime"]] = $exception->starttime; + } + if(isset($exception->endtime)) { + $mapiexception["end"] = $this->getLocaltimeByTZ($exception->endtime, $tz); + $exceptionprops[$appointmentprops["endtime"]] = $exception->endtime; + } + if(isset($exception->subject)) + $exceptionprops[$appointmentprops["subject"]] = $mapiexception["subject"] = u2w($exception->subject); + if(isset($exception->location)) + $exceptionprops[$appointmentprops["location"]] = $mapiexception["location"] = u2w($exception->location); + if(isset($exception->busystatus)) + $exceptionprops[$appointmentprops["busystatus"]] = $mapiexception["busystatus"] = $exception->busystatus; + if(isset($exception->reminder)) { + $exceptionprops[$appointmentprops["reminderset"]] = $mapiexception["reminder_set"] = 1; + $exceptionprops[$appointmentprops["remindertime"]] = $mapiexception["remind_before"] = $exception->reminder; + } + if(isset($exception->alldayevent)) + $exceptionprops[$appointmentprops["alldayevent"]] = $mapiexception["alldayevent"] = $exception->alldayevent; + + + if(!isset($recur["changed_occurences"])) + $recur["changed_occurences"] = array(); + + if (isset($exception->body)) + $exceptionprops[$appointmentprops["body"]] = u2w($exception->body); + + if (isset($exception->asbody)) { + $this->setASbody($exception->asbody, $exceptionprops, $appointmentprops); + $mapiexception["body"] = $exceptionprops[$appointmentprops["body"]] = + (isset($exceptionprops[$appointmentprops["body"]])) ? $exceptionprops[$appointmentprops["body"]] : + ((isset($exceptionprops[$appointmentprops["html"]])) ? $exceptionprops[$appointmentprops["html"]] : ""); + } + + array_push($recur["changed_occurences"], $mapiexception); + + if (!empty($exceptionprops)) { + $noexceptions = false; + if($recurrence->isException($basedate)){ + $recurrence->modifyException($exceptionprops, $basedate); + } + else { + $recurrence->createException($exceptionprops, $basedate); + } + } + + } + } + } + + //setRecurrence deletes the attachments from an appointment + if ($noexceptions) { + $recurrence->setRecurrence($tz, $recur); + } + } + else { + $props[$appointmentprops["isrecurring"]] = false; + } + + //always set the PR_SENT_REPRESENTING_* props so that the attendee status update also works with the webaccess + $p = array( $appointmentprops["representingentryid"], $appointmentprops["representingname"], $appointmentprops["sentrepresentingaddt"], + $appointmentprops["sentrepresentingemail"], $appointmentprops["sentrepresentinsrchk"]); + $representingprops = $this->getProps($mapimessage, $p); + + if (!isset($representingprops[$appointmentprops["representingentryid"]])) { + $storeProps = mapi_getprops($this->store, array(PR_MAILBOX_OWNER_ENTRYID)); + $props[$appointmentprops["representingentryid"]] = $storeProps[PR_MAILBOX_OWNER_ENTRYID]; + $displayname = $this->getFullnameFromEntryID($storeProps[PR_MAILBOX_OWNER_ENTRYID]); + + $props[$appointmentprops["representingname"]] = ($displayname !== false) ? $displayname : Request::GetAuthUser(); + $props[$appointmentprops["sentrepresentingemail"]] = Request::GetAuthUser(); + $props[$appointmentprops["sentrepresentingaddt"]] = "ZARAFA"; + $props[$appointmentprops["sentrepresentinsrchk"]] = $props[$appointmentprops["sentrepresentingaddt"]].":".$props[$appointmentprops["sentrepresentingemail"]]; + + if(isset($appointment->attendees) && is_array($appointment->attendees) && !empty($appointment->attendees)) { + $props[$appointmentprops["icon"]] = 1026; + // the user is the organizer + // set these properties to show tracking tab in webapp + + $props[$appointmentprops["mrwassent"]] = true; + $props[$appointmentprops["responsestatus"]] = olResponseOrganized; + $props[$appointmentprops["meetingstatus"]] = olMeeting; + } + } + + // Do attendees + if(isset($appointment->attendees) && is_array($appointment->attendees)) { + $recips = array(); + + // Outlook XP requires organizer in the attendee list as well + $org = array(); + $org[PR_ENTRYID] = isset($representingprops[$appointmentprops["representingentryid"]]) ? $representingprops[$appointmentprops["representingentryid"]] : $props[$appointmentprops["representingentryid"]]; + $org[PR_DISPLAY_NAME] = isset($representingprops[$appointmentprops["representingname"]]) ? $representingprops[$appointmentprops["representingname"]] : $props[$appointmentprops["representingname"]]; + $org[PR_ADDRTYPE] = isset($representingprops[$appointmentprops["sentrepresentingaddt"]]) ? $representingprops[$appointmentprops["sentrepresentingaddt"]] : $props[$appointmentprops["sentrepresentingaddt"]]; + $org[PR_EMAIL_ADDRESS] = isset($representingprops[$appointmentprops["sentrepresentingemail"]]) ? $representingprops[$appointmentprops["sentrepresentingemail"]] : $props[$appointmentprops["sentrepresentingemail"]]; + $org[PR_SEARCH_KEY] = isset($representingprops[$appointmentprops["sentrepresentinsrchk"]]) ? $representingprops[$appointmentprops["sentrepresentinsrchk"]] : $props[$appointmentprops["sentrepresentinsrchk"]]; + $org[PR_RECIPIENT_FLAGS] = recipOrganizer | recipSendable; + $org[PR_RECIPIENT_TYPE] = MAPI_TO; + + array_push($recips, $org); + + //open addresss book for user resolve + $addrbook = $this->getAddressbook(); + foreach($appointment->attendees as $attendee) { + $recip = array(); + $recip[PR_EMAIL_ADDRESS] = u2w($attendee->email); + + // lookup information in GAB if possible so we have up-to-date name for given address + $userinfo = array( array( PR_DISPLAY_NAME => $recip[PR_EMAIL_ADDRESS] ) ); + $userinfo = mapi_ab_resolvename($addrbook, $userinfo, EMS_AB_ADDRESS_LOOKUP); + if(mapi_last_hresult() == NOERROR) { + $recip[PR_DISPLAY_NAME] = $userinfo[0][PR_DISPLAY_NAME]; + $recip[PR_EMAIL_ADDRESS] = $userinfo[0][PR_EMAIL_ADDRESS]; + $recip[PR_SEARCH_KEY] = $userinfo[0][PR_SEARCH_KEY]; + $recip[PR_ADDRTYPE] = $userinfo[0][PR_ADDRTYPE]; + $recip[PR_ENTRYID] = $userinfo[0][PR_ENTRYID]; + $recip[PR_RECIPIENT_TYPE] = MAPI_TO; + $recip[PR_RECIPIENT_FLAGS] = recipSendable; + } + else { + $recip[PR_DISPLAY_NAME] = u2w($attendee->name); + $recip[PR_SEARCH_KEY] = "SMTP:".$recip[PR_EMAIL_ADDRESS]."\0"; + $recip[PR_ADDRTYPE] = "SMTP"; + $recip[PR_RECIPIENT_TYPE] = MAPI_TO; + $recip[PR_ENTRYID] = mapi_createoneoff($recip[PR_DISPLAY_NAME], $recip[PR_ADDRTYPE], $recip[PR_EMAIL_ADDRESS]); + } + + array_push($recips, $recip); + } + + mapi_message_modifyrecipients($mapimessage, 0, $recips); + } + mapi_setprops($mapimessage, $props); + } + + /** + * Writes a SyncContact to MAPI + * + * @param mixed $mapimessage + * @param SyncContact $contact + * + * @access private + * @return boolean + */ + private function setContact($mapimessage, $contact) { + mapi_setprops($mapimessage, array(PR_MESSAGE_CLASS => "IPM.Contact")); + + // normalize email addresses + if (isset($contact->email1address) && (($contact->email1address = $this->extractEmailAddress($contact->email1address)) === false)) + unset($contact->email1address); + + if (isset($contact->email2address) && (($contact->email2address = $this->extractEmailAddress($contact->email2address)) === false)) + unset($contact->email2address); + + if (isset($contact->email3address) && (($contact->email3address = $this->extractEmailAddress($contact->email3address)) === false)) + unset($contact->email3address); + + $contactmapping = MAPIMapping::GetContactMapping(); + $contactprops = MAPIMapping::GetContactProperties(); + $this->setPropsInMAPI($mapimessage, $contact, $contactmapping); + + ///set display name from contact's properties + $cname = $this->composeDisplayName($contact); + + //get contact specific mapi properties and merge them with the AS properties + $contactprops = array_merge($this->getPropIdsFromStrings($contactmapping), $this->getPropIdsFromStrings($contactprops)); + + //contact specific properties to be set + $props = array(); + + //need to be set in order to show contacts properly in outlook and wa + $nremails = array(); + $abprovidertype = 0; + + if (isset($contact->email1address)) + $this->setEmailAddress($contact->email1address, $cname, 1, $props, $contactprops, $nremails, $abprovidertype); + if (isset($contact->email2address)) + $this->setEmailAddress($contact->email2address, $cname, 2, $props, $contactprops, $nremails, $abprovidertype); + if (isset($contact->email3address)) + $this->setEmailAddress($contact->email3address, $cname, 3, $props, $contactprops, $nremails, $abprovidertype); + + $props[$contactprops["addressbooklong"]] = $abprovidertype; + $props[$contactprops["displayname"]] = $props[$contactprops["subject"]] = $cname; + + //pda multiple e-mail addresses bug fix for the contact + if (!empty($nremails)) $props[$contactprops["addressbookmv"]] = $nremails; + + + //set addresses + $this->setAddress("home", $contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props, $contactprops); + $this->setAddress("business", $contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props, $contactprops); + $this->setAddress("other", $contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props, $contactprops); + + //set the mailing address and its type + if (isset($props[$contactprops["businessaddress"]])) { + $props[$contactprops["mailingaddress"]] = 2; + $this->setMailingAddress($contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props[$contactprops["businessaddress"]], $props, $contactprops); + } + elseif (isset($props[$contactprops["homeaddress"]])) { + $props[$contactprops["mailingaddress"]] = 1; + $this->setMailingAddress($contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props[$contactprops["homeaddress"]], $props, $contactprops); + } + elseif (isset($props[$contactprops["otheraddress"]])) { + $props[$contactprops["mailingaddress"]] = 3; + $this->setMailingAddress($contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props[$contactprops["otheraddress"]], $props, $contactprops); + } + + if (isset($contact->picture)) { + $picbinary = base64_decode($contact->picture); + $picsize = strlen($picbinary); + $props[$contactprops["haspic"]] = false; + + // TODO contact picture handling + // check if contact has already got a picture. delete it first in that case + // delete it also if it was removed on a mobile + $picprops = mapi_getprops($mapimessage, array($contactprops["haspic"])); + if (isset($picprops[$contactprops["haspic"]]) && $picprops[$contactprops["haspic"]]) { + ZLog::Write(LOGLEVEL_DEBUG, "Contact already has a picture. Delete it"); + + $attachtable = mapi_message_getattachmenttable($mapimessage); + mapi_table_restrict($attachtable, MAPIUtils::GetContactPicRestriction()); + $rows = mapi_table_queryallrows($attachtable, array(PR_ATTACH_NUM)); + if (isset($rows) && is_array($rows)) { + foreach ($rows as $row) { + mapi_message_deleteattach($mapimessage, $row[PR_ATTACH_NUM]); + } + } + } + + // only set picture if there's data in the request + if ($picbinary !== false && $picsize > 0) { + $props[$contactprops["haspic"]] = true; + $pic = mapi_message_createattach($mapimessage); + // Set properties of the attachment + $picprops = array( + PR_ATTACH_LONG_FILENAME_A => "ContactPicture.jpg", + PR_DISPLAY_NAME => "ContactPicture.jpg", + 0x7FFF000B => true, + PR_ATTACHMENT_HIDDEN => false, + PR_ATTACHMENT_FLAGS => 1, + PR_ATTACH_METHOD => ATTACH_BY_VALUE, + PR_ATTACH_EXTENSION_A => ".jpg", + PR_ATTACH_NUM => 1, + PR_ATTACH_SIZE => $picsize, + PR_ATTACH_DATA_BIN => $picbinary, + ); + + mapi_setprops($pic, $picprops); + mapi_savechanges($pic); + } + } + + if (isset($contact->asbody)) { + $this->setASbody($contact->asbody, $props, $contactprops); + } + + //set fileas + if (defined('FILEAS_ORDER')) { + $lastname = (isset($contact->lastname)) ? $contact->lastname : ""; + $firstname = (isset($contact->firstname)) ? $contact->firstname : ""; + $middlename = (isset($contact->middlename)) ? $contact->middlename : ""; + $company = (isset($contact->companyname)) ? $contact->companyname : ""; + $props[$contactprops["fileas"]] = Utils::BuildFileAs($lastname, $firstname, $middlename, $company); + } + else ZLog::Write(LOGLEVEL_DEBUG, "FILEAS_ORDER not defined"); + + mapi_setprops($mapimessage, $props); + } + + /** + * Writes a SyncTask to MAPI + * + * @param mixed $mapimessage + * @param SyncTask $task + * + * @access private + * @return boolean + */ + private function setTask($mapimessage, $task) { + mapi_setprops($mapimessage, array(PR_MESSAGE_CLASS => "IPM.Task")); + + $taskmapping = MAPIMapping::GetTaskMapping(); + $taskprops = MAPIMapping::GetTaskProperties(); + $this->setPropsInMAPI($mapimessage, $task, $taskmapping); + $taskprops = array_merge($this->getPropIdsFromStrings($taskmapping), $this->getPropIdsFromStrings($taskprops)); + + // task specific properties to be set + $props = array(); + + if (isset($task->asbody)) { + $this->setASbody($task->asbody, $props, $taskprops); + } + + if(isset($task->complete)) { + if($task->complete) { + // Set completion to 100% + // Set status to 'complete' + $props[$taskprops["completion"]] = 1.0; + $props[$taskprops["status"]] = 2; + $props[$taskprops["reminderset"]] = false; + } else { + // Set completion to 0% + // Set status to 'not started' + $props[$taskprops["completion"]] = 0.0; + $props[$taskprops["status"]] = 0; + } + } + if (isset($task->recurrence) && class_exists('TaskRecurrence')) { + $deadoccur = false; + if ((isset($task->recurrence->occurrences) && $task->recurrence->occurrences == 1) || + (isset($task->recurrence->deadoccur) && $task->recurrence->deadoccur == 1)) //ios5 sends deadoccur inside the recurrence + $deadoccur = true; + + // Set PR_ICON_INDEX to 1281 to show correct icon in category view + $props[$taskprops["icon"]] = 1281; + // dead occur - false if new occurrences should be generated from the task + // true - if it is the last ocurrence of the task + $props[$taskprops["deadoccur"]] = $deadoccur; + $props[$taskprops["isrecurringtag"]] = true; + + $recurrence = new TaskRecurrence($this->store, $mapimessage); + $recur = array(); + $this->setRecurrence($task, $recur); + + // task specific recurrence properties which we need to set here + // "start" and "end" are in GMT when passing to class.recurrence + // set recurrence start here because it's calculated differently for tasks and appointments + $recur["start"] = $task->recurrence->start; + $recur["regen"] = $task->regenerate; + //Also add dates to $recur + $recur["duedate"] = $task->duedate; + $recurrence->setRecurrence($recur); + } + + //open addresss book for user resolve to set the owner + $addrbook = $this->getAddressbook(); + + // check if there is already an owner for the task, set current user if not + $p = array( $taskprops["owner"]); + $owner = $this->getProps($mapimessage, $p); + if (!isset($owner[$taskprops["owner"]])) { + $userinfo = mapi_zarafa_getuser($this->store, Request::GetAuthUser()); + if(mapi_last_hresult() == NOERROR && isset($userinfo["fullname"])) { + $props[$taskprops["owner"]] = $userinfo["fullname"]; + } + } + mapi_setprops($mapimessage, $props); + + + } + + /** + * Writes a SyncNote to MAPI + * + * @param mixed $mapimessage + * @param SyncNote $note + * + * @access private + * @return boolean + */ + private function setNote($mapimessage, $note) { + // Touchdown does not send categories if all are unset or there is none. + // Setting it to an empty array will unset the property in Zarafa as well + if (!isset($note->categories)) $note->categories = array(); + + $this->setPropsInMAPI($mapimessage, $note, MAPIMapping::GetNoteMapping()); + + $noteprops = MAPIMapping::GetNoteProperties(); + $noteprops = $this->getPropIdsFromStrings($noteprops); + + // note specific properties to be set + $props = array(); + $props[$noteprops["messageclass"]] = "IPM.StickyNote"; + // set body otherwise the note will be "broken" when editing it in outlook + $this->setASbody($note->asbody, $props, $noteprops); + + $props[$noteprops["internetcpid"]] = INTERNET_CPID_UTF8; + mapi_setprops($mapimessage, $props); + } + + /**---------------------------------------------------------------------------------------------------------- + * HELPER + */ + + /** + * Returns the tiemstamp offset + * + * @param string $ts + * + * @access private + * @return long + */ + private function GetTZOffset($ts) { + $Offset = date("O", $ts); + + $Parity = $Offset < 0 ? -1 : 1; + $Offset = $Parity * $Offset; + $Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100; + + return $Parity * $Offset; + } + + /** + * Localtime of the timestamp + * + * @param long $time + * + * @access private + * @return array + */ + private function gmtime($time) { + $TZOffset = $this->GetTZOffset($time); + + $t_time = $time - $TZOffset * 60; #Counter adjust for localtime() + $t_arr = localtime($t_time, 1); + + return $t_arr; + } + + /** + * Sets the properties in a MAPI object according to an Sync object and a property mapping + * + * @param mixed $mapimessage + * @param SyncObject $message + * @param array $mapping + * + * @access private + * @return + */ + private function setPropsInMAPI($mapimessage, $message, $mapping) { + $mapiprops = $this->getPropIdsFromStrings($mapping); + $unsetVars = $message->getUnsetVars(); + $propsToDelete = array(); + $propsToSet = array(); + + foreach ($mapiprops as $asprop => $mapiprop) { + if(isset($message->$asprop)) { + + // UTF8->windows1252.. this is ok for all numerical values + if(mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) { + if(is_array($message->$asprop)) + $value = array_map("u2wi", $message->$asprop); + else + $value = u2wi($message->$asprop); + } else { + $value = $message->$asprop; + } + + // Make sure the php values are the correct type + switch(mapi_prop_type($mapiprop)) { + case PT_BINARY: + case PT_STRING8: + settype($value, "string"); + break; + case PT_BOOLEAN: + settype($value, "boolean"); + break; + case PT_SYSTIME: + case PT_LONG: + settype($value, "integer"); + break; + } + + // decode base64 value + if($mapiprop == PR_RTF_COMPRESSED) { + $value = base64_decode($value); + if(strlen($value) == 0) + continue; // PDA will sometimes give us an empty RTF, which we'll ignore. + + // Note that you can still remove notes because when you remove notes it gives + // a valid compressed RTF with nothing in it. + + } + // if an "empty array" is to be saved, it the mvprop should be deleted - fixes Mantis #468 + if (is_array($value) && empty($value)) { + $propsToDelete[] = $mapiprop; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->setPropsInMAPI(): Property '%s' to be deleted as it is an empty array", $asprop)); + } + else { + // all properties will be set at once + $propsToSet[$mapiprop] = $value; + } + } + elseif (in_array($asprop, $unsetVars)) { + $propsToDelete[] = $mapiprop; + } + } + + mapi_setprops($mapimessage, $propsToSet); + if (mapi_last_hresult()) { + Zlog::Write(LOGLEVEL_WARN, sprintf("Failed to set properties, trying to set them separately. Error code was:%x", mapi_last_hresult())); + $this->setPropsIndividually($mapimessage, $propsToSet, $mapiprops); + } + + mapi_deleteprops($mapimessage, $propsToDelete); + + //clean up + unset($unsetVars, $propsToDelete); + } + + /** + * Sets the properties one by one in a MAPI object + * + * @param mixed &$mapimessage + * @param array &$propsToSet + * @param array &$mapiprops + * + * @access private + * @return + */ + private function setPropsIndividually(&$mapimessage, &$propsToSet, &$mapiprops) { + foreach ($propsToSet as $prop => $value) { + mapi_setprops($mapimessage, array($prop => $value)); + if (mapi_last_hresult()) { + Zlog::Write(LOGLEVEL_ERROR, sprintf("Failed setting property [%s] with value [%s], error code was:%x", array_search($prop, $mapiprops), $value, mapi_last_hresult())); + } + } + + } + + /** + * Gets the properties from a MAPI object and sets them in the Sync object according to mapping + * + * @param SyncObject &$message + * @param mixed $mapimessage + * @param array $mapping + * + * @access private + * @return + */ + private function getPropsFromMAPI(&$message, $mapimessage, $mapping) { + $messageprops = $this->getProps($mapimessage, $mapping); + foreach ($mapping as $asprop => $mapiprop) { + // Get long strings via openproperty + if (isset($messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))])) { + if ($messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))] == MAPI_E_NOT_ENOUGH_MEMORY_32BIT || + $messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))] == MAPI_E_NOT_ENOUGH_MEMORY_64BIT) { + $messageprops[$mapiprop] = MAPIUtils::readPropStream($mapimessage, $mapiprop); + } + } + + if(isset($messageprops[$mapiprop])) { + if(mapi_prop_type($mapiprop) == PT_BOOLEAN) { + // Force to actual '0' or '1' + if($messageprops[$mapiprop]) + $message->$asprop = 1; + else + $message->$asprop = 0; + } else { + // Special handling for PR_MESSAGE_FLAGS + if($mapiprop == PR_MESSAGE_FLAGS) + $message->$asprop = $messageprops[$mapiprop] & 1; // only look at 'read' flag + else if($mapiprop == PR_RTF_COMPRESSED) + //do not send rtf to the mobile + continue; + else if(is_array($messageprops[$mapiprop])) + $message->$asprop = array_map("w2u", $messageprops[$mapiprop]); + else { + if(mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) + $message->$asprop = w2u($messageprops[$mapiprop]); + else + $message->$asprop = $messageprops[$mapiprop]; + } + } + } + } + } + + /** + * Wraps getPropIdsFromStrings() calls + * + * @param mixed &$mapiprops + * + * @access private + * @return + */ + private function getPropIdsFromStrings(&$mapiprops) { + return getPropIdsFromStrings($this->store, $mapiprops); + } + + /** + * Wraps mapi_getprops() calls + * + * @param mixed &$mapiprops + * + * @access private + * @return + */ + protected function getProps($mapimessage, &$mapiproperties) { + $mapiproperties = $this->getPropIdsFromStrings($mapiproperties); + return mapi_getprops($mapimessage, $mapiproperties); + } + + /** + * Returns an GMT timezone array + * + * @access private + * @return array + */ + private function getGMTTZ() { + $tz = array( + "bias" => 0, + "tzname" => "", + "dstendyear" => 0, + "dstendmonth" => 10, + "dstendday" => 0, + "dstendweek" => 5, + "dstendhour" => 2, + "dstendminute" => 0, + "dstendsecond" => 0, + "dstendmillis" => 0, + "stdbias" => 0, + "tznamedst" => "", + "dststartyear" => 0, + "dststartmonth" => 3, + "dststartday" => 0, + "dststartweek" => 5, + "dststarthour" => 1, + "dststartminute" => 0, + "dststartsecond" => 0, + "dststartmillis" => 0, + "dstbias" => -60 + ); + + return $tz; + } + + /** + * Unpack timezone info from MAPI + * + * @param string $data + * + * @access private + * @return array + */ + private function getTZFromMAPIBlob($data) { + $unpacked = unpack("lbias/lstdbias/ldstbias/" . + "vconst1/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" . + "vconst2/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis", $data); + return $unpacked; + } + + /** + * Unpack timezone info from Sync + * + * @param string $data + * + * @access private + * @return array + */ + private function getTZFromSyncBlob($data) { + $tz = unpack( "lbias/a64tzname/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" . + "lstdbias/a64tznamedst/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis/" . + "ldstbias", $data); + + // Make the structure compatible with class.recurrence.php + $tz["timezone"] = $tz["bias"]; + $tz["timezonedst"] = $tz["dstbias"]; + + 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 + * + * @param array $tz + * + * @access private + * @return string + */ + private function getMAPIBlobFromTZ($tz) { + $packed = pack("lll" . "vvvvvvvvv" . "vvvvvvvvv", + $tz["bias"], $tz["stdbias"], $tz["dstbias"], + 0, 0, $tz["dstendmonth"], $tz["dstendday"], $tz["dstendweek"], $tz["dstendhour"], $tz["dstendminute"], $tz["dstendsecond"], $tz["dstendmillis"], + 0, 0, $tz["dststartmonth"], $tz["dststartday"], $tz["dststartweek"], $tz["dststarthour"], $tz["dststartminute"], $tz["dststartsecond"], $tz["dststartmillis"]); + + return $packed; + } + + /** + * Checks the date to see if it is in DST, and returns correct GMT date accordingly + * + * @param long $localtime + * @param array $tz + * + * @access private + * @return long + */ + private function getGMTTimeByTZ($localtime, $tz) { + if(!isset($tz) || !is_array($tz)) + return $localtime; + + if($this->isDST($localtime, $tz)) + return $localtime + $tz["bias"]*60 + $tz["dstbias"]*60; + else + return $localtime + $tz["bias"]*60; + } + + /** + * Returns the local time for the given GMT time, taking account of the given timezone + * + * @param long $gmttime + * @param array $tz + * + * @access private + * @return long + */ + private function getLocaltimeByTZ($gmttime, $tz) { + if(!isset($tz) || !is_array($tz)) + return $gmttime; + + if($this->isDST($gmttime - $tz["bias"]*60, $tz)) // may bug around the switch time because it may have to be 'gmttime - bias - dstbias' + return $gmttime - $tz["bias"]*60 - $tz["dstbias"]*60; + else + return $gmttime - $tz["bias"]*60; + } + + /** + * Returns TRUE if it is the summer and therefore DST is in effect + * + * @param long $localtime + * @param array $tz + * + * @access private + * @return boolean + */ + private function isDST($localtime, $tz) { + if( !isset($tz) || !is_array($tz) || + !isset($tz["dstbias"]) || $tz["dstbias"] == 0 || + !isset($tz["dststartmonth"]) || $tz["dststartmonth"] == 0 || + !isset($tz["dstendmonth"]) || $tz["dstendmonth"] == 0) + return false; + + $year = gmdate("Y", $localtime); + $start = $this->getTimestampOfWeek($year, $tz["dststartmonth"], $tz["dststartweek"], $tz["dststartday"], $tz["dststarthour"], $tz["dststartminute"], $tz["dststartsecond"]); + $end = $this->getTimestampOfWeek($year, $tz["dstendmonth"], $tz["dstendweek"], $tz["dstendday"], $tz["dstendhour"], $tz["dstendminute"], $tz["dstendsecond"]); + + if($start < $end) { + // northern hemisphere (july = dst) + if($localtime >= $start && $localtime < $end) + $dst = true; + else + $dst = false; + } else { + // southern hemisphere (january = dst) + if($localtime >= $end && $localtime < $start) + $dst = false; + else + $dst = true; + } + + return $dst; + } + + /** + * Returns the local timestamp for the $week'th $wday of $month in $year at $hour:$minute:$second + * + * @param int $year + * @param int $month + * @param int $week + * @param int $wday + * @param int $hour + * @param int $minute + * @param int $second + * + * @access private + * @return long + */ + private function getTimestampOfWeek($year, $month, $week, $wday, $hour, $minute, $second) { + if ($month == 0) + return; + + $date = gmmktime($hour, $minute, $second, $month, 1, $year); + + // Find first day in month which matches day of the week + while(1) { + $wdaynow = gmdate("w", $date); + if($wdaynow == $wday) + break; + $date += 24 * 60 * 60; + } + + // Forward $week weeks (may 'overflow' into the next month) + $date = $date + $week * (24 * 60 * 60 * 7); + + // Reverse 'overflow'. Eg week '10' will always be the last week of the month in which the + // specified weekday exists + while(1) { + $monthnow = gmdate("n", $date); // gmdate returns 1-12 + if($monthnow > $month) + $date = $date - (24 * 7 * 60 * 60); + else + break; + } + + return $date; + } + + /** + * Normalize the given timestamp to the start of the day + * + * @param long $timestamp + * + * @access private + * @return long + */ + private function getDayStartOfTimestamp($timestamp) { + return $timestamp - ($timestamp % (60 * 60 * 24)); + } + + /** + * Returns an SMTP address from an entry id + * + * @param string $entryid + * + * @access private + * @return string + */ + private function getSMTPAddressFromEntryID($entryid) { + $addrbook = $this->getAddressbook(); + + $mailuser = mapi_ab_openentry($addrbook, $entryid); + if(!$mailuser) + return ""; + + $props = mapi_getprops($mailuser, array(PR_ADDRTYPE, PR_SMTP_ADDRESS, PR_EMAIL_ADDRESS)); + + $addrtype = isset($props[PR_ADDRTYPE]) ? $props[PR_ADDRTYPE] : ""; + + if(isset($props[PR_SMTP_ADDRESS])) + return $props[PR_SMTP_ADDRESS]; + + if($addrtype == "SMTP" && isset($props[PR_EMAIL_ADDRESS])) + return $props[PR_EMAIL_ADDRESS]; + elseif ($addrtype == "ZARAFA" && isset($props[PR_EMAIL_ADDRESS])) { + $userinfo = mapi_zarafa_getuser_by_name($this->store, $props[PR_EMAIL_ADDRESS]); + if (is_array($userinfo) && isset($userinfo["emailaddress"])) + return $userinfo["emailaddress"]; + } + + return ""; + } + + /** + * Returns fullname from an entryid + * + * @param binary $entryid + * @return string fullname or false on error + */ + private function getFullnameFromEntryID($entryid) { + $addrbook = $this->getAddressbook(); + $mailuser = mapi_ab_openentry($addrbook, $entryid); + if(!$mailuser) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("Unable to get mailuser for getFullnameFromEntryID (0x%X)", mapi_last_hresult())); + return false; + } + + $props = mapi_getprops($mailuser, array(PR_DISPLAY_NAME)); + if (isset($props[PR_DISPLAY_NAME])) { + return $props[PR_DISPLAY_NAME]; + } + ZLog::Write(LOGLEVEL_ERROR, sprintf("Unable to get fullname for getFullnameFromEntryID (0x%X)", mapi_last_hresult())); + return false; + } + + /** + * Builds a displayname from several separated values + * + * @param SyncContact $contact + * + * @access private + * @return string + */ + private function composeDisplayName(&$contact) { + // Set display name and subject to a combined value of firstname and lastname + $cname = (isset($contact->prefix))?u2w($contact->prefix)." ":""; + $cname .= u2w($contact->firstname); + $cname .= (isset($contact->middlename))?" ". u2w($contact->middlename):""; + $cname .= " ". u2w($contact->lastname); + $cname .= (isset($contact->suffix))?" ". u2w($contact->suffix):""; + return trim($cname); + } + + /** + * Sets all dependent properties for an email address + * + * @param string $emailAddress + * @param string $displayName + * @param int $cnt + * @param array &$props + * @param array &$properties + * @param array &$nremails + * @param int &$abprovidertype + * + * @access private + * @return + */ + private function setEmailAddress($emailAddress, $displayName, $cnt, &$props, &$properties, &$nremails, &$abprovidertype){ + if (isset($emailAddress)) { + $name = (isset($displayName)) ? $displayName : $emailAddress; + + $props[$properties["emailaddress$cnt"]] = $emailAddress; + $props[$properties["emailaddressdemail$cnt"]] = $emailAddress; + $props[$properties["emailaddressdname$cnt"]] = $name; + $props[$properties["emailaddresstype$cnt"]] = "SMTP"; + $props[$properties["emailaddressentryid$cnt"]] = mapi_createoneoff($name, "SMTP", $emailAddress); + $nremails[] = $cnt - 1; + $abprovidertype |= 2 ^ ($cnt - 1); + } + } + + /** + * Sets the properties for an address string + * + * @param string $type which address is being set + * @param string $city + * @param string $country + * @param string $postalcode + * @param string $state + * @param string $street + * @param array &$props + * @param array &$properties + * + * @access private + * @return + */ + private function setAddress($type, &$city, &$country, &$postalcode, &$state, &$street, &$props, &$properties) { + if (isset($city)) $props[$properties[$type."city"]] = $city = u2w($city); + + if (isset($country)) $props[$properties[$type."country"]] = $country = u2w($country); + + if (isset($postalcode)) $props[$properties[$type."postalcode"]] = $postalcode = u2w($postalcode); + + if (isset($state)) $props[$properties[$type."state"]] = $state = u2w($state); + + if (isset($street)) $props[$properties[$type."street"]] = $street = u2w($street); + + //set composed address + $address = Utils::BuildAddressString($street, $postalcode, $city, $state, $country); + if ($address) $props[$properties[$type."address"]] = $address; + } + + /** + * Sets the properties for a mailing address + * + * @param string $city + * @param string $country + * @param string $postalcode + * @param string $state + * @param string $street + * @param string $address + * @param array &$props + * @param array &$properties + * + * @access private + * @return + */ + private function setMailingAddress($city, $country, $postalcode, $state, $street, $address, &$props, &$properties) { + if (isset($city)) $props[$properties["city"]] = $city; + if (isset($country)) $props[$properties["country"]] = $country; + if (isset($postalcode)) $props[$properties["postalcode"]] = $postalcode; + if (isset($state)) $props[$properties["state"]] = $state; + if (isset($street)) $props[$properties["street"]] = $street; + if (isset($address)) $props[$properties["postaladdress"]] = $address; + } + + /** + * Sets data in a recurrence array + * + * @param SyncObject $message + * @param array &$recur + * + * @access private + * @return + */ + private function setRecurrence($message, &$recur) { + if (isset($message->complete)) { + $recur["complete"] = $message->complete; + } + + if(!isset($message->recurrence->interval)) + $message->recurrence->interval = 1; + + //set the default value of numoccur + $recur["numoccur"] = 0; + //a place holder for recurrencetype property + $recur["recurrencetype"] = 0; + + switch($message->recurrence->type) { + case 0: + $recur["type"] = 10; + if(isset($message->recurrence->dayofweek)) + $recur["subtype"] = 1; + else + $recur["subtype"] = 0; + + $recur["everyn"] = $message->recurrence->interval * (60 * 24); + $recur["recurrencetype"] = 1; + break; + case 1: + $recur["type"] = 11; + $recur["subtype"] = 1; + $recur["everyn"] = $message->recurrence->interval; + $recur["recurrencetype"] = 2; + break; + case 2: + $recur["type"] = 12; + $recur["subtype"] = 2; + $recur["everyn"] = $message->recurrence->interval; + $recur["recurrencetype"] = 3; + break; + case 3: + $recur["type"] = 12; + $recur["subtype"] = 3; + $recur["everyn"] = $message->recurrence->interval; + $recur["recurrencetype"] = 3; + break; + case 4: + $recur["type"] = 13; + $recur["subtype"] = 1; + $recur["everyn"] = $message->recurrence->interval * 12; + $recur["recurrencetype"] = 4; + break; + case 5: + $recur["type"] = 13; + $recur["subtype"] = 2; + $recur["everyn"] = $message->recurrence->interval * 12; + $recur["recurrencetype"] = 4; + break; + case 6: + $recur["type"] = 13; + $recur["subtype"] = 3; + $recur["everyn"] = $message->recurrence->interval * 12; + $recur["recurrencetype"] = 4; + break; + } + + // "start" and "end" are in GMT when passing to class.recurrence + $recur["end"] = $this->getDayStartOfTimestamp(0x7fffffff); // Maximum GMT value for end by default + + if(isset($message->recurrence->until)) { + $recur["term"] = 0x21; + $recur["end"] = $message->recurrence->until; + } else if(isset($message->recurrence->occurrences)) { + $recur["term"] = 0x22; + $recur["numoccur"] = $message->recurrence->occurrences; + } else { + $recur["term"] = 0x23; + } + + if(isset($message->recurrence->dayofweek)) + $recur["weekdays"] = $message->recurrence->dayofweek; + if(isset($message->recurrence->weekofmonth)) + $recur["nday"] = $message->recurrence->weekofmonth; + if(isset($message->recurrence->monthofyear)) { + // MAPI stores months as the amount of minutes until the beginning of the month in a + // non-leapyear. Why this is, is totally unclear. + $monthminutes = array(0,44640,84960,129600,172800,217440,260640,305280,348480,393120,437760,480960); + $recur["month"] = $monthminutes[$message->recurrence->monthofyear-1]; + } + if(isset($message->recurrence->dayofmonth)) + $recur["monthday"] = $message->recurrence->dayofmonth; + } + + /** + * Extracts the email address (mailbox@host) from an email address because + * some devices send email address as "Firstname Lastname" + * + * @link http://developer.berlios.de/mantis/view.php?id=486 + * + * @param string $email + * + * @access private + * @return string or false on error + */ + private function extractEmailAddress($email) { + if (!isset($this->zRFC822)) $this->zRFC822 = new Mail_RFC822(); + $parsedAddress = $this->zRFC822->parseAddressList($email); + if (!isset($parsedAddress[0]->mailbox) || !isset($parsedAddress[0]->host)) return false; + + return $parsedAddress[0]->mailbox.'@'.$parsedAddress[0]->host; + } + + /** + * Returns the message body for a required format + * + * @param MAPIMessage $mapimessage + * @param int $bpReturnType + * @param SyncObject $message + * + * @access private + * @return boolean + */ + private function setMessageBodyForType($mapimessage, $bpReturnType, &$message) { + //default value is PR_BODY + $property = PR_BODY; + switch ($bpReturnType) { + case SYNC_BODYPREFERENCE_HTML: + $property = PR_HTML; + break; + case SYNC_BODYPREFERENCE_RTF: + $property = PR_RTF_COMPRESSED; + break; + case SYNC_BODYPREFERENCE_MIME: + $stat = $this->imtoinet($mapimessage, $message); + if (isset($message->asbody)) + $message->asbody->type = $bpReturnType; + return $stat; + } + + $body = mapi_message_openproperty($mapimessage, $property); + //set the properties according to supported AS version + if (Request::GetProtocolVersion() >= 12.0) { + $message->asbody = new SyncBaseBody(); + $message->asbody->type = $bpReturnType; + if ($bpReturnType == SYNC_BODYPREFERENCE_RTF) + $message->asbody->data = base64_encode($body); + elseif (isset($message->internetcpid) && $bpReturnType == SYNC_BODYPREFERENCE_HTML) + $message->asbody->data = Utils::ConvertCodepageStringToUtf8($message->internetcpid, $body); + else + $message->asbody->data = w2u($body); + $message->asbody->estimatedDataSize = strlen($message->asbody->data); + } + else { + $message->body = str_replace("\n","\r\n", w2u(str_replace("\r", "", $body))); + $message->bodysize = strlen($message->body); + $message->bodytruncated = 0; + } + + return true; + } + + /** + * A wrapper for mapi_inetmapi_imtoinet function + * + * @param MAPIMessage $mapimessage + * @param SyncObject $message + * + * @access private + * @return boolean + */ + private function imtoinet($mapimessage, &$message) { + // if it is a signed message get a full attachment generated by ZCP + $props = mapi_getprops($mapimessage, array(PR_MESSAGE_CLASS)); + if (isset($props[PR_MESSAGE_CLASS]) && $props[PR_MESSAGE_CLASS] && strpos(strtolower($props[PR_MESSAGE_CLASS]), 'multipartsigned')) { + // find the required attachment + $attachtable = mapi_message_getattachmenttable($mapimessage); + mapi_table_restrict($attachtable, MAPIUtils::GetSignedAttachmentRestriction()); + if (mapi_table_getrowcount($attachtable) == 1) { + $rows = mapi_table_queryrows($attachtable, array(PR_ATTACH_NUM, PR_ATTACH_SIZE), 0, 1); + if (isset($rows[0][PR_ATTACH_NUM])) { + $mapiattach = mapi_message_openattach($mapimessage, $rows[0][PR_ATTACH_NUM]); + $stream = mapi_openpropertytostream($mapiattach, PR_ATTACH_DATA_BIN); + $streamsize = $rows[0][PR_ATTACH_SIZE]; + } + } + } + elseif (function_exists("mapi_inetmapi_imtoinet")) { + $addrbook = $this->getAddressbook(); + $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $mapimessage, array('use_tnef' => -1)); + $mstreamstat = mapi_stream_stat($stream); + $streamsize = $mstreamstat["cb"]; + } + + if (isset($stream) && isset($streamsize)) { + if (Request::GetProtocolVersion() >= 12.0) { + if (!isset($message->asbody)) + $message->asbody = new SyncBaseBody(); + //TODO data should be wrapped in a MapiStreamWrapper + $message->asbody->data = mapi_stream_read($stream, $streamsize); + $message->asbody->estimatedDataSize = $streamsize; + $message->asbody->truncated = 0; + } + else { + $message->mimetruncated = 0; + //TODO mimedata should be a wrapped in a MapiStreamWrapper + $message->mimedata = mapi_stream_read($stream, $streamsize); + $message->mimesize = $streamsize; + } + unset($message->body, $message->bodytruncated); + return true; + } + else { + ZLog::Write(LOGLEVEL_ERROR, sprintf("Error opening attachment for imtoinet")); + } + return false; + } + + /** + * Sets the message body + * + * @param MAPIMessage $mapimessage + * @param ContentParameters $contentparameters + * @param SyncObject $message + */ + private function setMessageBody($mapimessage, $contentparameters, &$message) { + //get the available body preference types + $bpTypes = $contentparameters->GetBodyPreference(); + if ($bpTypes !== false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BodyPreference types: %s", implode(', ', $bpTypes))); + //do not send mime data if the client requests it + if (($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_NEVER) && ($key = array_search(SYNC_BODYPREFERENCE_MIME, $bpTypes)!== false)) { + unset($bpTypes[$key]); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Remove mime body preference type because the device required no mime support. BodyPreference types: %s", implode(', ', $bpTypes))); + } + //get the best fitting preference type + $bpReturnType = Utils::GetBodyPreferenceBestMatch($bpTypes); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("GetBodyPreferenceBestMatch: %d", $bpReturnType)); + $bpo = $contentparameters->BodyPreference($bpReturnType); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("bpo: truncation size:'%d', allornone:'%d', preview:'%d'", $bpo->GetTruncationSize(), $bpo->GetAllOrNone(), $bpo->GetPreview())); + + $this->setMessageBodyForType($mapimessage, $bpReturnType, $message); + //only set the truncation size data if device set it in request + if ( $bpo->GetTruncationSize() != false && + $bpReturnType != SYNC_BODYPREFERENCE_MIME && + $message->asbody->estimatedDataSize > $bpo->GetTruncationSize() && + $contentparameters->GetTruncation() != SYNC_TRUNCATION_ALL // do not truncate message if the whole is requested, e.g. on fetch + ) { + $message->asbody->data = Utils::Utf8_truncate($message->asbody->data, $bpo->GetTruncationSize()); + $message->asbody->truncated = 1; + + } + // set the preview or windows phones won't show the preview of an email + if (Request::GetProtocolVersion() >= 14.0 && $bpo->GetPreview()) { + $message->asbody->preview = Utils::Utf8_truncate(MAPIUtils::readPropStream($mapimessage, PR_BODY), $bpo->GetPreview()); + } + } + else { + // Override 'body' for truncation + $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation()); + $this->setMessageBodyForType($mapimessage, SYNC_BODYPREFERENCE_PLAIN, $message); + + if($message->bodysize > $truncsize) { + $message->body = Utils::Utf8_truncate($message->body, $truncsize); + $message->bodytruncated = 1; + } + + if (!isset($message->body) || strlen($message->body) == 0) + $message->body = " "; + + if ($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_ALWAYS) { + //set the html body for iphone in AS 2.5 version + $this->imtoinet($mapimessage, $message); + } + } + } + + /** + * Calculates the native body type of a message using available properties. Refer to oxbbody + * + * @param array $messageprops + * + * @access private + * @return int + */ + private function getNativeBodyType($messageprops) { + //check if the properties are set and get the error code if needed + if (!isset($messageprops[PR_BODY])) $messageprops[PR_BODY] = $this->getError(PR_BODY, $messageprops); + if (!isset($messageprops[PR_RTF_COMPRESSED])) $messageprops[PR_RTF_COMPRESSED] = $this->getError(PR_RTF_COMPRESSED, $messageprops); + if (!isset($messageprops[PR_HTML])) $messageprops[PR_HTML] = $this->getError(PR_HTML, $messageprops); + if (!isset($messageprops[PR_RTF_IN_SYNC])) $messageprops[PR_RTF_IN_SYNC] = $this->getError(PR_RTF_IN_SYNC, $messageprops); + + if ( // 1 + ($messageprops[PR_BODY] == MAPI_E_NOT_FOUND) && + ($messageprops[PR_RTF_COMPRESSED] == MAPI_E_NOT_FOUND) && + ($messageprops[PR_HTML] == MAPI_E_NOT_FOUND)) + return SYNC_BODYPREFERENCE_PLAIN; + elseif ( // 2 + ($messageprops[PR_BODY] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_RTF_COMPRESSED] == MAPI_E_NOT_FOUND) && + ($messageprops[PR_HTML] == MAPI_E_NOT_FOUND)) + return SYNC_BODYPREFERENCE_PLAIN; + elseif ( // 3 + ($messageprops[PR_BODY] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_RTF_COMPRESSED] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_HTML] == MAPI_E_NOT_FOUND)) + return SYNC_BODYPREFERENCE_RTF; + elseif ( // 4 + ($messageprops[PR_BODY] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_RTF_COMPRESSED] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_HTML] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_RTF_IN_SYNC])) + return SYNC_BODYPREFERENCE_RTF; + elseif ( // 5 + ($messageprops[PR_BODY] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_RTF_COMPRESSED] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_HTML] == MAPI_E_NOT_ENOUGH_MEMORY) && + (!$messageprops[PR_RTF_IN_SYNC])) + return SYNC_BODYPREFERENCE_HTML; + elseif ( // 6 + ($messageprops[PR_RTF_COMPRESSED] != MAPI_E_NOT_FOUND || $messageprops[PR_RTF_COMPRESSED] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_HTML] != MAPI_E_NOT_FOUND || $messageprops[PR_HTML] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_RTF_IN_SYNC])) + return SYNC_BODYPREFERENCE_RTF; + elseif ( // 7 + ($messageprops[PR_RTF_COMPRESSED] != MAPI_E_NOT_FOUND || $messageprops[PR_RTF_COMPRESSED] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_HTML] != MAPI_E_NOT_FOUND || $messageprops[PR_HTML] == MAPI_E_NOT_ENOUGH_MEMORY) && + (!$messageprops[PR_RTF_IN_SYNC])) + return SYNC_BODYPREFERENCE_HTML; + elseif ( // 8 + ($messageprops[PR_BODY] != MAPI_E_NOT_FOUND || $messageprops[PR_BODY] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_RTF_COMPRESSED] != MAPI_E_NOT_FOUND || $messageprops[PR_RTF_COMPRESSED] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_RTF_IN_SYNC])) + return SYNC_BODYPREFERENCE_RTF; + elseif ( // 9.1 + ($messageprops[PR_BODY] != MAPI_E_NOT_FOUND || $messageprops[PR_BODY] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_RTF_COMPRESSED] != MAPI_E_NOT_FOUND || $messageprops[PR_RTF_COMPRESSED] == MAPI_E_NOT_ENOUGH_MEMORY) && + (!$messageprops[PR_RTF_IN_SYNC])) + return SYNC_BODYPREFERENCE_PLAIN; + elseif ( // 9.2 + ($messageprops[PR_RTF_COMPRESSED] != MAPI_E_NOT_FOUND || $messageprops[PR_RTF_COMPRESSED] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_BODY] == MAPI_E_NOT_FOUND) && + ($messageprops[PR_HTML] == MAPI_E_NOT_FOUND)) + return SYNC_BODYPREFERENCE_RTF; + elseif ( // 9.3 + ($messageprops[PR_BODY] != MAPI_E_NOT_FOUND || $messageprops[PR_BODY] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_RTF_COMPRESSED] == MAPI_E_NOT_FOUND) && + ($messageprops[PR_HTML] == MAPI_E_NOT_FOUND)) + return SYNC_BODYPREFERENCE_PLAIN; + elseif ( // 9.4 + ($messageprops[PR_HTML] != MAPI_E_NOT_FOUND || $messageprops[PR_HTML] == MAPI_E_NOT_ENOUGH_MEMORY) && + ($messageprops[PR_BODY] == MAPI_E_NOT_FOUND) && + ($messageprops[PR_RTF_COMPRESSED] == MAPI_E_NOT_FOUND)) + return SYNC_BODYPREFERENCE_HTML; + else // 10 + return SYNC_BODYPREFERENCE_PLAIN; + } + + /** + * Returns the error code for a given property. Helper for getNativeBodyType function. + * + * @param int $tag + * @param array $messageprops + * + * @access private + * @return int (MAPI_ERROR_CODE) + */ + private function getError($tag, $messageprops) { + $prBodyError = mapi_prop_tag(PT_ERROR, mapi_prop_id($tag)); + if(isset($messageprops[$prBodyError]) && mapi_is_error($messageprops[$prBodyError])) { + if($messageprops[$prBodyError] == MAPI_E_NOT_ENOUGH_MEMORY_32BIT || + $messageprops[$prBodyError] == MAPI_E_NOT_ENOUGH_MEMORY_64BIT) { + return MAPI_E_NOT_ENOUGH_MEMORY; + } + } + return MAPI_E_NOT_FOUND; + } + + /** + * Sets properties for an email message + * + * @param mixed $mapimessage + * @param SyncMail $message + * + * @access private + * @return void + */ + private function setFlag($mapimessage, &$message){ + // do nothing if protocoll version is lower than 12.0 as flags haven't been defined before + if (Request::GetProtocolVersion() < 12.0 ) return; + + $message->flag = new SyncMailFlags(); + + $this->getPropsFromMAPI($message->flag, $mapimessage, MAPIMapping::GetMailFlagsMapping()); + } + + /** + * Sets information from SyncBaseBody type for a MAPI message. + * + * @param SyncBaseBody $asbody + * @param array $props + * @param array $appointmentprops + * + * @access private + * @return void + */ + private function setASbody($asbody, &$props, $appointmentprops) { + if (isset($asbody->type) && isset($asbody->data) && strlen($asbody->data) > 0) { + switch ($asbody->type) { + case SYNC_BODYPREFERENCE_PLAIN: + default: + //set plain body if the type is not in valid range + $props[$appointmentprops["body"]] = u2w($asbody->data); + break; + case SYNC_BODYPREFERENCE_HTML: + $props[$appointmentprops["html"]] = u2w($asbody->data); + break; + case SYNC_BODYPREFERENCE_RTF: + break; + case SYNC_BODYPREFERENCE_MIME: + break; + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setASbody either type or data are not set. Setting to empty body"); + $props[$appointmentprops["body"]] = ""; + } + } + + /** + * Get MAPI addressbook object + * + * @access private + * @return MAPIAddressbook object to be used with mapi_ab_* or false on failure + */ + private function getAddressbook() { + if (isset($this->addressbook) && $this->addressbook) { + return $this->addressbook; + } + $this->addressbook = mapi_openaddressbook($this->session); + $result = mapi_last_hresult(); + if ($result && $this->addressbook === false) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("MAPIProvider->getAddressbook error opening addressbook 0x%X", $result)); + return false; + } + return $this->addressbook; + } +} + +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapistreamwrapper.php b/sources/backend/zarafa/mapistreamwrapper.php new file mode 100644 index 0000000..a8f5e23 --- /dev/null +++ b/sources/backend/zarafa/mapistreamwrapper.php @@ -0,0 +1,148 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class MAPIStreamWrapper { + const PROTOCOL = "mapistream"; + + private $mapistream; + private $position; + private $streamlength; + + /** + * Opens the stream + * The mapistream reference is passed over the context + * + * @param string $path Specifies the URL that was passed to the original function + * @param string $mode The mode used to open the file, as detailed for fopen() + * @param int $options Holds additional flags set by the streams API + * @param string $opened_path If the path is opened successfully, and STREAM_USE_PATH is set in options, + * opened_path should be set to the full path of the file/resource that was actually opened. + * + * @access public + * @return boolean + */ + public function stream_open($path, $mode, $options, &$opened_path) { + $contextOptions = stream_context_get_options($this->context); + if (!isset($contextOptions[self::PROTOCOL]['stream'])) + return false; + + $this->position = 0; + + // this is our stream! + $this->mapistream = $contextOptions[self::PROTOCOL]['stream']; + + // get the data length from mapi + $stat = mapi_stream_stat($this->mapistream); + $this->streamlength = $stat["cb"]; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIStreamWrapper::stream_open(): initialized mapistream: %s streamlength: %d", $this->mapistream, $this->streamlength)); + + return true; + } + + /** + * Reads from stream + * + * @param int $len amount of bytes to be read + * + * @access public + * @return string + */ + public function stream_read($len) { + $len = ($this->position + $len > $this->streamlength) ? ($this->streamlength - $this->position) : $len; + $data = mapi_stream_read($this->mapistream, $len); + $this->position += strlen($data); + return $data; + } + + /** + * Returns the current position on stream + * + * @access public + * @return int + */ + public function stream_tell() { + return $this->position; + } + + /** + * Indicates if 'end of file' is reached + * + * @access public + * @return boolean + */ + public function stream_eof() { + return ($this->position >= $this->streamlength); + } + + /** + * Retrieves information about a stream + * + * @access public + * @return array + */ + public function stream_stat() { + return array( + 7 => $this->streamlength, + 'size' => $this->streamlength, + ); + } + + /** + * Instantiates a MAPIStreamWrapper + * + * @param mapistream $mapistream The stream to be wrapped + * + * @access public + * @return MAPIStreamWrapper + */ + static public function Open($mapistream) { + $context = stream_context_create(array(self::PROTOCOL => array('stream' => &$mapistream))); + return fopen(self::PROTOCOL . "://",'r', false, $context); + } +} + +stream_wrapper_register(MAPIStreamWrapper::PROTOCOL, "MAPIStreamWrapper") + +?> \ No newline at end of file diff --git a/sources/backend/zarafa/mapiutils.php b/sources/backend/zarafa/mapiutils.php new file mode 100644 index 0000000..fd97fe3 --- /dev/null +++ b/sources/backend/zarafa/mapiutils.php @@ -0,0 +1,487 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +/** + * + * MAPI to AS mapping class + * + * + */ +class MAPIUtils { + + /** + * Create a MAPI restriction to use within an email folder which will + * return all messages since since $timestamp + * + * @param long $timestamp Timestamp since when to include messages + * + * @access public + * @return array + */ + public static function GetEmailRestriction($timestamp) { + // ATTENTION: ON CHANGING THIS RESTRICTION, MAPIUtils::IsInEmailSyncInterval() also needs to be changed + $restriction = array ( RES_PROPERTY, + array ( RELOP => RELOP_GE, + ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, + VALUE => $timestamp + ) + ); + + return $restriction; + } + + + /** + * Create a MAPI restriction to use in the calendar which will + * return all future calendar items, plus those since $timestamp + * + * @param MAPIStore $store the MAPI store + * @param long $timestamp Timestamp since when to include messages + * + * @access public + * @return array + */ + //TODO getting named properties + public static function GetCalendarRestriction($store, $timestamp) { + // This is our viewing window + $start = $timestamp; + $end = 0x7fffffff; // infinite end + + $props = MAPIMapping::GetAppointmentProperties(); + $props = getPropIdsFromStrings($store, $props); + + // ATTENTION: ON CHANGING THIS RESTRICTION, MAPIUtils::IsInCalendarSyncInterval() also needs to be changed + $restriction = Array(RES_OR, + Array( + // OR + // item.end > window.start && item.start < window.end + Array(RES_AND, + Array( + Array(RES_PROPERTY, + Array(RELOP => RELOP_LE, + ULPROPTAG => $props["starttime"], + VALUE => $end + ) + ), + Array(RES_PROPERTY, + Array(RELOP => RELOP_GE, + ULPROPTAG => $props["endtime"], + VALUE => $start + ) + ) + ) + ), + // OR + Array(RES_OR, + Array( + // OR + // (EXIST(recurrence_enddate_property) && item[isRecurring] == true && recurrence_enddate_property >= start) + Array(RES_AND, + Array( + Array(RES_EXIST, + Array(ULPROPTAG => $props["recurrenceend"], + ) + ), + Array(RES_PROPERTY, + Array(RELOP => RELOP_EQ, + ULPROPTAG => $props["isrecurring"], + VALUE => true + ) + ), + Array(RES_PROPERTY, + Array(RELOP => RELOP_GE, + ULPROPTAG => $props["recurrenceend"], + VALUE => $start + ) + ) + ) + ), + // OR + // (!EXIST(recurrence_enddate_property) && item[isRecurring] == true && item[start] <= end) + Array(RES_AND, + Array( + Array(RES_NOT, + Array( + Array(RES_EXIST, + Array(ULPROPTAG => $props["recurrenceend"] + ) + ) + ) + ), + Array(RES_PROPERTY, + Array(RELOP => RELOP_LE, + ULPROPTAG => $props["starttime"], + VALUE => $end + ) + ), + Array(RES_PROPERTY, + Array(RELOP => RELOP_EQ, + ULPROPTAG => $props["isrecurring"], + VALUE => true + ) + ) + ) + ) + ) + ) // EXISTS OR + ) + ); // global OR + + return $restriction; + } + + + /** + * Create a MAPI restriction in order to check if a contact has a picture + * + * @access public + * @return array + */ + public static function GetContactPicRestriction() { + return array ( RES_PROPERTY, + array ( + RELOP => RELOP_EQ, + ULPROPTAG => mapi_prop_tag(PT_BOOLEAN, 0x7FFF), + VALUE => true + ) + ); + } + + + /** + * Create a MAPI restriction for search + * + * @access public + * + * @param string $query + * @return array + */ + public static function GetSearchRestriction($query) { + return array(RES_AND, + array( + array(RES_OR, + array( + array(RES_CONTENT, array(FUZZYLEVEL => FL_SUBSTRING | FL_IGNORECASE, ULPROPTAG => PR_DISPLAY_NAME, VALUE => $query)), + array(RES_CONTENT, array(FUZZYLEVEL => FL_SUBSTRING | FL_IGNORECASE, ULPROPTAG => PR_ACCOUNT, VALUE => $query)), + array(RES_CONTENT, array(FUZZYLEVEL => FL_SUBSTRING | FL_IGNORECASE, ULPROPTAG => PR_SMTP_ADDRESS, VALUE => $query)), + ), // RES_OR + ), + array(RES_OR, + array ( + array( + RES_PROPERTY, + array(RELOP => RELOP_EQ, ULPROPTAG => PR_OBJECT_TYPE, VALUE => MAPI_MAILUSER) + ), + array( + RES_PROPERTY, + array(RELOP => RELOP_EQ, ULPROPTAG => PR_OBJECT_TYPE, VALUE => MAPI_DISTLIST) + ) + ) + ) // RES_OR + ) // RES_AND + ); + } + + /** + * Create a MAPI restriction for a certain email address + * + * @access public + * + * @param MAPIStore $store the MAPI store + * @param string $query email address + * + * @return array + */ + public static function GetEmailAddressRestriction($store, $email) { + $props = MAPIMapping::GetContactProperties(); + $props = getPropIdsFromStrings($store, $props); + + return array(RES_OR, + array( + array( RES_PROPERTY, + array( RELOP => RELOP_EQ, + ULPROPTAG => $props['emailaddress1'], + VALUE => array($props['emailaddress1'] => $email), + ), + ), + array( RES_PROPERTY, + array( RELOP => RELOP_EQ, + ULPROPTAG => $props['emailaddress2'], + VALUE => array($props['emailaddress2'] => $email), + ), + ), + array( RES_PROPERTY, + array( RELOP => RELOP_EQ, + ULPROPTAG => $props['emailaddress3'], + VALUE => array($props['emailaddress3'] => $email), + ), + ), + ), + ); + } + + /** + * Create a MAPI restriction for a certain folder type + * + * @access public + * + * @param string $foldertype folder type for restriction + * @return array + */ + public static function GetFolderTypeRestriction($foldertype) { + return array( RES_PROPERTY, + array( RELOP => RELOP_EQ, + ULPROPTAG => PR_CONTAINER_CLASS, + VALUE => array(PR_CONTAINER_CLASS => $foldertype) + ), + ); + } + + /** + * Returns subfolders of given type for a folder or false if there are none. + * + * @access public + * + * @param MAPIFolder $folder + * @param string $type + * + * @return MAPITable|boolean + */ + public static function GetSubfoldersForType($folder, $type) { + $subfolders = mapi_folder_gethierarchytable($folder, CONVENIENT_DEPTH); + mapi_table_restrict($subfolders, MAPIUtils::GetFolderTypeRestriction($type)); + if (mapi_table_getrowcount($subfolders) > 0) { + return mapi_table_queryallrows($subfolders, array(PR_ENTRYID)); + } + return false; + } + + /** + * Checks if mapimessage is inside the synchronization interval + * also defined by MAPIUtils::GetEmailRestriction() + * + * @param MAPIStore $store mapi store + * @param MAPIMessage $mapimessage the mapi message to be checked + * @param long $timestamp the lower time limit + * + * @access public + * @return boolean + */ + public static function IsInEmailSyncInterval($store, $mapimessage, $timestamp) { + $p = mapi_getprops($mapimessage, array(PR_MESSAGE_DELIVERY_TIME)); + + if (isset($p[PR_MESSAGE_DELIVERY_TIME]) && $p[PR_MESSAGE_DELIVERY_TIME] >= $timestamp) { + ZLog::Write(LOGLEVEL_DEBUG, "MAPIUtils->IsInEmailSyncInterval: Message is in the synchronization interval"); + return true; + } + + ZLog::Write(LOGLEVEL_WARN, "MAPIUtils->IsInEmailSyncInterval: Message is OUTSIDE the synchronization interval"); + return false; + } + + /** + * Checks if mapimessage is inside the synchronization interval + * also defined by MAPIUtils::GetCalendarRestriction() + * + * @param MAPIStore $store mapi store + * @param MAPIMessage $mapimessage the mapi message to be checked + * @param long $timestamp the lower time limit + * + * @access public + * @return boolean + */ + public static function IsInCalendarSyncInterval($store, $mapimessage, $timestamp) { + // This is our viewing window + $start = $timestamp; + $end = 0x7fffffff; // infinite end + + $props = MAPIMapping::GetAppointmentProperties(); + $props = getPropIdsFromStrings($store, $props); + + $p = mapi_getprops($mapimessage, array($props["starttime"], $props["endtime"], $props["recurrenceend"], $props["isrecurring"], $props["recurrenceend"])); + + if ( + ( + isset($p[$props["endtime"]]) && isset($p[$props["starttime"]]) && + + //item.end > window.start && item.start < window.end + $p[$props["endtime"]] > $start && $p[$props["starttime"]] < $end + ) + || + ( + isset($p[$props["isrecurring"]]) && + + //(EXIST(recurrence_enddate_property) && item[isRecurring] == true && recurrence_enddate_property >= start) + isset($p[$props["recurrenceend"]]) && $p[$props["isrecurring"]] == true && $p[$props["recurrenceend"]] >= $start + ) + || + ( + isset($p[$props["isrecurring"]]) && isset($p[$props["starttime"]]) && + + //(!EXIST(recurrence_enddate_property) && item[isRecurring] == true && item[start] <= end) + !isset($p[$props["recurrenceend"]]) && $p[$props["isrecurring"]] == true && $p[$props["starttime"]] <= $end + ) + ) { + ZLog::Write(LOGLEVEL_DEBUG, "MAPIUtils->IsInCalendarSyncInterval: Message is in the synchronization interval"); + return true; + } + + + ZLog::Write(LOGLEVEL_WARN, "MAPIUtils->IsInCalendarSyncInterval: Message is OUTSIDE the synchronization interval"); + return false; + } + + + /** + * Reads data of large properties from a stream + * + * @param MAPIMessage $message + * @param long $prop + * + * @access public + * @return string + */ + public static function readPropStream($message, $prop) { + $stream = mapi_openproperty($message, $prop, IID_IStream, 0, 0); + $ret = mapi_last_hresult(); + if ($ret == MAPI_E_NOT_FOUND) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIUtils->readPropStream: property 0x%s not found. It is either empty or not set. It will be ignored.", str_pad(dechex($prop), 8, 0, STR_PAD_LEFT))); + return ""; + } + elseif ($ret) { + ZLog::Write(LOGLEVEL_ERROR, "MAPIUtils->readPropStream error opening stream: 0X%X", $ret); + return ""; + } + $data = ""; + $string = ""; + while(1) { + $data = mapi_stream_read($stream, 1024); + if(strlen($data) == 0) + break; + $string .= $data; + } + + return $string; + } + + + /** + * Checks if a store supports properties containing unicode characters + * + * @param MAPIStore $store + * + * @access public + * @return + */ + public static function IsUnicodeStore($store) { + $supportmask = mapi_getprops($store, array(PR_STORE_SUPPORT_MASK)); + if (isset($supportmask[PR_STORE_SUPPORT_MASK]) && ($supportmask[PR_STORE_SUPPORT_MASK] & STORE_UNICODE_OK)) { + ZLog::Write(LOGLEVEL_DEBUG, "Store supports properties containing Unicode characters."); + define('STORE_SUPPORTS_UNICODE', true); + //setlocale to UTF-8 in order to support properties containing Unicode characters + setlocale(LC_CTYPE, "en_US.UTF-8"); + define('STORE_INTERNET_CPID', INTERNET_CPID_UTF8); + } + } + + /** + * Returns the MAPI PR_CONTAINER_CLASS string for an ActiveSync Foldertype + * + * @param int $foldertype + * + * @access public + * @return string + */ + public static function GetContainerClassFromFolderType($foldertype) { + switch ($foldertype) { + case SYNC_FOLDER_TYPE_TASK: + case SYNC_FOLDER_TYPE_USER_TASK: + return "IPF.Task"; + break; + + case SYNC_FOLDER_TYPE_APPOINTMENT: + case SYNC_FOLDER_TYPE_USER_APPOINTMENT: + return "IPF.Appointment"; + break; + + case SYNC_FOLDER_TYPE_CONTACT: + case SYNC_FOLDER_TYPE_USER_CONTACT: + return "IPF.Contact"; + break; + + case SYNC_FOLDER_TYPE_NOTE: + case SYNC_FOLDER_TYPE_USER_NOTE: + return "IPF.StickyNote"; + break; + + case SYNC_FOLDER_TYPE_JOURNAL: + case SYNC_FOLDER_TYPE_USER_JOURNAL: + return "IPF.Journal"; + break; + + case SYNC_FOLDER_TYPE_INBOX: + case SYNC_FOLDER_TYPE_DRAFTS: + case SYNC_FOLDER_TYPE_WASTEBASKET: + case SYNC_FOLDER_TYPE_SENTMAIL: + case SYNC_FOLDER_TYPE_OUTBOX: + case SYNC_FOLDER_TYPE_USER_MAIL: + case SYNC_FOLDER_TYPE_OTHER: + case SYNC_FOLDER_TYPE_UNKNOWN: + default: + return "IPF.Note"; + break; + } + } + + public static function GetSignedAttachmentRestriction() { + return array( RES_PROPERTY, + array( RELOP => RELOP_EQ, + ULPROPTAG => PR_ATTACH_MIME_TAG, + VALUE => array(PR_ATTACH_MIME_TAG => 'multipart/signed') + ), + ); + } + +} + +?> \ No newline at end of file diff --git a/sources/backend/zarafa/tnefparser.php b/sources/backend/zarafa/tnefparser.php new file mode 100644 index 0000000..d9eece6 --- /dev/null +++ b/sources/backend/zarafa/tnefparser.php @@ -0,0 +1,721 @@ +. +* +* Consult LICENSE file for details +************************************************/ +/** + * For more information on tnef refer to: + * http://msdn.microsoft.com/en-us/library/ms530652(EXCHG.10).aspx + * http://msdn.microsoft.com/en-us/library/cc425498(EXCHG.80).aspx + * + * The mapping between Microsoft Mail IPM classes and those used in + * MAPI see: http://msdn2.microsoft.com/en-us/library/ms527360.aspx + */ + +class TNEFParser { + const TNEF_SIGNATURE = 0x223e9f78; + const TNEF_LVL_MESSAGE = 0x01; + const TNEF_LVL_ATTACHMENT = 0x02; + const DWORD = 32; + const WORD = 16; + const BYTE = 8; + + /** + * Constructor + * We need a store in order to get the namedpropers + * + * @param mapistore $store + * @param array &$props properties to be set + * + * @access public + */ + public function TNEFParser(&$store, &$props) { + $this->store = $store; + $this->props = $props; + } + + /** + * Function reads tnef stream and puts mapi properties into an array. + * + * @param string $tnefstream + * @param array &$mapiprops mapi properties + * + * @access public + * @return int + */ + public function ExtractProps($tnefstream, &$mapiprops) { + $hresult = NOERROR; + $signature = 0; //tnef signature - 32 Bit + $key = 0; //a nonzero 16-bit unsigned integer + + $type = 0; // 32-bit value + $size = 0; // 32-bit value + $checksum = 0; //16-bit value + $component = 0; //8-bit value - either self::TNEF_LVL_MESSAGE or self::TNEF_LVL_ATTACHMENT + $buffer = ""; + + //mapping between Microsoft Mail IPM classes and those in MAPI + $aClassMap = array( + "IPM.Microsoft Schedule.MtgReq" => "IPM.Schedule.Meeting.Request", + "IPM.Microsoft Schedule.MtgRespP" => "IPM.Schedule.Meeting.Resp.Pos", + "IPM.Microsoft Schedule.MtgRespN" => "IPM.Schedule.Meeting.Resp.Neg", + "IPM.Microsoft Schedule.MtgRespA" => "IPM.Schedule.Meeting.Resp.Tent", + "IPM.Microsoft Schedule.MtgCncl" => "IPM.Schedule.Meeting.Canceled", + "IPM.Microsoft Mail.Non-Delivery" => "Report.IPM.Note.NDR", + "IPM.Microsoft Mail.Read Receipt" => "Report.IPM.Note.IPNRN", + "IPM.Microsoft Mail.Note" => "IPM.Note", + "IPM.Microsoft Mail.Note" => "IPM", + ); + + //read signature + $hresult = $this->readFromTnefStream($tnefstream, self::DWORD, $signature); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: STREAM:".bin2hex($tnefstream)); + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading tnef signature"); + return $hresult; + } + + //check signature + if ($signature != self::TNEF_SIGNATURE) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: Corrupt signature."); + return MAPI_E_CORRUPT_DATA; + } + + //read key + $hresult = $this->readFromTnefStream($tnefstream, self::WORD, $key); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading tnef key."); + return $hresult; + } + + // File is made of blocks, with each a type and size. Component and Key are ignored. + while(1) { + //the stream is empty. exit + if (strlen($tnefstream) == 0) return NOERROR; + + //read component - it is either self::TNEF_LVL_MESSAGE or self::TNEF_LVL_ATTACHMENT + $hresult = $this->readFromTnefStream($tnefstream, self::BYTE, $component); + if ($hresult !== NOERROR) { + $hresult = NOERROR; //EOF -> no error + return $hresult; + break; + } + + //read type + $hresult = $this->readFromTnefStream($tnefstream, self::DWORD, $type); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property type"); + return $hresult; + } + + //read size + $hresult = $this->readFromTnefStream($tnefstream, self::DWORD, $size); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property size"); + return $hresult; + } + + if ($size == 0) { + // do not allocate 0 size data block + ZLog::Write(LOGLEVEL_WARN, "TNEF: Size is 0. Corrupt data."); + return MAPI_E_CORRUPT_DATA; + } + + //read buffer + $hresult = $this->readBuffer($tnefstream, $size, $buffer); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + + //read checksum + $hresult = $this->readFromTnefStream($tnefstream, self::WORD, $checksum); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property checksum."); + return $hresult; + } + + // Loop through all the blocks of the TNEF data. We are only interested + // in the properties block for now (0x00069003) + switch ($type) { + case 0x00069003: + $hresult = $this->readMapiProps($buffer, $size, $mapiprops); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading mapi properties' part."); + return $hresult; + } + break; + case 0x00078008: // PR_MESSAGE_CLASS + $msMailClass = trim($buffer); + if (array_key_exists($msMailClass, $aClassMap)) { + $messageClass = $aClassMap[$msMailClass]; + } + else { + $messageClass = $msMailClass; + } + $mapiprops[PR_MESSAGE_CLASS] = $messageClass; + break; + case 0x00050008: // PR_OWNER_APPT_ID + $mapiprops[PR_OWNER_APPT_ID] = $buffer; + break; + case 0x00040009: // PR_RESPONSE_REQUESTED + $mapiprops[PR_RESPONSE_REQUESTED] = $buffer; + break; + + // --- TNEF attachemnts --- + case 0x00069002: + break; + case 0x00018010: // PR_ATTACH_FILENAME + break; + case 0x00068011: // PR_ATTACH_RENDERING, extra icon information + break; + case 0x0006800f: // PR_ATTACH_DATA_BIN, will be set via OpenProperty() in ECTNEF::Finish() + break; + case 0x00069005: // Attachment property stream + break; + default: + // Ignore this block + break; + } + } + return NOERROR; + } + + /** + * Reads a given number of bits from stream and converts them from little indian in a "normal" order. The function + * also cuts the tnef stream after reading. + * + * @param string &$tnefstream + * @param int $bits + * @param array &$element the read element + * + * @access private + * @return int + */ + private function readFromTnefStream(&$tnefstream, $bits, &$element) { + $bytes = $bits / 8; + + $part = substr($tnefstream, 0, $bytes); + $packs = array(); + + switch ($bits) { + case self::DWORD: + $packs = unpack("V", $part); + break; + case self::WORD: + $packs = unpack("v", $part); + break; + case self::BYTE: + $packs[1] = ord($part[0]); + break; + default: + $packs = array(); + break; + } + + if (empty($packs) || !isset($packs[1])) return MAPI_E_CORRUPT_DATA; + + $tnefstream = substr($tnefstream, $bytes); + $element = $packs[1]; + return NOERROR; + } + + /** + * Reads a given number of bytes from stream and puts them into $element. The function + * also cuts the tnef stream after reading. + * + * @param string &$tnefstream + * @param int $bytes + * @param array &$element the read element + * + * @access private + * @return int + */ + private function readBuffer(&$tnefstream, $bytes, &$element) { + $element = substr($tnefstream, 0, $bytes); + $tnefstream = substr($tnefstream, $bytes); + return NOERROR; + + } + + /** + * Reads mapi props from buffer into an anrray. + * + * @param string &$buffer + * @param int $size + * @param array &$mapiprops + * + * @access private + * @return int + */ + function readMapiProps(&$buffer, $size, &$mapiprops) { + $nrprops = 0; + //get number of mapi properties + $hresult = $this->readFromTnefStream($buffer, self::DWORD, $nrprops); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error getting the number of mapi properties in stream."); + return $hresult; + } + + $size -= 4; + + ZLog::Write(LOGLEVEL_DEBUG, "TNEF: nrprops:$nrprops"); + //loop through all the properties and add them to our internal list + while($nrprops) { + $hresult = $this->readSingleMapiProp($buffer, $size, $read, $mapiprops); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading a mapi property."); + ZLog::Write(LOGLEVEL_WARN, "TNEF: result: " . sprintf("0x%X", $hresult)); + + return $hresult; + } + $nrprops--; + } + return NOERROR; + } + + /** + * Reads a single mapi prop. + * + * @param string &$buffer + * @param int $size + * @param mixed &$read + * @param array &$mapiprops + * + * @access private + * @return int + */ + private function readSingleMapiProp(&$buffer, &$size, &$read, &$mapiprops) { + $propTag = 0; + $len = 0; + $origSize = $size; + $isNamedId = 0; + $namedProp = 0; + $count = 0; + $mvProp = 0; + $guid = 0; + + if($size < 8) { + return MAPI_E_NOT_FOUND; + } + + $hresult = $this->readFromTnefStream($buffer, self::DWORD, $propTag); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading a mapi property tag from the stream."); + return $hresult; + } + $size -= 4; + ZLog::Write(LOGLEVEL_DEBUG, "TNEF: mapi prop type:".dechex(mapi_prop_type($propTag))); + ZLog::Write(LOGLEVEL_DEBUG, "TNEF: mapi prop tag: 0x".sprintf("%04x", mapi_prop_id($propTag))); + if (mapi_prop_id($propTag) >= 0x8000) { + // Named property, first read GUID, then name/id + if($size < 24) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: Corrupt guid size for named property:".dechex($propTag)); + return MAPI_E_CORRUPT_DATA; + } + //strip GUID & name/id + $hresult = $this->readBuffer($buffer, 16, $guid); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + + $size -= 16; + //it is not used and is here only for eventual debugging + $readableGuid = unpack("VV/v2v/n4n", $guid); + $readableGuid = sprintf("{%08x-%04x-%04x-%04x-%04x%04x%04x}",$readableGuid['V'], $readableGuid['v1'], $readableGuid['v2'],$readableGuid['n1'],$readableGuid['n2'],$readableGuid['n3'],$readableGuid['n4']); + ZLog::Write(LOGLEVEL_DEBUG, "TNEF: guid:$readableGuid"); + $hresult = $this->readFromTnefStream($buffer, self::DWORD, $isNamedId); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property checksum."); + return $hresult; + } + $size -= 4; + + if($isNamedId != 0) { + // A string name follows + //read length of the property + $hresult = $this->readFromTnefStream($buffer, self::DWORD, $len); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading mapi property's length"); + return $hresult; + } + $size -= 4; + if ($size < $len) { + return MAPI_E_CORRUPT_DATA; + } + //read the name of the property, eg Keywords + $hresult = $this->readBuffer($buffer, $len, $namedProp); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + + $size -= $len; + + //Re-align + $buffer = substr($buffer, ($len & 3 ? 4 - ($len & 3) : 0)); + $size -= $len & 3 ? 4 - ($len & 3) : 0; + } + else { + $hresult = $this->readFromTnefStream($buffer, self::DWORD, $namedProp); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading mapi property's length"); + return $hresult; + } + ZLog::Write(LOGLEVEL_DEBUG, "TNEF: named: 0x".sprintf("%04x", $namedProp)); + $size -= 4; + } + + if ($this->store !== false) { + $named = mapi_getidsfromnames($this->store, array($namedProp), array(makeguid($readableGuid))); + + $propTag = mapi_prop_tag(mapi_prop_type($propTag), mapi_prop_id($named[0])); + } + else { + ZLog::Write(LOGLEVEL_WARN, "TNEF: Store not available. It is impossible to get named properties"); + } + } + ZLog::Write(LOGLEVEL_DEBUG, "TNEF: mapi prop tag: 0x".sprintf("%04x", mapi_prop_id($propTag))." ".sprintf("%04x", mapi_prop_type($propTag))); + if($propTag & MV_FLAG) { + if($size < 4) { + return MAPI_E_CORRUPT_DATA; + } + //read the number of properties + $hresult = $this->readFromTnefStream($buffer, self::DWORD, $count); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading number of properties for:".dechex($propTag)); + return $hresult; + } + $size -= 4; + } + else { + $count = 1; + } + + for ($mvProp = 0; $mvProp < $count; $mvProp++) { + switch(mapi_prop_type($propTag) & ~MV_FLAG ) { + case PT_I2: + case PT_LONG: + $hresult = $this->readBuffer($buffer, 4, $value); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + $value = unpack("V", $value); + $value = intval($value[1], 16); + + if($propTag & MV_FLAG) { + $mapiprops[$propTag][] = $value; + } + else { + $mapiprops[$propTag] = $value; + } + $size -= 4; + ZLog::Write(LOGLEVEL_DEBUG, "TNEF: int or long propvalue:".$value); + break; + + case PT_R4: + if($propTag & MV_FLAG) { + $hresult = $this->readBuffer($buffer, 4, $mapiprops[$propTag][]); + + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + } + else { + $hresult = $this->readBuffer($buffer, 4, $mapiprops[$propTag]); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + } + $size -= 4; + ZLog::Write(LOGLEVEL_DEBUG, "TNEF: propvalue:".$mapiprops[$propTag]); + break; + + case PT_BOOLEAN: + $hresult = $this->readBuffer($buffer, 4, $mapiprops[$propTag]); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + $size -= 4; + //reported by dw2412 + //cast to integer as it evaluates to 1 or 0 because + //a non empty string evaluates to true :( + $mapiprops[$propTag] = (integer) bin2hex($mapiprops[$propTag]{0}); + ZLog::Write(LOGLEVEL_DEBUG, "TNEF: propvalue:".$mapiprops[$propTag]); + break; + + + case PT_SYSTIME: + if($size < 8) { + return MAPI_E_CORRUPT_DATA; + } + if($propTag & MV_FLAG) { + $hresult = $this->readBuffer($buffer, 8, $mapiprops[$propTag][]); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + } + else { + $hresult = $this->readBuffer($buffer, 8, $mapiprops[$propTag]); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + } + //we have to convert the filetime to an unixtime timestamp + $filetime = unpack("V2v", $mapiprops[$propTag]); + //php on 64-bit systems converts unsigned values differently than on 32 bit systems + //we need this "fix" in order to get the same values on both types of systems + $filetime['v2'] = substr(sprintf("%08x",$filetime['v2']), -8); + $filetime['v1'] = substr(sprintf("%08x",$filetime['v1']), -8); + + $filetime = hexdec($filetime['v2'].$filetime['v1']); + $filetime = ($filetime - 116444736000000000) / 10000000; + $mapiprops[$propTag] = $filetime; + // we have to set the start and end times separately because the standard PR_START_DATE and PR_END_DATE aren't enough + if ($propTag == PR_START_DATE) { + $mapiprops[$this->props["starttime"]] = $mapiprops[$this->props["commonstart"]] = $filetime; + } + if ($propTag == PR_END_DATE) { + $mapiprops[$this->props["endtime"]] = $mapiprops[$this->props["commonend"]] = $filetime; + } + $size -= 8; + ZLog::Write(LOGLEVEL_DEBUG, "TNEF: propvalue:".$mapiprops[$propTag]); + break; + + case PT_DOUBLE: + case PT_CURRENCY: + case PT_I8: + case PT_APPTIME: + if($size < 8) { + return MAPI_E_CORRUPT_DATA; + } + if($propTag & MV_FLAG) { + $hresult = $this->readBuffer($buffer, 8, $mapiprops[$propTag][]); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + } + else { + $hresult = $this->readBuffer($buffer, 8, $mapiprops[$propTag]); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + } + $size -= 8; + ZLog::Write(LOGLEVEL_DEBUG, "TNEF: propvalue:".$mapiprops[$propTag]); + break; + + case PT_STRING8: + if($size < 8) { + return MAPI_E_CORRUPT_DATA; + } + // Skip next 4 bytes, it's always '1' (ULONG) + $buffer = substr($buffer, 4); + $size -= 4; + + //read length of the property + $hresult = $this->readFromTnefStream($buffer, self::DWORD, $len); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading mapi property's length"); + return $hresult; + } + $size -= 4; + if ($size < $len) { + return MAPI_E_CORRUPT_DATA; + } + + if ($propTag & MV_FLAG) { + $hresult = $this->readBuffer($buffer, $len, $mapiprops[$propTag][]); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + } + else { + $hresult = $this->readBuffer($buffer, $len, $mapiprops[$propTag]); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + } + //location fix. it looks like tnef uses this value for location + if (mapi_prop_id($propTag) == 0x8342) { + $mapiprops[$this->props["location"]] = $mapiprops[$propTag]; + unset($mapiprops[$propTag]); + } + + $size -= $len; + + //Re-align + $buffer = substr($buffer, ($len & 3 ? 4 - ($len & 3) : 0)); + $size -= $len & 3 ? 4 - ($len & 3) : 0; + ZLog::Write(LOGLEVEL_DEBUG, "TNEF: propvalue:".$mapiprops[$propTag]); + break; + + case PT_UNICODE: + if($size < 8) { + return MAPI_E_CORRUPT_DATA; + } + // Skip next 4 bytes, it's always '1' (ULONG) + $buffer = substr($buffer, 4); + $size -= 4; + + //read length of the property + $hresult = $this->readFromTnefStream($buffer, self::DWORD, $len); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading mapi property's length"); + return $hresult; + } + $size -= 4; + if ($size < $len) { + return MAPI_E_CORRUPT_DATA; + } + //currently unicode strings are not supported bz mapi_setprops, so we'll use PT_STRING8 + $propTag = mapi_prop_tag(PT_STRING8, mapi_prop_id($propTag)); + + if ($propTag & MV_FLAG) { + $hresult = $this->readBuffer($buffer, $len, $mapiprops[$propTag][]); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + } + else { + $hresult = $this->readBuffer($buffer, $len, $mapiprops[$propTag]); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + } + + //location fix. it looks like tnef uses this value for location + if (mapi_prop_id($propTag) == 0x8342) { + $mapiprops[$this->props["location"]] = iconv("UCS-2","windows-1252", $mapiprops[$propTag]); + unset($mapiprops[$propTag]); + } + + //convert from unicode to windows encoding + if (isset($mapiprops[$propTag])) $mapiprops[$propTag] = iconv("UCS-2","windows-1252", $mapiprops[$propTag]); + $size -= $len; + + //Re-align + $buffer = substr($buffer, ($len & 3 ? 4 - ($len & 3) : 0)); + $size -= $len & 3 ? 4 - ($len & 3) : 0; + if (isset($mapiprops[$propTag])) ZLog::Write(LOGLEVEL_DEBUG, "TNEF: propvalue:".$mapiprops[$propTag]); + break; + + case PT_OBJECT: // PST sends PT_OBJECT data. Treat as PT_BINARY + case PT_BINARY: + if($size < self::BYTE) { + return MAPI_E_CORRUPT_DATA; + } + // Skip next 4 bytes, it's always '1' (ULONG) + $buffer = substr($buffer, 4); + $size -= 4; + + //read length of the property + $hresult = $this->readFromTnefStream($buffer, self::DWORD, $len); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading mapi property's length"); + return $hresult; + } + $size -= 4; + + if (mapi_prop_type($propTag) == PT_OBJECT) { + // IMessage guid [ 0x00020307 C000 0000 0000 0000 00 00 00 46 ] + $buffer = substr($buffer, 16); + $size -= 16; + $len -= 16; + } + + if ($size < $len) { + return MAPI_E_CORRUPT_DATA; + } + + if ($propTag & MV_FLAG) { + $hresult = $this->readBuffer($buffer, $len, $mapiprops[$propTag][]); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + } + else { + $hresult = $this->readBuffer($buffer, $len, $mapiprops[$propTag]); + if ($hresult !== NOERROR) { + ZLog::Write(LOGLEVEL_WARN, "TNEF: There was an error reading stream property buffer"); + return $hresult; + } + } + + $size -= $len; + + //Re-align + $buffer = substr($buffer, ($len & 3 ? 4 - ($len & 3) : 0)); + $size -= $len & 3 ? 4 - ($len & 3) : 0; + ZLog::Write(LOGLEVEL_DEBUG, "TNEF: propvalue:".bin2hex($mapiprops[$propTag])); + break; + + default: + return MAPI_E_INVALID_PARAMETER; + break; + } + } + return NOERROR; + } +} +?> \ No newline at end of file diff --git a/sources/backend/zarafa/zarafa.php b/sources/backend/zarafa/zarafa.php new file mode 100644 index 0000000..7f2b37a --- /dev/null +++ b/sources/backend/zarafa/zarafa.php @@ -0,0 +1,1841 @@ +. +* +* Consult LICENSE file for details +*************************************************/ + +// config file +require_once("backend/zarafa/config.php"); + +// include PHP-MAPI classes +include_once('backend/zarafa/mapi/mapi.util.php'); +include_once('backend/zarafa/mapi/mapidefs.php'); +include_once('backend/zarafa/mapi/mapitags.php'); +include_once('backend/zarafa/mapi/mapicode.php'); +include_once('backend/zarafa/mapi/mapiguid.php'); +include_once('backend/zarafa/mapi/class.baseexception.php'); +include_once('backend/zarafa/mapi/class.mapiexception.php'); +include_once('backend/zarafa/mapi/class.baserecurrence.php'); +include_once('backend/zarafa/mapi/class.taskrecurrence.php'); +include_once('backend/zarafa/mapi/class.recurrence.php'); +include_once('backend/zarafa/mapi/class.meetingrequest.php'); +include_once('backend/zarafa/mapi/class.freebusypublish.php'); + +// processing of RFC822 messages +include_once('include/mimeDecode.php'); +require_once('include/z_RFC822.php'); + +// components of Zarafa backend +include_once('backend/zarafa/mapiutils.php'); +include_once('backend/zarafa/mapimapping.php'); +include_once('backend/zarafa/mapiprovider.php'); +include_once('backend/zarafa/mapiphpwrapper.php'); +include_once('backend/zarafa/mapistreamwrapper.php'); +include_once('backend/zarafa/importer.php'); +include_once('backend/zarafa/exporter.php'); + + +class BackendZarafa implements IBackend, ISearchProvider { + private $mainUser; + private $session; + private $defaultstore; + private $store; + private $storeName; + private $storeCache; + private $importedFolders; + private $notifications; + private $changesSink; + private $changesSinkFolders; + private $changesSinkStores; + private $wastebasket; + private $addressbook; + + /** + * Constructor of the Zarafa Backend + * + * @access public + */ + public function BackendZarafa() { + $this->session = false; + $this->store = false; + $this->storeName = false; + $this->storeCache = array(); + $this->importedFolders = array(); + $this->notifications = false; + $this->changesSink = false; + $this->changesSinkFolders = array(); + $this->changesSinkStores = array(); + $this->wastebasket = false; + $this->session = false; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa using PHP-MAPI version: %s", phpversion("mapi"))); + } + + /** + * Indicates which StateMachine should be used + * + * @access public + * @return boolean ZarafaBackend uses the default FileStateMachine + */ + public function GetStateMachine() { + return false; + } + + /** + * Returns the ZarafaBackend 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; + } + + /** + * Indicates which AS version is supported by the backend. + * + * @access public + * @return string AS version constant + */ + public function GetSupportedASVersion() { + return ZPush::ASV_14; + } + + /** + * Authenticates the user with the configured Zarafa server + * + * @param string $username + * @param string $domain + * @param string $password + * + * @access public + * @return boolean + * @throws AuthenticationRequiredException + */ + public function Logon($user, $domain, $pass) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->Logon(): Trying to authenticate user '%s'..", $user)); + $this->mainUser = strtolower($user); + + try { + // check if notifications are available in php-mapi + if(function_exists('mapi_feature') && mapi_feature('LOGONFLAGS')) { + $this->session = @mapi_logon_zarafa($user, $pass, MAPI_SERVER, null, null, 0); + $this->notifications = true; + } + // old fashioned session + else { + $this->session = @mapi_logon_zarafa($user, $pass, MAPI_SERVER); + $this->notifications = false; + } + + if (mapi_last_hresult()) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZarafaBackend->Logon(): login failed with error code: 0x%X", mapi_last_hresult())); + if (mapi_last_hresult() == MAPI_E_NETWORK_ERROR) + throw new HTTPReturnCodeException("Error connecting to ZCP (login)", 503, null, LOGLEVEL_INFO); + } + } + catch (MAPIException $ex) { + throw new AuthenticationRequiredException($ex->getDisplayMessage()); + } + + if(!$this->session) { + ZLog::Write(LOGLEVEL_WARN, sprintf("ZarafaBackend->Logon(): logon failed for user '%s'", $user)); + $this->defaultstore = false; + return false; + } + + // Get/open default store + $this->defaultstore = $this->openMessageStore($this->mainUser); + + if (mapi_last_hresult() == MAPI_E_FAILONEPROVIDER) + throw new HTTPReturnCodeException("Error connecting to ZCP (open store)", 503, null, LOGLEVEL_INFO); + + if($this->defaultstore === false) + throw new AuthenticationRequiredException(sprintf("ZarafaBackend->Logon(): User '%s' has no default store", $user)); + + $this->store = $this->defaultstore; + $this->storeName = $this->mainUser; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->Logon(): User '%s' is authenticated",$user)); + + // check if this is a Zarafa 7 store with unicode support + MAPIUtils::IsUnicodeStore($this->store); + return true; + } + + /** + * Setup the backend to work on a specific store or checks ACLs there. + * If only the $store is submitted, all Import/Export/Fetch/Etc operations should be + * performed on this store (switch operations store). + * If the ACL check is enabled, this operation should just indicate the ACL status on + * the submitted store, without changing the store for operations. + * For the ACL status, the currently logged on user MUST have access rights on + * - the entire store - admin access if no folderid is sent, or + * - on a specific folderid in the store (secretary/full access rights) + * + * The ACLcheck MUST fail if a folder of the authenticated user is checked! + * + * @param string $store target store, could contain a "domain\user" value + * @param boolean $checkACLonly if set to true, Setup() should just check ACLs + * @param string $folderid if set, only ACLs on this folderid are relevant + * + * @access public + * @return boolean + */ + public function Setup($store, $checkACLonly = false, $folderid = false) { + list($user, $domain) = Utils::SplitDomainUser($store); + + if (!isset($this->mainUser)) + return false; + + if ($user === false) + $user = $this->mainUser; + + // This is a special case. A user will get it's entire folder structure by the foldersync by default. + // The ACL check is executed when an additional folder is going to be sent to the mobile. + // Configured that way the user could receive the same folderid twice, with two different names. + if ($this->mainUser == $user && $checkACLonly && $folderid) { + ZLog::Write(LOGLEVEL_DEBUG, "ZarafaBackend->Setup(): Checking ACLs for folder of the users defaultstore. Fail is forced to avoid folder duplications on mobile."); + return false; + } + + // get the users store + $userstore = $this->openMessageStore($user); + + // only proceed if a store was found, else return false + if ($userstore) { + // only check permissions + if ($checkACLonly == true) { + // check for admin rights + if (!$folderid) { + if ($user != $this->mainUser) { + $zarafauserinfo = @mapi_zarafa_getuser_by_name($this->defaultstore, $this->mainUser); + $admin = (isset($zarafauserinfo['admin']) && $zarafauserinfo['admin'])?true:false; + } + // the user has always full access to his own store + else + $admin = true; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->Setup(): Checking for admin ACLs on store '%s': '%s'", $user, Utils::PrintAsString($admin))); + return $admin; + } + // check 'secretary' permissions on this folder + else { + $rights = $this->hasSecretaryACLs($userstore, $folderid); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->Setup(): Checking for secretary ACLs on '%s' of store '%s': '%s'", $folderid, $user, Utils::PrintAsString($rights))); + return $rights; + } + } + + // switch operations store + // this should also be done if called with user = mainuser or user = false + // which means to switch back to the default store + else { + // switch active store + $this->store = $userstore; + $this->storeName = $user; + return true; + } + } + return false; + } + + /** + * Logs off + * Free/Busy information is updated for modified calendars + * This is done after the synchronization process is completed + * + * @access public + * @return boolean + */ + public function Logoff() { + // update if the calendar which received incoming changes + foreach($this->importedFolders as $folderid => $store) { + // open the root of the store + $storeprops = mapi_getprops($store, array(PR_USER_ENTRYID)); + $root = mapi_msgstore_openentry($store); + if (!$root) + continue; + + // get the entryid of the main calendar of the store and the calendar to be published + $rootprops = mapi_getprops($root, array(PR_IPM_APPOINTMENT_ENTRYID)); + $entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($folderid)); + + // only publish free busy for the main calendar + if(isset($rootprops[PR_IPM_APPOINTMENT_ENTRYID]) && $rootprops[PR_IPM_APPOINTMENT_ENTRYID] == $entryid) { + ZLog::Write(LOGLEVEL_INFO, sprintf("ZarafaBackend->Logoff(): Updating freebusy information on folder id '%s'", $folderid)); + $calendar = mapi_msgstore_openentry($store, $entryid); + + $pub = new FreeBusyPublish($this->session, $store, $calendar, $storeprops[PR_USER_ENTRYID]); + $pub->publishFB(time() - (7 * 24 * 60 * 60), 6 * 30 * 24 * 60 * 60); // publish from one week ago, 6 months ahead + } + } + + return true; + } + + /** + * Returns an array of SyncFolder types with the entire folder hierarchy + * on the server (the array itself is flat, but refers to parents via the 'parent' property + * + * provides AS 1.0 compatibility + * + * @access public + * @return array SYNC_FOLDER + */ + public function GetHierarchy() { + $folders = array(); + $importer = false; + $mapiprovider = new MAPIProvider($this->session, $this->store); + + $rootfolder = mapi_msgstore_openentry($this->store); + $rootfolderprops = mapi_getprops($rootfolder, array(PR_SOURCE_KEY)); + $rootfoldersourcekey = bin2hex($rootfolderprops[PR_SOURCE_KEY]); + + $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH); + $rows = mapi_table_queryallrows($hierarchy, array(PR_ENTRYID)); + + foreach ($rows as $row) { + $mapifolder = mapi_msgstore_openentry($this->store, $row[PR_ENTRYID]); + $folder = $mapiprovider->GetFolder($mapifolder); + + if (isset($folder->parentid) && $folder->parentid != $rootfoldersourcekey) + $folders[] = $folder; + } + + return $folders; + } + + /** + * Returns the importer to process changes from the mobile + * If no $folderid is given, hierarchy importer is expected + * + * @param string $folderid (opt) + * + * @access public + * @return object(ImportChanges) + */ + public function GetImporter($folderid = false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->GetImporter() folderid: '%s'", Utils::PrintAsString($folderid))); + if($folderid !== false) { + // check if the user of the current store has permissions to import to this folderid + if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->GetImporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid))); + return false; + } + $this->importedFolders[$folderid] = $this->store; + return new ImportChangesICS($this->session, $this->store, hex2bin($folderid)); + } + else + return new ImportChangesICS($this->session, $this->store); + } + + /** + * Returns the exporter to send changes to the mobile + * If no $folderid is given, hierarchy exporter is expected + * + * @param string $folderid (opt) + * + * @access public + * @return object(ExportChanges) + * @throws StatusException + */ + public function GetExporter($folderid = false) { + if($folderid !== false) { + // check if the user of the current store has permissions to export from this folderid + if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->GetExporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid))); + return false; + } + return new ExportChangesICS($this->session, $this->store, hex2bin($folderid)); + } + else + return new ExportChangesICS($this->session, $this->store); + } + + /** + * Sends an e-mail + * This messages needs to be saved into the 'sent items' folder + * + * @param SyncSendMail $sm SyncSendMail object + * + * @access public + * @return boolean + * @throws StatusException + */ + public function SendMail($sm) { + // Check if imtomapi function is available and use it to send the mime message. + // It is available since ZCP 7.0.6 + // @see http://jira.zarafa.com/browse/ZCP-9508 + if (!(function_exists('mapi_feature') && mapi_feature('INETMAPI_IMTOMAPI'))) { + throw new StatusException("ZarafaBackend->SendMail(): ZCP version is too old, INETMAPI_IMTOMAPI is not available. Install at least ZCP version 7.0.6 or later.", SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED, null, LOGLEVEL_FATAL); + return false; + } + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->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), + Utils::PrintAsString((isset($sm->source->folderid) ? $sm->source->folderid : false)), + 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); + + $sendMailProps = MAPIMapping::GetSendMailProperties(); + $sendMailProps = getPropIdsFromStrings($this->store, $sendMailProps); + + // Open the outbox and create the message there + $storeprops = mapi_getprops($this->store, array($sendMailProps["outboxentryid"], $sendMailProps["ipmsentmailentryid"])); + if(isset($storeprops[$sendMailProps["outboxentryid"]])) + $outbox = mapi_msgstore_openentry($this->store, $storeprops[$sendMailProps["outboxentryid"]]); + + if(!$outbox) + throw new StatusException(sprintf("ZarafaBackend->SendMail(): No Outbox found or unable to create message: 0x%X", mapi_last_hresult()), SYNC_COMMONSTATUS_SERVERERROR); + + $mapimessage = mapi_folder_createmessage($outbox); + + //message properties to be set + $mapiprops = array(); + // only save the outgoing in sent items folder if the mobile requests it + $mapiprops[$sendMailProps["sentmailentryid"]] = $storeprops[$sendMailProps["ipmsentmailentryid"]]; + + ZLog::Write(LOGLEVEL_DEBUG, "Use the mapi_inetmapi_imtomapi function"); + $ab = mapi_openaddressbook($this->session); + mapi_inetmapi_imtomapi($this->session, $this->store, $ab, $mapimessage, $sm->mime, array()); + + // Set the appSeqNr so that tracking tab can be updated for meeting request updates + // @see http://jira.zarafa.com/browse/ZP-68 + $meetingRequestProps = MAPIMapping::GetMeetingRequestProperties(); + $meetingRequestProps = getPropIdsFromStrings($this->store, $meetingRequestProps); + $props = mapi_getprops($mapimessage, array(PR_MESSAGE_CLASS, $meetingRequestProps["goidtag"], $sendMailProps["internetcpid"])); + + // Convert sent message's body to UTF-8. + // @see http://jira.zarafa.com/browse/ZP-505 + if (isset($props[$sendMailProps["internetcpid"]]) && $props[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Sent email cpid is not unicode (%d). Set it to unicode and convert email body.", $props[$sendMailProps["internetcpid"]])); + $mapiprops[$sendMailProps["internetcpid"]] = INTERNET_CPID_UTF8; + + $body = MAPIUtils::readPropStream($mapimessage, PR_BODY); + $body = Utils::ConvertCodepageStringToUtf8($props[$sendMailProps["internetcpid"]], $body); + $mapiprops[$sendMailProps["body"]] = $body; + + $bodyHtml = MAPIUtils::readPropStream($mapimessage, PR_HTML); + $bodyHtml = Utils::ConvertCodepageStringToUtf8($props[$sendMailProps["internetcpid"]], $bodyHtml); + $mapiprops[$sendMailProps["html"]] = $bodyHtml; + + mapi_setprops($mapimessage, $mapiprops); + } + if (stripos($props[PR_MESSAGE_CLASS], "IPM.Schedule.Meeting.Resp.") === 0) { + // search for calendar items using goid + $mr = new Meetingrequest($this->store, $mapimessage); + $appointments = $mr->findCalendarItems($props[$meetingRequestProps["goidtag"]]); + if (is_array($appointments) && !empty($appointments)) { + $app = mapi_msgstore_openentry($this->store, $appointments[0]); + $appprops = mapi_getprops($app, array($meetingRequestProps["appSeqNr"])); + if (isset($appprops[$meetingRequestProps["appSeqNr"]]) && $appprops[$meetingRequestProps["appSeqNr"]]) { + $mapiprops[$meetingRequestProps["appSeqNr"]] = $appprops[$meetingRequestProps["appSeqNr"]]; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Set sequence number to:%d", $appprops[$meetingRequestProps["appSeqNr"]])); + } + } + } + + // Delete the PR_SENT_REPRESENTING_* properties because some android devices + // do not send neither From nor Sender header causing empty PR_SENT_REPRESENTING_NAME and + // PR_SENT_REPRESENTING_EMAIL_ADDRESS properties and "broken" PR_SENT_REPRESENTING_ENTRYID + // which results in spooler not being able to send the message. + // @see http://jira.zarafa.com/browse/ZP-85 + mapi_deleteprops($mapimessage, + array( $sendMailProps["sentrepresentingname"], $sendMailProps["sentrepresentingemail"], $sendMailProps["representingentryid"], + $sendMailProps["sentrepresentingaddt"], $sendMailProps["sentrepresentinsrchk"])); + + if(isset($sm->source->itemid) && $sm->source->itemid) { + // answering an email in a public/shared folder + if (!$this->Setup(ZPush::GetAdditionalSyncFolderStore($sm->source->folderid))) + throw new StatusException(sprintf("ZarafaBackend->SendMail() could not Setup() the backend for folder id '%s'", $sm->source->folderid), SYNC_COMMONSTATUS_SERVERERROR); + + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($sm->source->folderid), hex2bin($sm->source->itemid)); + if ($entryid) + $fwmessage = mapi_msgstore_openentry($this->store, $entryid); + + if(!isset($fwmessage) || !$fwmessage) + throw new StatusException(sprintf("ZarafaBackend->SendMail(): Could not open message id '%s' in folder id '%s' to be replied/forwarded: 0x%X", $sm->source->itemid, $sm->source->folderid, mapi_last_hresult()), SYNC_COMMONSTATUS_ITEMNOTFOUND); + + //update icon when forwarding or replying message + if ($sm->forwardflag) mapi_setprops($fwmessage, array(PR_ICON_INDEX=>262)); + elseif ($sm->replyflag) mapi_setprops($fwmessage, array(PR_ICON_INDEX=>261)); + mapi_savechanges($fwmessage); + + // only attach the original message if the mobile does not send it itself + if (!isset($sm->replacemime)) { + // get message's body in order to append forward or reply text + if (!isset($body)) { + $body = MAPIUtils::readPropStream($mapimessage, PR_BODY); + } + if (!isset($bodyHtml)) { + $bodyHtml = MAPIUtils::readPropStream($mapimessage, PR_HTML); + } + $cpid = mapi_getprops($fwmessage, array($sendMailProps["internetcpid"])); + if($sm->forwardflag) { + // attach the original attachments to the outgoing message + $this->copyAttachments($mapimessage, $fwmessage); + } + + // regarding the conversion @see ZP-470 + if (strlen($body) > 0) { + $fwbody = MAPIUtils::readPropStream($fwmessage, PR_BODY); + // if only the old message's cpid is set, convert from old charset to utf-8 + if (isset($cpid[$sendMailProps["internetcpid"]]) && $cpid[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->SendMail(): convert plain forwarded message charset (only fw set) from '%s' to '65001'", $cpid[$sendMailProps["internetcpid"]])); + $fwbody = Utils::ConvertCodepageStringToUtf8($cpid[$sendMailProps["internetcpid"]], $fwbody); + } + // otherwise to the general conversion + else { + ZLog::Write(LOGLEVEL_DEBUG, "ZarafaBackend->SendMail(): no charset conversion done for plain forwarded message"); + $fwbody = w2u($fwbody); + } + + $mapiprops[$sendMailProps["body"]] = $body."\r\n\r\n".$fwbody; + } + + if (strlen($bodyHtml) > 0) { + $fwbodyHtml = MAPIUtils::readPropStream($fwmessage, PR_HTML); + // if only new message's cpid is set, convert to UTF-8 + if (isset($cpid[$sendMailProps["internetcpid"]]) && $cpid[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->SendMail(): convert html forwarded message charset (only fw set) from '%s' to '65001'", $cpid[$sendMailProps["internetcpid"]])); + $fwbodyHtml = Utils::ConvertCodepageStringToUtf8($cpid[$sendMailProps["internetcpid"]], $fwbodyHtml); + } + // otherwise to the general conversion + else { + ZLog::Write(LOGLEVEL_DEBUG, "ZarafaBackend->SendMail(): no charset conversion done for html forwarded message"); + $fwbodyHtml = w2u($fwbodyHtml); + } + + $mapiprops[$sendMailProps["html"]] = $bodyHtml."

".$fwbodyHtml; + } + } + } + + mapi_setprops($mapimessage, $mapiprops); + mapi_message_savechanges($mapimessage); + mapi_message_submitmessage($mapimessage); + $hr = mapi_last_hresult(); + + if ($hr) + throw new StatusException(sprintf("ZarafaBackend->SendMail(): Error saving/submitting the message to the Outbox: 0x%X", mapi_last_hresult()), SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED); + + ZLog::Write(LOGLEVEL_DEBUG, "ZarafaBackend->SendMail(): email submitted"); + return true; + } + + /** + * Returns all available data of a single message + * + * @param string $folderid + * @param string $id + * @param ContentParameters $contentparameters flag + * + * @access public + * @return object(SyncObject) + * @throws StatusException + */ + public function Fetch($folderid, $id, $contentparameters) { + // get the entry id of the message + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($id)); + if(!$entryid) + throw new StatusException(sprintf("BackendZarafa->Fetch('%s','%s'): Error getting entryid: 0x%X", $folderid, $id, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); + + // open the message + $message = mapi_msgstore_openentry($this->store, $entryid); + if(!$message) + throw new StatusException(sprintf("BackendZarafa->Fetch('%s','%s'): Error, unable to open message: 0x%X", $folderid, $id, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); + + // convert the mapi message into a SyncObject and return it + $mapiprovider = new MAPIProvider($this->session, $this->store); + + // override truncation + $contentparameters->SetTruncation(SYNC_TRUNCATION_ALL); + // TODO check for body preferences + return $mapiprovider->GetMessage($message, $contentparameters); + } + + /** + * Returns the waste basket + * + * @access public + * @return string + */ + public function GetWasteBasket() { + if ($this->wastebasket) { + return $this->wastebasket; + } + + $storeprops = mapi_getprops($this->defaultstore, array(PR_IPM_WASTEBASKET_ENTRYID)); + if (isset($storeprops[PR_IPM_WASTEBASKET_ENTRYID])) { + $wastebasket = mapi_msgstore_openentry($this->store, $storeprops[PR_IPM_WASTEBASKET_ENTRYID]); + $wastebasketprops = mapi_getprops($wastebasket, array(PR_SOURCE_KEY)); + if (isset($wastebasketprops[PR_SOURCE_KEY])) { + $this->wastebasket = bin2hex($wastebasketprops[PR_SOURCE_KEY]); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Got waste basket with id '%s'", $this->wastebasket)); + return $this->wastebasket; + } + } + return false; + } + + /** + * Returns the content of the named attachment as stream + * + * @param string $attname + * @access public + * @return SyncItemOperationsAttachment + * @throws StatusException + */ + public function GetAttachmentData($attname) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->GetAttachmentData('%s')", $attname)); + + if(!strpos($attname, ":")) + throw new StatusException(sprintf("ZarafaBackend->GetAttachmentData('%s'): Error, attachment requested for non-existing item", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + + list($id, $attachnum) = explode(":", $attname); + + $entryid = hex2bin($id); + $message = mapi_msgstore_openentry($this->store, $entryid); + if(!$message) + throw new StatusException(sprintf("ZarafaBackend->GetAttachmentData('%s'): Error, unable to open item for attachment data for id '%s' with: 0x%X", $attname, $id, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + + $attach = mapi_message_openattach($message, $attachnum); + if(!$attach) + throw new StatusException(sprintf("ZarafaBackend->GetAttachmentData('%s'): Error, unable to open attachment number '%s' with: 0x%X", $attname, $attachnum, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + + // get necessary attachment props + $attprops = mapi_getprops($attach, array(PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W, PR_ATTACH_METHOD)); + $attachment = new SyncItemOperationsAttachment(); + // check if it's an embedded message and open it in such a case + if (isset($attprops[PR_ATTACH_METHOD]) && $attprops[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG) { + $embMessage = mapi_attach_openobj($attach); + $addrbook = $this->getAddressbook(); + $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, array('use_tnef' => -1)); + // set the default contenttype for this kind of messages + $attachment->contenttype = "message/rfc822"; + } + else + $stream = mapi_openpropertytostream($attach, PR_ATTACH_DATA_BIN); + + if(!$stream) + throw new StatusException(sprintf("ZarafaBackend->GetAttachmentData('%s'): Error, unable to open attachment data stream: 0x%X", $attname, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + + // put the mapi stream into a wrapper to get a standard stream + $attachment->data = MapiStreamWrapper::Open($stream); + if (isset($attprops[PR_ATTACH_MIME_TAG])) + $attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG]; + elseif (isset($attprops[PR_ATTACH_MIME_TAG_W])) + $attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG_W]; + //TODO default contenttype + return $attachment; + } + + + /** + * 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) { + $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid)); + if (!$folderentryid) + throw new StatusException(sprintf("BackendZarafa->EmptyFolder('%s','%s'): Error, unable to open folder (no entry id)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR); + $folder = mapi_msgstore_openentry($this->store, $folderentryid); + + if (!$folder) + throw new StatusException(sprintf("BackendZarafa->EmptyFolder('%s','%s'): Error, unable to open parent folder (open entry)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR); + + $flags = 0; + if ($includeSubfolders) + $flags = DEL_ASSOCIATED; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->EmptyFolder('%s','%s'): emptying folder",$folderid, Utils::PrintAsString($includeSubfolders))); + + // empty folder! + mapi_folder_emptyfolder($folder, $flags); + if (mapi_last_hresult()) + throw new StatusException(sprintf("BackendZarafa->EmptyFolder('%s','%s'): Error, mapi_folder_emptyfolder() failed: 0x%X", $folderid, Utils::PrintAsString($includeSubfolders), mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR); + + return true; + } + + /** + * Processes a response to a meeting request. + * CalendarID is a reference and has to be set if a new calendar item is created + * + * @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) { + // Use standard meeting response code to process meeting request + $reqentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($requestid)); + if (!$reqentryid) + throw new StatusException(sprintf("BackendZarafa->MeetingResponse('%s', '%s', '%s'): Error, unable to entryid of the message 0x%X", $requestid, $folderid, $response, mapi_last_hresult()), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); + + $mapimessage = mapi_msgstore_openentry($this->store, $reqentryid); + if(!$mapimessage) + throw new StatusException(sprintf("BackendZarafa->MeetingResponse('%s','%s', '%s'): Error, unable to open request message for response 0x%X", $requestid, $folderid, $response, mapi_last_hresult()), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); + + $meetingrequest = new Meetingrequest($this->store, $mapimessage, $this->session); + + if(!$meetingrequest->isMeetingRequest()) + throw new StatusException(sprintf("BackendZarafa->MeetingResponse('%s','%s', '%s'): Error, attempt to respond to non-meeting request", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); + + if($meetingrequest->isLocalOrganiser()) + throw new StatusException(sprintf("BackendZarafa->MeetingResponse('%s','%s', '%s'): Error, attempt to response to meeting request that we organized", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); + + // Process the meeting response. We don't have to send the actual meeting response + // e-mail, because the device will send it itself. + switch($response) { + case 1: // accept + default: + $entryid = $meetingrequest->doAccept(false, false, false, false, false, false, true); // last true is the $userAction + break; + case 2: // tentative + $entryid = $meetingrequest->doAccept(true, false, false, false, false, false, true); // last true is the $userAction + break; + case 3: // decline + $meetingrequest->doDecline(false); + break; + } + + // F/B will be updated on logoff + + // We have to return the ID of the new calendar item, so do that here + $calendarid = ""; + if (isset($entryid)) { + $newitem = mapi_msgstore_openentry($this->store, $entryid); + // new item might be in a delegator's store. ActiveSync does not support accepting them. + if (!$newitem) { + throw new StatusException(sprintf("BackendZarafa->MeetingResponse('%s','%s', '%s'): Object with entryid '%s' was not found in user's store (0x%X). It might be in a delegator's store.", $requestid, $folderid, $response, bin2hex($entryid), mapi_last_hresult()), SYNC_MEETRESPSTATUS_SERVERERROR, null, LOGLEVEL_WARN); + } + + $newprops = mapi_getprops($newitem, array(PR_SOURCE_KEY)); + $calendarid = bin2hex($newprops[PR_SOURCE_KEY]); + } + + // on recurring items, the MeetingRequest class responds with a wrong entryid + if ($requestid == $calendarid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->MeetingResponse('%s','%s', '%s'): returned calender id is the same as the requestid - re-searching", $requestid, $folderid, $response)); + + $props = MAPIMapping::GetMeetingRequestProperties(); + $props = getPropIdsFromStrings($this->store, $props); + + $messageprops = mapi_getprops($mapimessage, Array($props["goidtag"])); + $goid = $messageprops[$props["goidtag"]]; + + $items = $meetingrequest->findCalendarItems($goid); + + if (is_array($items)) { + $newitem = mapi_msgstore_openentry($this->store, $items[0]); + $newprops = mapi_getprops($newitem, array(PR_SOURCE_KEY)); + $calendarid = bin2hex($newprops[PR_SOURCE_KEY]); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->MeetingResponse('%s','%s', '%s'): found other calendar entryid", $requestid, $folderid, $response)); + } + + if ($requestid == $calendarid) + throw new StatusException(sprintf("BackendZarafa->MeetingResponse('%s','%s', '%s'): Error finding the accepted meeting response in the calendar", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); + } + + // delete meeting request from Inbox + $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid)); + $folder = mapi_msgstore_openentry($this->store, $folderentryid); + mapi_folder_deletemessages($folder, array($reqentryid), 0); + + return $calendarid; + } + + /** + * Indicates if the backend has a ChangesSink. + * A sink is an active notification mechanism which does not need polling. + * Since Zarafa 7.0.5 such a sink is available. + * The Zarafa backend uses this method to initialize the sink with mapi. + * + * @access public + * @return boolean + */ + public function HasChangesSink() { + if (!$this->notifications) { + ZLog::Write(LOGLEVEL_DEBUG, "ZarafaBackend->HasChangesSink(): sink is not available"); + return false; + } + + $this->changesSink = @mapi_sink_create(); + + if (! $this->changesSink || mapi_last_hresult()) { + ZLog::Write(LOGLEVEL_WARN, sprintf("ZarafaBackend->HasChangesSink(): sink could not be created with 0x%X", mapi_last_hresult())); + return false; + } + + ZLog::Write(LOGLEVEL_DEBUG, "ZarafaBackend->HasChangesSink(): created"); + + // advise the main store and also to check if the connection supports it + return $this->adviseStoreToSink($this->defaultstore); + } + + /** + * 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 entryid can not be found for that folder + */ + public function ChangesSinkInitialize($folderid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->ChangesSinkInitialize(): folderid '%s'", $folderid)); + + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid)); + if (!$entryid) + return false; + + // add entryid to the monitored folders + $this->changesSinkFolders[$entryid] = $folderid; + + // advise the current store to the sink + return $this->adviseStoreToSink($this->store); + } + + /** + * 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(); + $sinkresult = @mapi_sink_timedwait($this->changesSink, $timeout * 1000); + foreach ($sinkresult as $sinknotif) { + // check if something in the monitored folders changed + if (isset($sinknotif['parentid']) && array_key_exists($sinknotif['parentid'], $this->changesSinkFolders)) { + $notifications[] = $this->changesSinkFolders[$sinknotif['parentid']]; + } + // deletes and moves + if (isset($sinknotif['oldparentid']) && array_key_exists($sinknotif['oldparentid'], $this->changesSinkFolders)) { + $notifications[] = $this->changesSinkFolders[$sinknotif['oldparentid']]; + } + } + return $notifications; + } + + /** + * Applies settings to and gets informations from the device + * + * @param SyncObject $settings (SyncOOF or SyncUserInformation possible) + * + * @access public + * @return SyncObject $settings + */ + public function Settings($settings) { + if ($settings instanceof SyncOOF) { + $this->settingsOOF($settings); + } + + if ($settings instanceof SyncUserInformation) { + $this->settingsUserInformation($settings); + } + + return $settings; + } + + /** + * Resolves recipients + * + * @param SyncObject $resolveRecipients + * + * @access public + * @return SyncObject $resolveRecipients + */ + public function ResolveRecipients($resolveRecipients) { + if ($resolveRecipients instanceof SyncResolveRecipients) { + $resolveRecipients->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS; + $resolveRecipients->recipient = array(); + foreach ($resolveRecipients->to as $i => $to) { + $recipient = $this->resolveRecipient($to); + if ($recipient instanceof SyncResolveRecipient) { + $resolveRecipients->recipient[$i] = $recipient; + } + elseif (is_int($recipient)) { + $resolveRecipients->status = $recipient; + } + } + + return $resolveRecipients; + } + ZLog::Write(LOGLEVEL_WARN, "Not a valid SyncResolveRecipients object."); + // return a SyncResolveRecipients object so that sync doesn't fail + $r = new SyncResolveRecipients(); + $r->status = SYNC_RESOLVERECIPSSTATUS_PROTOCOLERROR; + $r->recipient = array(); + return $r; + } + + + /**---------------------------------------------------------------------------------------------------------- + * Implementation of the ISearchProvider interface + */ + + /** + * 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) { + return ($searchtype == ISearchProvider::SEARCH_GAL) || ($searchtype == ISearchProvider::SEARCH_MAILBOX); + } + + /** + * Searches the GAB of Zarafa + * Can be overwitten globally by configuring a SearchBackend + * + * @param string $searchquery + * @param string $searchrange + * + * @access public + * @return array + * @throws StatusException + */ + public function GetGALSearchResults($searchquery, $searchrange){ + // only return users from who the displayName or the username starts with $name + //TODO: use PR_ANR for this restriction instead of PR_DISPLAY_NAME and PR_ACCOUNT + $addrbook = mapi_openaddressbook($this->session); + if ($addrbook) + $ab_entryid = mapi_ab_getdefaultdir($addrbook); + if ($ab_entryid) + $ab_dir = mapi_ab_openentry($addrbook, $ab_entryid); + if ($ab_dir) + $table = mapi_folder_getcontentstable($ab_dir); + + if (!$table) + throw new StatusException(sprintf("ZarafaBackend->GetGALSearchResults(): could not open addressbook: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_CONNECTIONFAILED); + + $restriction = MAPIUtils::GetSearchRestriction(u2w($searchquery)); + mapi_table_restrict($table, $restriction); + mapi_table_sort($table, array(PR_DISPLAY_NAME => TABLE_SORT_ASCEND)); + + if (mapi_last_hresult()) + throw new StatusException(sprintf("ZarafaBackend->GetGALSearchResults(): could not apply restriction: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_TOOCOMPLEX); + + //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(); + + $querycnt = mapi_table_getrowcount($table); + //do not return more results as requested in range + $querylimit = (($rangeend + 1) < $querycnt) ? ($rangeend + 1) : $querycnt; + $items['range'] = ($querylimit > 0) ? $rangestart.'-'.($querylimit - 1) : '0-0'; + $items['searchtotal'] = $querycnt; + if ($querycnt > 0) + $abentries = mapi_table_queryrows($table, array(PR_ACCOUNT, PR_DISPLAY_NAME, PR_SMTP_ADDRESS, PR_BUSINESS_TELEPHONE_NUMBER, PR_GIVEN_NAME, PR_SURNAME, PR_MOBILE_TELEPHONE_NUMBER, PR_HOME_TELEPHONE_NUMBER, PR_TITLE, PR_COMPANY_NAME, PR_OFFICE_LOCATION), $rangestart, $querylimit); + + for ($i = 0; $i < $querylimit; $i++) { + $items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_DISPLAY_NAME]); + + if (strlen(trim($items[$i][SYNC_GAL_DISPLAYNAME])) == 0) + $items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_ACCOUNT]); + + $items[$i][SYNC_GAL_ALIAS] = w2u($abentries[$i][PR_ACCOUNT]); + //it's not possible not get first and last name of an user + //from the gab and user functions, so we just set lastname + //to displayname and leave firstname unset + //this was changed in Zarafa 6.40, so we try to get first and + //last name and fall back to the old behaviour if these values are not set + if (isset($abentries[$i][PR_GIVEN_NAME])) + $items[$i][SYNC_GAL_FIRSTNAME] = w2u($abentries[$i][PR_GIVEN_NAME]); + if (isset($abentries[$i][PR_SURNAME])) + $items[$i][SYNC_GAL_LASTNAME] = w2u($abentries[$i][PR_SURNAME]); + + if (!isset($items[$i][SYNC_GAL_LASTNAME])) $items[$i][SYNC_GAL_LASTNAME] = $items[$i][SYNC_GAL_DISPLAYNAME]; + + $items[$i][SYNC_GAL_EMAILADDRESS] = w2u($abentries[$i][PR_SMTP_ADDRESS]); + //check if an user has an office number or it might produce warnings in the log + if (isset($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER])) + $items[$i][SYNC_GAL_PHONE] = w2u($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER]); + //check if an user has a mobile number or it might produce warnings in the log + if (isset($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER])) + $items[$i][SYNC_GAL_MOBILEPHONE] = w2u($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER]); + //check if an user has a home number or it might produce warnings in the log + if (isset($abentries[$i][PR_HOME_TELEPHONE_NUMBER])) + $items[$i][SYNC_GAL_HOMEPHONE] = w2u($abentries[$i][PR_HOME_TELEPHONE_NUMBER]); + + if (isset($abentries[$i][PR_COMPANY_NAME])) + $items[$i][SYNC_GAL_COMPANY] = w2u($abentries[$i][PR_COMPANY_NAME]); + + if (isset($abentries[$i][PR_TITLE])) + $items[$i][SYNC_GAL_TITLE] = w2u($abentries[$i][PR_TITLE]); + + if (isset($abentries[$i][PR_OFFICE_LOCATION])) + $items[$i][SYNC_GAL_OFFICE] = w2u($abentries[$i][PR_OFFICE_LOCATION]); + } + return $items; + } + + /** + * Searches for the emails on the server + * + * @param ContentParameter $cpo + * + * @return array + */ + public function GetMailboxSearchResults($cpo) { + $searchFolder = $this->getSearchFolder(); + $searchRestriction = $this->getSearchRestriction($cpo); + $searchRange = explode('-', $cpo->GetSearchRange()); + $searchFolderId = $cpo->GetSearchFolderid(); + $searchFolders = array(); + // search only in required folders + if (!empty($searchFolderId)) { + $searchFolderEntryId = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($searchFolderId)); + $searchFolders[] = $searchFolderEntryId; + } + // if no folder was required then search in the entire store + else { + $tmp = mapi_getprops($this->store, array(PR_ENTRYID,PR_DISPLAY_NAME,PR_IPM_SUBTREE_ENTRYID)); + $searchFolders[] = $tmp[PR_IPM_SUBTREE_ENTRYID]; + } + $items = array(); + $flags = 0; + // if subfolders are required, do a recursive search + if ($cpo->GetSearchDeepTraversal()) { + $flags |= SEARCH_RECURSIVE; + } + + mapi_folder_setsearchcriteria($searchFolder, $searchRestriction, $searchFolders, $flags); + + $table = mapi_folder_getcontentstable($searchFolder); + $searchStart = time(); + // do the search and wait for all the results available + while (time() - $searchStart < SEARCH_WAIT) { + $searchcriteria = mapi_folder_getsearchcriteria($searchFolder); + if(($searchcriteria["searchstate"] & SEARCH_REBUILD) == 0) + break; // Search is done + sleep(1); + } + + // if the search range is set limit the result to it, otherwise return all found messages + $rows = (is_array($searchRange) && isset($searchRange[0]) && isset($searchRange[1])) ? + mapi_table_queryrows($table, array(PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY), $searchRange[0], $searchRange[1] - $searchRange[0] + 1) : + mapi_table_queryrows($table, array(PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY), 0, SEARCH_MAXRESULTS); + + $cnt = count($rows); + $items['searchtotal'] = $cnt; + $items["range"] = $cpo->GetSearchRange(); + for ($i = 0; $i < $cnt; $i++) { + $items[$i]['class'] = 'Email'; + $items[$i]['longid'] = bin2hex($rows[$i][PR_PARENT_SOURCE_KEY]) . ":" . bin2hex($rows[$i][PR_SOURCE_KEY]); + $items[$i]['folderid'] = bin2hex($rows[$i][PR_PARENT_SOURCE_KEY]); + } + return $items; + } + + /** + * Terminates a search for a given PID + * + * @param int $pid + * + * @return boolean + */ + public function TerminateSearch($pid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->TerminateSearch(): terminating search for pid %d", $pid)); + $storeProps = mapi_getprops($this->store, array(PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID)); + if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) { + ZLog::Write(LOGLEVEL_WARN, "Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder"); + return false; + } + + $finderfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]); + if(mapi_last_hresult() != NOERROR) { + ZLog::Write(LOGLEVEL_WARN, sprintf("Unable to open search folder (0x%X)", mapi_last_hresult())); + return false; + } + + $hierarchytable = mapi_folder_gethierarchytable($finderfolder); + mapi_table_restrict($hierarchytable, + array(RES_CONTENT, + array( + FUZZYLEVEL => FL_PREFIX, + ULPROPTAG => PR_DISPLAY_NAME, + VALUE => array(PR_DISPLAY_NAME=>"Z-Push Search Folder ".$pid) + ) + ), + TBL_BATCH); + + $folders = mapi_table_queryallrows($hierarchytable, array(PR_ENTRYID, PR_DISPLAY_NAME, PR_LAST_MODIFICATION_TIME)); + foreach($folders as $folder) { + mapi_folder_deletefolder($finderfolder, $folder[PR_ENTRYID]); + } + return true; + } + + /** + * Disconnects from the current search provider + * + * @access public + * @return boolean + */ + public function Disconnect() { + return true; + } + + /** + * Returns the MAPI store ressource for a folderid + * This is not part of IBackend but necessary for the ImportChangesICS->MoveMessage() operation if + * the destination folder is not in the default store + * Note: The current backend store might be changed as IBackend->Setup() is executed + * + * @param string $store target store, could contain a "domain\user" value - if emtpy default store is returned + * @param string $folderid + * + * @access public + * @return Ressource/boolean + */ + public function GetMAPIStoreForFolderId($store, $folderid) { + if ($store == false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->GetMAPIStoreForFolderId('%s', '%s'): no store specified, returning default store", $store, $folderid)); + return $this->defaultstore; + } + + // setup the correct store + if ($this->Setup($store, false, $folderid)) { + return $this->store; + } + else { + ZLog::Write(LOGLEVEL_WARN, sprintf("ZarafaBackend->GetMAPIStoreForFolderId('%s', '%s'): store is not available", $store, $folderid)); + return false; + } + } + + + /**---------------------------------------------------------------------------------------------------------- + * Private methods + */ + + /** + * Advises a store to the changes sink + * + * @param mapistore $store store to be advised + * + * @access private + * @return boolean + */ + private function adviseStoreToSink($store) { + // check if we already advised the store + if (!in_array($store, $this->changesSinkStores)) { + mapi_msgstore_advise($this->store, null, fnevObjectModified | fnevObjectCreated | fnevObjectMoved | fnevObjectDeleted, $this->changesSink); + $this->changesSinkStores[] = $store; + + if (mapi_last_hresult()) { + ZLog::Write(LOGLEVEL_WARN, sprintf("ZarafaBackend->adviseStoreToSink(): failed to advised store '%s' with code 0x%X. Polling will be performed.", $this->store, mapi_last_hresult())); + return false; + } + else + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->adviseStoreToSink(): advised store '%s'", $this->store)); + } + return true; + } + + /** + * Open the store marked with PR_DEFAULT_STORE = TRUE + * if $return_public is set, the public store is opened + * + * @param string $user User which store should be opened + * + * @access public + * @return boolean + */ + private function openMessageStore($user) { + // During PING requests the operations store has to be switched constantly + // the cache prevents the same store opened several times + if (isset($this->storeCache[$user])) + return $this->storeCache[$user]; + + $entryid = false; + $return_public = false; + + if (strtoupper($user) == 'SYSTEM') + $return_public = true; + + // loop through the storestable if authenticated user of public folder + if ($user == $this->mainUser || $return_public === true) { + // Find the default store + $storestables = mapi_getmsgstorestable($this->session); + $result = mapi_last_hresult(); + + if ($result == NOERROR){ + $rows = mapi_table_queryallrows($storestables, array(PR_ENTRYID, PR_DEFAULT_STORE, PR_MDB_PROVIDER)); + + foreach($rows as $row) { + if(!$return_public && isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE] == true) { + $entryid = $row[PR_ENTRYID]; + break; + } + if ($return_public && isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) { + $entryid = $row[PR_ENTRYID]; + break; + } + } + } + } + else + $entryid = @mapi_msgstore_createentryid($this->defaultstore, $user); + + if($entryid) { + $store = @mapi_openmsgstore($this->session, $entryid); + + if (!$store) { + ZLog::Write(LOGLEVEL_WARN, sprintf("ZarafaBackend->openMessageStore('%s'): Could not open store", $user)); + return false; + } + + // add this store to the cache + if (!isset($this->storeCache[$user])) + $this->storeCache[$user] = $store; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->openMessageStore('%s'): Found '%s' store: '%s'", $user, (($return_public)?'PUBLIC':'DEFAULT'),$store)); + return $store; + } + else { + ZLog::Write(LOGLEVEL_WARN, sprintf("ZarafaBackend->openMessageStore('%s'): No store found for this user", $user)); + return false; + } + } + + private function hasSecretaryACLs($store, $folderid) { + $entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($folderid)); + if (!$entryid) return false; + + $folder = mapi_msgstore_openentry($store, $entryid); + if (!$folder) return false; + + $props = mapi_getprops($folder, array(PR_RIGHTS)); + if (isset($props[PR_RIGHTS]) && + ($props[PR_RIGHTS] & ecRightsReadAny) && + ($props[PR_RIGHTS] & ecRightsCreate) && + ($props[PR_RIGHTS] & ecRightsEditOwned) && + ($props[PR_RIGHTS] & ecRightsDeleteOwned) && + ($props[PR_RIGHTS] & ecRightsEditAny) && + ($props[PR_RIGHTS] & ecRightsDeleteAny) && + ($props[PR_RIGHTS] & ecRightsFolderVisible) ) { + return true; + } + return false; + } + + /** + * The meta function for out of office settings. + * + * @param SyncObject $oof + * + * @access private + * @return void + */ + private function settingsOOF(&$oof) { + //if oof state is set it must be set of oof and get otherwise + if (isset($oof->oofstate)) { + $this->settingsOOFSEt($oof); + } + else { + $this->settingsOOFGEt($oof); + } + } + + /** + * Gets the out of office settings + * + * @param SyncObject $oof + * + * @access private + * @return void + */ + private function settingsOOFGEt(&$oof) { + $oofprops = mapi_getprops($this->defaultstore, array(PR_EC_OUTOFOFFICE, PR_EC_OUTOFOFFICE_MSG, PR_EC_OUTOFOFFICE_SUBJECT)); + $oof->oofstate = SYNC_SETTINGSOOF_DISABLED; + $oof->Status = SYNC_SETTINGSSTATUS_SUCCESS; + if ($oofprops != false) { + $oof->oofstate = isset($oofprops[PR_EC_OUTOFOFFICE]) ? ($oofprops[PR_EC_OUTOFOFFICE] ? SYNC_SETTINGSOOF_GLOBAL : SYNC_SETTINGSOOF_DISABLED) : SYNC_SETTINGSOOF_DISABLED; + //TODO external and external unknown + $oofmessage = new SyncOOFMessage(); + $oofmessage->appliesToInternal = ""; + $oofmessage->enabled = $oof->oofstate; + $oofmessage->replymessage = (isset($oofprops[PR_EC_OUTOFOFFICE_MSG])) ? w2u($oofprops[PR_EC_OUTOFOFFICE_MSG]) : ""; + $oofmessage->bodytype = $oof->bodytype; + unset($oofmessage->appliesToExternal, $oofmessage->appliesToExternalUnknown); + $oof->oofmessage[] = $oofmessage; + } + else { + ZLog::Write(LOGLEVEL_WARN, "Unable to get out of office information"); + } + + //unset body type for oof in order not to stream it + unset($oof->bodytype); + } + + /** + * Sets the out of office settings. + * + * @param SyncObject $oof + * + * @access private + * @return void + */ + private function settingsOOFSEt(&$oof) { + $oof->Status = SYNC_SETTINGSSTATUS_SUCCESS; + $props = array(); + if ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL || $oof->oofstate == SYNC_SETTINGSOOF_TIMEBASED) { + $props[PR_EC_OUTOFOFFICE] = true; + foreach ($oof->oofmessage as $oofmessage) { + if (isset($oofmessage->appliesToInternal)) { + $props[PR_EC_OUTOFOFFICE_MSG] = isset($oofmessage->replymessage) ? u2w($oofmessage->replymessage) : ""; + $props[PR_EC_OUTOFOFFICE_SUBJECT] = "Out of office"; + } + } + } + elseif($oof->oofstate == SYNC_SETTINGSOOF_DISABLED) { + $props[PR_EC_OUTOFOFFICE] = false; + } + + if (!empty($props)) { + @mapi_setprops($this->defaultstore, $props); + $result = mapi_last_hresult(); + if ($result != NOERROR) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("Setting oof information failed (%X)", $result)); + return false; + } + } + + return true; + } + + /** + * Gets the user's email address from server + * + * @param SyncObject $userinformation + * + * @access private + * @return void + */ + private function settingsUserInformation(&$userinformation) { + if (!isset($this->defaultstore) || !isset($this->mainUser)) { + ZLog::Write(LOGLEVEL_ERROR, "The store or user are not available for getting user information"); + return false; + } + $user = mapi_zarafa_getuser($this->defaultstore, $this->mainUser); + if ($user != false) { + $userinformation->Status = SYNC_SETTINGSSTATUS_USERINFO_SUCCESS; + $userinformation->emailaddresses[] = $user["emailaddress"]; + return true; + } + ZLog::Write(LOGLEVEL_ERROR, sprintf("Getting user information failed: mapi_zarafa_getuser(%X)", mapi_last_hresult())); + return false; + } + + /** + * Sets the importance and priority of a message from a RFC822 message headers. + * + * @param int $xPriority + * @param array $mapiprops + * + * @return void + */ + private function getImportanceAndPriority($xPriority, &$mapiprops, $sendMailProps) { + switch($xPriority) { + case 1: + case 2: + $priority = PRIO_URGENT; + $importance = IMPORTANCE_HIGH; + break; + case 4: + case 5: + $priority = PRIO_NONURGENT; + $importance = IMPORTANCE_LOW; + break; + case 3: + default: + $priority = PRIO_NORMAL; + $importance = IMPORTANCE_NORMAL; + break; + } + $mapiprops[$sendMailProps["importance"]] = $importance; + $mapiprops[$sendMailProps["priority"]] = $priority; + } + + /** + * Copies attachments from one message to another. + * + * @param MAPIMessage $toMessage + * @param MAPIMessage $fromMessage + * + * @return void + */ + private function copyAttachments(&$toMessage, $fromMessage) { + $attachtable = mapi_message_getattachmenttable($fromMessage); + $rows = mapi_table_queryallrows($attachtable, array(PR_ATTACH_NUM)); + + foreach($rows as $row) { + if(isset($row[PR_ATTACH_NUM])) { + $attach = mapi_message_openattach($fromMessage, $row[PR_ATTACH_NUM]); + $newattach = mapi_message_createattach($toMessage); + mapi_copyto($attach, array(), array(), $newattach, 0); + mapi_savechanges($newattach); + } + } + } + + /** + * Function will create a search folder in FINDER_ROOT folder + * if folder exists then it will open it + * + * @see createSearchFolder($store, $openIfExists = true) function in the webaccess + * + * @return mapiFolderObject $folder created search folder + */ + private function getSearchFolder() { + // create new or open existing search folder + $searchFolderRoot = $this->getSearchFoldersRoot($this->store); + if($searchFolderRoot === false) { + // error in finding search root folder + // or store doesn't support search folders + return false; + } + + $searchFolder = $this->createSearchFolder($searchFolderRoot); + + if($searchFolder !== false && mapi_last_hresult() == NOERROR) { + return $searchFolder; + } + return false; + } + + /** + * Function will open FINDER_ROOT folder in root container + * public folder's don't have FINDER_ROOT folder + * + * @see getSearchFoldersRoot($store) function in the webaccess + * + * @return mapiFolderObject root folder for search folders + */ + private function getSearchFoldersRoot() { + // check if we can create search folders + $storeProps = mapi_getprops($this->store, array(PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID)); + if(($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) { + ZLog::Write(LOGLEVEL_WARN, "Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder"); + return false; + } + + // open search folders root + $searchRootFolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]); + if(mapi_last_hresult() != NOERROR) { + ZLog::Write(LOGLEVEL_WARN, sprintf("Unable to open search folder (0x%X)", mapi_last_hresult())); + return false; + } + + return $searchRootFolder; + } + + + /** + * Creates a search folder if it not exists or opens an existing one + * and returns it. + * + * @param mapiFolderObject $searchFolderRoot + * + * @return mapiFolderObject + */ + private function createSearchFolder($searchFolderRoot) { + $folderName = "Z-Push Search Folder ".@getmypid(); + $searchFolders = mapi_folder_gethierarchytable($searchFolderRoot); + $restriction = array( + RES_CONTENT, + array( + FUZZYLEVEL => FL_PREFIX, + ULPROPTAG => PR_DISPLAY_NAME, + VALUE => array(PR_DISPLAY_NAME=>$folderName) + ) + ); + //restrict the hierarchy to the z-push search folder only + mapi_table_restrict($searchFolders, $restriction); + if (mapi_table_getrowcount($searchFolders)) { + $searchFolder = mapi_table_queryrows($searchFolders, array(PR_ENTRYID), 0, 1); + + return mapi_msgstore_openentry($this->store, $searchFolder[0][PR_ENTRYID]); + } + return mapi_folder_createfolder($searchFolderRoot, $folderName, null, 0, FOLDER_SEARCH); + } + + /** + * Creates a search restriction + * + * @param ContentParameter $cpo + * @return array + */ + private function getSearchRestriction($cpo) { + $searchText = $cpo->GetSearchFreeText(); + + $searchGreater = strtotime($cpo->GetSearchValueGreater()); + $searchLess = strtotime($cpo->GetSearchValueLess()); + + // split the search on whitespache and look for every word + $searchText = preg_split("/\W+/", $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) { + $resOr = array(); + + foreach($searchProps as $property) { + array_push($resOr, + array(RES_CONTENT, + array( + FUZZYLEVEL => FL_SUBSTRING|FL_IGNORECASE, + ULPROPTAG => $property, + VALUE => u2w($term) + ) + ) + ); + } + array_push($resAnd, array(RES_OR, $resOr)); + } + + // add time range restrictions + if ($searchGreater) { + array_push($resAnd, array(RES_PROPERTY, array(RELOP => RELOP_GE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => array(PR_MESSAGE_DELIVERY_TIME => $searchGreater)))); // RES_AND; + } + if ($searchLess) { + array_push($resAnd, array(RES_PROPERTY, array(RELOP => RELOP_LE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => array(PR_MESSAGE_DELIVERY_TIME => $searchLess)))); + } + $mapiquery = array(RES_AND, $resAnd); + + return $mapiquery; + } + + /** + * Resolve recipient based on his email address. + * + * @param string $to + * + * @return SyncResolveRecipient|boolean + */ + private function resolveRecipient($to) { + $recipient = $this->resolveRecipientGAL($to); + + if ($recipient !== false) { + return $recipient; + } + + $recipient = $this->resolveRecipientContact($to); + + if ($recipient !== false) { + return $recipient; + } + + return false; + } + + /** + * Resolves recipient from the GAL and gets his certificates. + * + * @param string $to + * @return SyncResolveRecipient|boolean + */ + private function resolveRecipientGAL($to) { + $addrbook = $this->getAddressbook(); + $ab_entryid = mapi_ab_getdefaultdir($addrbook); + if ($ab_entryid) + $ab_dir = mapi_ab_openentry($addrbook, $ab_entryid); + if ($ab_dir) + $table = mapi_folder_getcontentstable($ab_dir); + + // if (!$table) + // throw new StatusException(sprintf("ZarafaBackend->resolveRecipient(): could not open addressbook: 0x%X", mapi_last_hresult()), SYNC_RESOLVERECIPSSTATUS_RESPONSE_UNRESOLVEDRECIP); + + if (!$table) { + ZLog::Write(LOGLEVEL_WARN, sprintf("Unable to open addressbook:0x%X", mapi_last_hresult())); + return false; + } + + $restriction = MAPIUtils::GetSearchRestriction(u2w($to)); + mapi_table_restrict($table, $restriction); + + $querycnt = mapi_table_getrowcount($table); + if ($querycnt > 0) { + $abentries = mapi_table_queryrows($table, array(PR_DISPLAY_NAME, PR_EMS_AB_TAGGED_X509_CERT), 0, 1); + $certificates = + // check if there are any certificates available + (isset($abentries[0][PR_EMS_AB_TAGGED_X509_CERT]) && is_array($abentries[0][PR_EMS_AB_TAGGED_X509_CERT]) && count($abentries[0][PR_EMS_AB_TAGGED_X509_CERT])) ? + $this->getCertificates($abentries[0][PR_EMS_AB_TAGGED_X509_CERT], $querycnt) : false; + if ($certificates === false) { + // the recipient does not have a valid certificate, set the appropriate status + ZLog::Write(LOGLEVEL_INFO, sprintf("No certificates found for '%s'", $to)); + $certificates = $this->getCertificates(false); + } + $recipient = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, w2u($abentries[0][PR_DISPLAY_NAME]), $to, $certificates); + return $recipient; + } + else { + ZLog::Write(LOGLEVEL_WARN, sprintf("No recipient found for: '%s'", $to)); + return SYNC_RESOLVERECIPSSTATUS_RESPONSE_UNRESOLVEDRECIP; + } + return false; + } + + /** + * Resolves recipient from the contact list and gets his certificates. + * + * @param string $to + * + * @return SyncResolveRecipient|boolean + */ + private function resolveRecipientContact($to) { + // go through all contact folders of the user and + // check if there's a contact with the given email address + $root = mapi_msgstore_openentry($this->defaultstore); + if (!$root) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("Unable to open default store: 0x%X", mapi_last_hresult)); + } + $rootprops = mapi_getprops($root, array(PR_IPM_CONTACT_ENTRYID)); + $contacts = $this->getContactsFromFolder($this->defaultstore, $rootprops[PR_IPM_CONTACT_ENTRYID], $to); + $recipients = array(); + + if ($contacts !== false) { + // create resolve recipient object + foreach ($contacts as $contact) { + $certificates = + // check if there are any certificates available + (isset($contact[PR_USER_X509_CERTIFICATE]) && is_array($contact[PR_USER_X509_CERTIFICATE]) && count($contact[PR_USER_X509_CERTIFICATE])) ? + $this->getCertificates($contact[PR_USER_X509_CERTIFICATE], 1) : false; + + if ($certificates !== false) { + return $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, u2w($contact[PR_DISPLAY_NAME]), $to, $certificates); + } + } + } + + $contactfolder = mapi_msgstore_openentry($this->defaultstore, $rootprops[PR_IPM_CONTACT_ENTRYID]); + $subfolders = MAPIUtils::GetSubfoldersForType($contactfolder, "IPF.Contact"); + foreach($subfolders as $folder) { + $contacts = $this->getContactsFromFolder($this->defaultstore, $folder[PR_ENTRYID], $to); + if ($contacts !== false) { + foreach ($contacts as $contact) { + $certificates = + // check if there are any certificates available + (isset($contact[PR_USER_X509_CERTIFICATE]) && is_array($contact[PR_USER_X509_CERTIFICATE]) && count($contact[PR_USER_X509_CERTIFICATE])) ? + $this->getCertificates($contact[PR_USER_X509_CERTIFICATE], 1) : false; + + if ($certificates !== false) { + return $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, u2w($contact[PR_DISPLAY_NAME]), $to, $certificates); + } + } + } + } + + // search contacts in public folders + $storestables = mapi_getmsgstorestable($this->session); + $result = mapi_last_hresult(); + + if ($result == NOERROR){ + $rows = mapi_table_queryallrows($storestables, array(PR_ENTRYID, PR_DEFAULT_STORE, PR_MDB_PROVIDER)); + foreach($rows as $row) { + if (isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) { + // TODO refactor public store + $publicstore = mapi_openmsgstore($this->session, $row[PR_ENTRYID]); + $publicfolder = mapi_msgstore_openentry($publicstore); + + $subfolders = MAPIUtils::GetSubfoldersForType($publicfolder, "IPF.Contact"); + if ($subfolders !== false) { + foreach($subfolders as $folder) { + $contacts = $this->getContactsFromFolder($publicstore, $folder[PR_ENTRYID], $to); + if ($contacts !== false) { + foreach ($contacts as $contact) { + $certificates = + // check if there are any certificates available + (isset($contact[PR_USER_X509_CERTIFICATE]) && is_array($contact[PR_USER_X509_CERTIFICATE]) && count($contact[PR_USER_X509_CERTIFICATE])) ? + $this->getCertificates($contact[PR_USER_X509_CERTIFICATE], 1) : false; + + if ($certificates !== false) { + return $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, u2w($contact[PR_DISPLAY_NAME]), $to, $certificates); + } + } + } + } + } + break; + } + } + } + else { + ZLog::Write(LOGLEVEL_WARN, sprintf("Unable to open public store: 0x%X", $result)); + } + + $certificates = $this->getCertificates(false); + return $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $to, $certificates); + } + + /** + * Creates SyncRRCertificates object for ResolveRecipients + * + * @param binary $certificates + * @param int $recipientCount + * + * @return SyncRRCertificates + */ + private function getCertificates($certificates, $recipientCount = 0) { + $cert = new SyncRRCertificates(); + if ($certificates === false) { + $cert->status = SYNC_RESOLVERECIPSSTATUS_CERTIFICATES_NOVALIDCERT; + return $cert; + } + $cert->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS; + $cert->certificatecount = count ($certificates); + $cert->recipientcount = $recipientCount; + $cert->certificate = array(); + foreach ($certificates as $certificate) { + $cert->certificate[] = base64_encode($certificate); + } + return $cert; + } + + /** + * + * @param int $type + * @param string $displayname + * @param string $email + * @param array $certificates + * + * @return SyncResolveRecipient + */ + private function createResolveRecipient($type, $displayname, $email, $certificates) { + $recipient = new SyncResolveRecipient(); + $recipient->type = $type; + $recipient->displayname = $displayname; + $recipient->emailaddress = $email; + $recipient->certificates = $certificates; + if ($recipient->certificates === false) { + // the recipient does not have a valid certificate, set the appropriate status + ZLog::Write(LOGLEVEL_INFO, sprintf("No certificates found for '%s'", $email)); + $cert = new SyncRRCertificates(); + $cert->status = SYNC_RESOLVERECIPSSTATUS_CERTIFICATES_NOVALIDCERT; + $recipient->certificates = $cert; + } + return $recipient; + } + + /** + * Returns contacts matching given email address from a folder. + * + * @param MAPIStore $store + * @param binary $folderEntryid + * @param string $email + * + * @return array|boolean + */ + private function getContactsFromFolder($store, $folderEntryid, $email) { + $folder = mapi_msgstore_openentry($store, $folderEntryid); + $folderContent = mapi_folder_getcontentstable($folder); + mapi_table_restrict($folderContent, MAPIUtils::GetEmailAddressRestriction($store, $email)); + // TODO max limit + if (mapi_table_getrowcount($folderContent) > 0) { + return mapi_table_queryallrows($folderContent, array(PR_DISPLAY_NAME, PR_USER_X509_CERTIFICATE)); + } + return false; + } + + /** + * Get MAPI addressbook object + * + * @access private + * @return MAPIAddressbook object to be used with mapi_ab_* or false on failure + */ + private function getAddressbook() { + if (isset($this->addressbook) && $this->addressbook) { + return $this->addressbook; + } + $this->addressbook = mapi_openaddressbook($this->session); + $result = mapi_last_hresult(); + if ($result && $this->addressbook === false) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("MAPIProvider->getAddressbook error opening addressbook 0x%X", $result)); + return false; + } + return $this->addressbook; + } +} + +/** + * DEPRECATED legacy class + */ +class BackendICS extends BackendZarafa {} + +?> \ No newline at end of file diff --git a/sources/config.php b/sources/config.php new file mode 100644 index 0000000..6f61c89 --- /dev/null +++ b/sources/config.php @@ -0,0 +1,269 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +/********************************************************************************** + * Default settings + */ + // Defines the default time zone, change e.g. to "Europe/London" if necessary + define('TIMEZONE', ''); + + // Defines the base path on the server + define('BASE_PATH', dirname($_SERVER['SCRIPT_FILENAME']). '/'); + + // Try to set unlimited timeout + define('SCRIPT_TIMEOUT', 0); + + // When accessing through a proxy, the "X-Forwarded-For" header contains the original remote IP + define('USE_X_FORWARDED_FOR_HEADER', false); + + // When using client certificates, we can check if the login sent matches the owner of the certificate. + // This setting specifies the owner parameter in the certificate to look at. + define("CERTIFICATE_OWNER_PARAMETER", "SSL_CLIENT_S_DN_CN"); + +/********************************************************************************** + * Default FileStateMachine settings + */ + define('STATE_DIR', '/var/lib/z-push/'); + + +/********************************************************************************** + * 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 . 'z-push.log'); + define('LOGERRORFILE', LOGFILEDIR . 'z-push-error.log'); + define('LOGLEVEL', LOGLEVEL_INFO); + define('LOGAUTHFAIL', false); + + + // To save e.g. WBXML data only for selected users, add the usernames to the array + // The data will be saved into a dedicated file per user in the LOGFILEDIR + // Users have to be encapusulated in quotes, several users are comma separated, like: + // $specialLogUsers = array('info@domain.com', 'myusername'); + define('LOGUSERLEVEL', LOGLEVEL_DEVICEID); + $specialLogUsers = array(); + + // Location of the trusted CA, e.g. '/etc/ssl/certs/EmailCA.pem' + // Uncomment and modify the following line if the validation of the certificates fails. + // define('CAINFO', '/etc/ssl/certs/EmailCA.pem'); + +/********************************************************************************** + * Mobile settings + */ + // Device Provisioning + define('PROVISIONING', true); + + // This option allows the 'loose enforcement' of the provisioning policies for older + // devices which don't support provisioning (like WM 5 and HTC Android Mail) - dw2412 contribution + // false (default) - Enforce provisioning for all devices + // true - allow older devices, but enforce policies on devices which support it + define('LOOSE_PROVISIONING', false); + + // Default conflict preference + // Some devices allow to set if the server or PIM (mobile) + // should win in case of a synchronization conflict + // SYNC_CONFLICT_OVERWRITE_SERVER - Server is overwritten, PIM wins + // SYNC_CONFLICT_OVERWRITE_PIM - PIM is overwritten, Server wins (default) + define('SYNC_CONFLICT_DEFAULT', SYNC_CONFLICT_OVERWRITE_PIM); + + // Global limitation of items to be synchronized + // The mobile can define a sync back period for calendar and email items + // For large stores with many items the time period could be limited to a max value + // If the mobile transmits a wider time period, the defined max value is used + // Applicable values: + // SYNC_FILTERTYPE_ALL (default, no limitation) + // SYNC_FILTERTYPE_1DAY, SYNC_FILTERTYPE_3DAYS, SYNC_FILTERTYPE_1WEEK, SYNC_FILTERTYPE_2WEEKS, + // SYNC_FILTERTYPE_1MONTH, SYNC_FILTERTYPE_3MONTHS, SYNC_FILTERTYPE_6MONTHS + define('SYNC_FILTERTIME_MAX', SYNC_FILTERTYPE_ALL); + + // Interval in seconds before checking if there are changes on the server when in Ping. + // It means the highest time span before a change is pushed to a mobile. Set it to + // a higher value if you have a high load on the server. + define('PING_INTERVAL', 30); + + // Interval in seconds to force a re-check of potentially missed notifications when + // using a changes sink. Default are 300 seconds (every 5 min). + // This can also be disabled by setting it to false + define('SINK_FORCERECHECK', 300); + + // Set the fileas (save as) order for contacts in the webaccess/webapp/outlook. + // It will only affect new/modified contacts on the mobile which then are synced to the server. + // Possible values are: + // SYNC_FILEAS_FIRSTLAST - fileas will be "Firstname Middlename Lastname" + // SYNC_FILEAS_LASTFIRST - fileas will be "Lastname, Firstname Middlename" + // SYNC_FILEAS_COMPANYONLY - fileas will be "Company" + // SYNC_FILEAS_COMPANYLAST - fileas will be "Company (Lastname, Firstname Middlename)" + // SYNC_FILEAS_COMPANYFIRST - fileas will be "Company (Firstname Middlename Lastname)" + // SYNC_FILEAS_LASTCOMPANY - fileas will be "Lastname, Firstname Middlename (Company)" + // SYNC_FILEAS_FIRSTCOMPANY - fileas will be "Firstname Middlename Lastname (Company)" + // The company-fileas will only be set if a contact has a company set. If one of + // company-fileas is selected and a contact doesn't have a company set, it will default + // to SYNC_FILEAS_FIRSTLAST or SYNC_FILEAS_LASTFIRST (depending on if last or first + // option is selected for company). + // If SYNC_FILEAS_COMPANYONLY is selected and company of the contact is not set + // SYNC_FILEAS_LASTFIRST will be used + define('FILEAS_ORDER', SYNC_FILEAS_LASTFIRST); + + // Amount of items to be synchronized per request + // Normally this value is requested by the mobile. Common values are 5, 25, 50 or 100. + // Exporting too much items can cause mobile timeout on busy systems. + // Z-Push will use the lowest value, either set here or by the mobile. + // default: 100 - value used if mobile does not limit amount of items + define('SYNC_MAX_ITEMS', 100); + + // The devices usually send a list of supported properties for calendar and contact + // items. If a device does not includes such a supported property in Sync request, + // it means the property's value will be deleted on the server. + // However some devices do not send a list of supported properties. It is then impossible + // to tell if a property was deleted or it was not set at all if it does not appear in Sync. + // This parameter defines Z-Push behaviour during Sync if a device does not issue a list with + // supported properties. + // See also https://jira.zarafa.com/browse/ZP-302. + // Possible values: + // false - do not unset properties which are not sent during Sync (default) + // true - unset properties which are not sent during Sync + define('UNSET_UNDEFINED_PROPERTIES', false); + + // ActiveSync specifies that a contact photo may not exceed 48 KB. This value is checked + // in the semantic sanity checks and contacts with larger photos are not synchronized. + // This limitation is not being followed by the ActiveSync clients which set much bigger + // contact photos. You can override the default value of the max photo size. + // default: 49152 - 48 KB default max photo size in bytes + define('SYNC_CONTACTS_MAXPICTURESIZE', 49152); + + // Over the WebserviceUsers command it is possible to retrieve a list of all + // known devices and users on this Z-Push system. The authenticated user needs to have + // admin rights and a public folder must exist. + // In multicompany environments this enable an admin user of any company to retrieve + // this full list, so this feature is disabled by default. Enable with care. + define('ALLOW_WEBSERVICE_USERS_ACCESS', false); + +/********************************************************************************** + * Backend settings + */ + // the backend data provider + define('BACKEND_PROVIDER', ''); + +/********************************************************************************** + * Search provider settings + * + * Alternative backend to perform SEARCH requests (GAL search) + * By default the main Backend defines the preferred search functionality. + * If set, the Search Provider will always be preferred. + * Use 'BackendSearchLDAP' to search in a LDAP directory (see backend/searchldap/config.php) + */ + define('SEARCH_PROVIDER', ''); + // Time in seconds for the server search. Setting it too high might result in timeout. + // Setting it too low might not return all results. Default is 10. + define('SEARCH_WAIT', 10); + // The maximum number of results to send to the client. Setting it too high + // might result in timeout. Default is 10. + define('SEARCH_MAXRESULTS', 10); + + +/********************************************************************************** + * Synchronize additional folders to all mobiles + * + * With this feature, special folders can be synchronized to all mobiles. + * This is useful for e.g. global company contacts. + * + * This feature is supported only by certain devices, like iPhones. + * Check the compatibility list for supported devices: + * http://z-push.sf.net/compatibility + * + * To synchronize a folder, add a section setting all parameters as below: + * store: the ressource where the folder is located. + * Zarafa users use 'SYSTEM' for the 'Public Folder' + * folderid: folder id of the folder to be synchronized + * name: name to be displayed on the mobile device + * type: supported types are: + * SYNC_FOLDER_TYPE_USER_CONTACT + * SYNC_FOLDER_TYPE_USER_APPOINTMENT + * SYNC_FOLDER_TYPE_USER_TASK + * SYNC_FOLDER_TYPE_USER_MAIL + * + * Additional notes: + * - on Zarafa systems use backend/zarafa/listfolders.php script to get a list + * of available folders + * + * - all Z-Push users must have full writing permissions (secretary rights) so + * the configured folders can be synchronized to the mobile + * + * - this feature is only partly suitable for multi-tenancy environments, + * as ALL users from ALL tenents need access to the configured store & folder. + * When configuring a public folder, this will cause problems, as each user has + * a different public folder in his tenant, so the folder are not available. + + * - changing this configuration could cause HIGH LOAD on the system, as all + * connected devices will be updated and load the data contained in the + * added/modified folders. + */ + + $additionalFolders = array( + // demo entry for the synchronization of contacts from the public folder. + // uncomment (remove '/*' '*/') and fill in the folderid +/* + array( + 'store' => "SYSTEM", + 'folderid' => "", + 'name' => "Public Contacts", + 'type' => SYNC_FOLDER_TYPE_USER_CONTACT, + ), +*/ + ); + +?> \ No newline at end of file diff --git a/sources/include/mimeDecode.php b/sources/include/mimeDecode.php new file mode 100644 index 0000000..80bab66 --- /dev/null +++ b/sources/include/mimeDecode.php @@ -0,0 +1,1060 @@ + + * 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 George Schlossnagle + * @author Cipriano Groenendal + * @author Sean Coates + * @copyright 2003-2006 PEAR + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version CVS: $Id: mimeDecode.php 305875 2010-12-01 07:17:10Z alan_k $ + * @link http://pear.php.net/package/Mail_mime + */ + + +/** + * Z-Push changes + * + * removed PEAR dependency by implementing own raiseError() + * implemented automated decoding of strings from mail charset + * + * Reference implementation used: + * http://download.pear.php.net/package/Mail_mimeDecode-1.5.5.tgz + * + * used "old" method of checking if called statically, as this is deprecated between php 5.0.0 and 5.3.0 + * (isStatic of decode() around line 215) + * + */ + +/** + * require PEAR + * + * This package depends on PEAR to raise errors. + */ +//require_once 'PEAR.php'; + +/** + * The Mail_mimeDecode class is used to decode mail/mime messages + * + * This class will parse a raw mime email and return the structure. + * Returned structure is similar to that returned by imap_fetchstructure(). + * + * +----------------------------- IMPORTANT ------------------------------+ + * | Usage of this class compared to native php extensions such as | + * | mailparse or imap, is slow and may be feature deficient. If available| + * | you are STRONGLY recommended to use the php extensions. | + * +----------------------------------------------------------------------+ + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes + * @author George Schlossnagle + * @author Cipriano Groenendal + * @author Sean Coates + * @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_mimeDecode +{ + /** + * The raw email to decode + * + * @var string + * @access private + */ + var $_input; + + /** + * The header part of the input + * + * @var string + * @access private + */ + var $_header; + + /** + * The body part of the input + * + * @var string + * @access private + */ + var $_body; + + /** + * If an error occurs, this is used to store the message + * + * @var string + * @access private + */ + var $_error; + + /** + * Flag to determine whether to include bodies in the + * returned object. + * + * @var boolean + * @access private + */ + var $_include_bodies; + + /** + * Flag to determine whether to decode bodies + * + * @var boolean + * @access private + */ + var $_decode_bodies; + + /** + * Flag to determine whether to decode headers + * + * @var boolean + * @access private + */ + var $_decode_headers; + + /** + * Flag to determine whether to include attached messages + * as body in the returned object. Depends on $_include_bodies + * + * @var boolean + * @access private + */ + var $_rfc822_bodies; + + /** + * Constructor. + * + * Sets up the object, initialise the variables, and splits and + * stores the header and body of the input. + * + * @param string The input to decode + * @access public + */ + function Mail_mimeDecode($input, $deprecated_linefeed = '') + { + list($header, $body) = $this->_splitBodyHeader($input); + + $this->_input = $input; + $this->_header = $header; + $this->_body = $body; + $this->_decode_bodies = false; + $this->_include_bodies = true; + $this->_rfc822_bodies = false; + } + + /** + * Begins the decoding process. If called statically + * it will create an object and call the decode() method + * of it. + * + * @param array An array of various parameters that determine + * various things: + * include_bodies - Whether to include the body in the returned + * object. + * decode_bodies - Whether to decode the bodies + * of the parts. (Transfer encoding) + * decode_headers - Whether to decode headers + * input - If called statically, this will be treated + * as the input + * charset - convert all data to this charset + * @return object Decoded results + * @access public + */ + function decode($params = null) + { + // determine if this method has been called statically + $isStatic = !(isset($this) && get_class($this) == __CLASS__); + + // Have we been called statically? + // If so, create an object and pass details to that. + if ($isStatic AND isset($params['input'])) { + + $obj = new Mail_mimeDecode($params['input']); + $structure = $obj->decode($params); + + // Called statically but no input + } elseif ($isStatic) { + return $this->raiseError('Called statically and no input given'); + + // Called via an object + } else { + $this->_include_bodies = isset($params['include_bodies']) ? + $params['include_bodies'] : false; + $this->_decode_bodies = isset($params['decode_bodies']) ? + $params['decode_bodies'] : false; + $this->_decode_headers = isset($params['decode_headers']) ? + $params['decode_headers'] : false; + $this->_rfc822_bodies = isset($params['rfc_822bodies']) ? + $params['rfc_822bodies'] : false; + $this->_charset = isset($params['charset']) ? + strtolower($params['charset']) : 'utf-8'; + + $structure = $this->_decode($this->_header, $this->_body); + if ($structure === false) { + $structure = $this->raiseError($this->_error); + } + } + + return $structure; + } + + /** + * Performs the decoding. Decodes the body string passed to it + * If it finds certain content-types it will call itself in a + * recursive fashion + * + * @param string Header section + * @param string Body section + * @return object Results of decoding process + * @access private + */ + function _decode($headers, $body, $default_ctype = 'text/plain') + { + $return = new stdClass; + $return->headers = array(); + $headers = $this->_parseHeaders($headers); + + foreach ($headers as $value) { + $value['value'] = $this->_decode_headers ? $this->_decodeHeader($value['value']) : $value['value']; + if (isset($return->headers[strtolower($value['name'])]) AND !is_array($return->headers[strtolower($value['name'])])) { + $return->headers[strtolower($value['name'])] = array($return->headers[strtolower($value['name'])]); + $return->headers[strtolower($value['name'])][] = $value['value']; + + } elseif (isset($return->headers[strtolower($value['name'])])) { + $return->headers[strtolower($value['name'])][] = $value['value']; + + } else { + $return->headers[strtolower($value['name'])] = $value['value']; + } + } + + + foreach ($headers as $key => $value) { + $headers[$key]['name'] = strtolower($headers[$key]['name']); + switch ($headers[$key]['name']) { + + case 'content-type': + $content_type = $this->_parseHeaderValue($headers[$key]['value']); + + if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) { + $return->ctype_primary = $regs[1]; + $return->ctype_secondary = $regs[2]; + } + + if (isset($content_type['other'])) { + foreach($content_type['other'] as $p_name => $p_value) { + $return->ctype_parameters[$p_name] = $p_value; + } + } + break; + + case 'content-disposition': + $content_disposition = $this->_parseHeaderValue($headers[$key]['value']); + $return->disposition = $content_disposition['value']; + if (isset($content_disposition['other'])) { + foreach($content_disposition['other'] as $p_name => $p_value) { + $return->d_parameters[$p_name] = $p_value; + } + } + break; + + case 'content-transfer-encoding': + $content_transfer_encoding = $this->_parseHeaderValue($headers[$key]['value']); + break; + } + } + + if (isset($content_type)) { + switch (strtolower($content_type['value'])) { + case 'text/plain': + $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; + $charset = isset($return->ctype_parameters['charset']) ? $return->ctype_parameters['charset'] : $this->_charset; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding, $charset) : $body) : null; + break; + + case 'text/html': + $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; + $charset = isset($return->ctype_parameters['charset']) ? $return->ctype_parameters['charset'] : $this->_charset; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding, $charset) : $body) : null; + break; + + case 'multipart/parallel': + case 'multipart/appledouble': // Appledouble mail + case 'multipart/report': // RFC1892 + case 'multipart/signed': // PGP + case 'multipart/digest': + case 'multipart/alternative': + case 'multipart/related': + case 'multipart/mixed': + case 'application/vnd.wap.multipart.related': + if(!isset($content_type['other']['boundary'])){ + $this->_error = 'No boundary found for ' . $content_type['value'] . ' part'; + return false; + } + + $default_ctype = (strtolower($content_type['value']) === 'multipart/digest') ? 'message/rfc822' : 'text/plain'; + + $parts = $this->_boundarySplit($body, $content_type['other']['boundary']); + for ($i = 0; $i < count($parts); $i++) { + list($part_header, $part_body) = $this->_splitBodyHeader($parts[$i]); + $part = $this->_decode($part_header, $part_body, $default_ctype); + if($part === false) + $part = $this->raiseError($this->_error); + $return->parts[] = $part; + } + break; + + case 'message/rfc822': + 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); + } + + $obj = new Mail_mimeDecode($body); + $return->parts[] = $obj->decode(array('include_bodies' => $this->_include_bodies, + 'decode_bodies' => $this->_decode_bodies, + 'decode_headers' => $this->_decode_headers)); + unset($obj); + break; + + default: + if(!isset($content_transfer_encoding['value'])) + $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; + break; + } + + } else { + $ctype = explode('/', $default_ctype); + $return->ctype_primary = $ctype[0]; + $return->ctype_secondary = $ctype[1]; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body) : $body) : null; + } + + return $return; + } + + /** + * Given the output of the above function, this will return an + * array of references to the parts, indexed by mime number. + * + * @param object $structure The structure to go through + * @param string $mime_number Internal use only. + * @return array Mime numbers + */ + function &getMimeNumbers(&$structure, $no_refs = false, $mime_number = '', $prepend = '') + { + $return = array(); + if (!empty($structure->parts)) { + if ($mime_number != '') { + $structure->mime_id = $prepend . $mime_number; + $return[$prepend . $mime_number] = &$structure; + } + for ($i = 0; $i < count($structure->parts); $i++) { + + + if (!empty($structure->headers['content-type']) AND substr(strtolower($structure->headers['content-type']), 0, 8) == 'message/') { + $prepend = $prepend . $mime_number . '.'; + $_mime_number = ''; + } else { + $_mime_number = ($mime_number == '' ? $i + 1 : sprintf('%s.%s', $mime_number, $i + 1)); + } + + $arr = &Mail_mimeDecode::getMimeNumbers($structure->parts[$i], $no_refs, $_mime_number, $prepend); + foreach ($arr as $key => $val) { + $no_refs ? $return[$key] = '' : $return[$key] = &$arr[$key]; + } + } + } else { + if ($mime_number == '') { + $mime_number = '1'; + } + $structure->mime_id = $prepend . $mime_number; + $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure; + } + + return $return; + } + + /** + * Given a string containing a header and body + * section, this function will split them (at the first + * blank line) and return them. + * + * @param string Input to split apart + * @return array Contains header and body section + * @access private + */ + function _splitBodyHeader($input) + { + if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) { + return array($match[1], $match[2]); + } + // bug #17325 - empty bodies are allowed. - we just check that at least one line + // of headers exist.. + if (count(explode("\n",$input))) { + return array($input, ''); + } + $this->_error = 'Could not split header and body'; + return false; + } + + /** + * Parse headers given in $input and return + * as assoc array. + * + * @param string Headers to parse + * @return array Contains parsed headers + * @access private + */ + function _parseHeaders($input) + { + + if ($input !== '') { + // Unfold the input + $input = preg_replace("/\r?\n/", "\r\n", $input); + //#7065 - wrapping.. with encoded stuff.. - probably not needed, + // wrapping space should only get removed if the trailing item on previous line is a + // encoded character + $input = preg_replace("/=\r\n(\t| )+/", '=', $input); + $input = preg_replace("/\r\n(\t| )+/", ' ', $input); + + $headers = explode("\r\n", trim($input)); + + foreach ($headers as $value) { + $hdr_name = substr($value, 0, $pos = strpos($value, ':')); + $hdr_value = substr($value, $pos+1); + if($hdr_value[0] == ' ') + $hdr_value = substr($hdr_value, 1); + + $return[] = array( + 'name' => $hdr_name, + 'value' => $hdr_value + ); + } + } else { + $return = array(); + } + + return $return; + } + + /** + * Function to parse a header value, + * extract first part, and any secondary + * parts (after ;) This function is not as + * robust as it could be. Eg. header comments + * in the wrong place will probably break it. + * + * @param string Header value to parse + * @return array Contains parsed result + * @access private + */ + function _parseHeaderValue($input) + { + + if (($pos = strpos($input, ';')) === false) { + $input = $this->_decode_headers ? $this->_decodeHeader($input) : $input; + $return['value'] = trim($input); + return $return; + } + + + + $value = substr($input, 0, $pos); + $value = $this->_decode_headers ? $this->_decodeHeader($value) : $value; + $return['value'] = trim($value); + $input = trim(substr($input, $pos+1)); + + if (!strlen($input) > 0) { + return $return; + } + // at this point input contains xxxx=".....";zzzz="...." + // since we are dealing with quoted strings, we need to handle this properly.. + $i = 0; + $l = strlen($input); + $key = ''; + $val = false; // our string - including quotes.. + $q = false; // in quote.. + $lq = ''; // last quote.. + + while ($i < $l) { + + $c = $input[$i]; + //var_dump(array('i'=>$i,'c'=>$c,'q'=>$q, 'lq'=>$lq, 'key'=>$key, 'val' =>$val)); + + $escaped = false; + if ($c == '\\') { + $i++; + if ($i == $l-1) { // end of string. + break; + } + $escaped = true; + $c = $input[$i]; + } + + + // state - in key.. + if ($val === false) { + if (!$escaped && $c == '=') { + $val = ''; + $key = trim($key); + $i++; + continue; + } + if (!$escaped && $c == ';') { + if ($key) { // a key without a value.. + $key= trim($key); + $return['other'][$key] = ''; + $return['other'][strtolower($key)] = ''; + } + $key = ''; + } + $key .= $c; + $i++; + continue; + } + + // state - in value.. (as $val is set..) + + if ($q === false) { + // not in quote yet. + if ((!strlen($val) || $lq !== false) && $c == ' ' || $c == "\t") { + $i++; + continue; // skip leading spaces after '=' or after '"' + } + if (!$escaped && ($c == '"' || $c == "'")) { + // start quoted area.. + $q = $c; + // in theory should not happen raw text in value part.. + // but we will handle it as a merged part of the string.. + $val = !strlen(trim($val)) ? '' : trim($val); + $i++; + continue; + } + // got end.... + if (!$escaped && $c == ';') { + + $val = trim($val); + $added = false; + if (preg_match('/\*[0-9]+$/', $key)) { + // this is the extended aaa*0=...;aaa*1=.... code + // it assumes the pieces arrive in order, and are valid... + $key = preg_replace('/\*[0-9]+$/', '', $key); + if (isset($return['other'][$key])) { + $return['other'][$key] .= $val; + if (strtolower($key) != $key) { + $return['other'][strtolower($key)] .= $val; + } + $added = true; + } + // continue and use standard setters.. + } + if (!$added) { + $return['other'][$key] = $val; + $return['other'][strtolower($key)] = $val; + } + $val = false; + $key = ''; + $lq = false; + $i++; + continue; + } + + $val .= $c; + $i++; + continue; + } + + // state - in quote.. + if (!$escaped && $c == $q) { // potential exit state.. + + // end of quoted string.. + $lq = $q; + $q = false; + $i++; + continue; + } + + // normal char inside of quoted string.. + $val.= $c; + $i++; + } + + // do we have anything left.. + if (strlen(trim($key)) || $val !== false) { + + $val = trim($val); + $added = false; + if ($val !== false && preg_match('/\*[0-9]+$/', $key)) { + // no dupes due to our crazy regexp. + $key = preg_replace('/\*[0-9]+$/', '', $key); + if (isset($return['other'][$key])) { + $return['other'][$key] .= $val; + if (strtolower($key) != $key) { + $return['other'][strtolower($key)] .= $val; + } + $added = true; + } + // continue and use standard setters.. + } + if (!$added) { + $return['other'][$key] = $val; + $return['other'][strtolower($key)] = $val; + } + } + // decode values. + foreach($return['other'] as $key =>$val) { + $return['other'][$key] = $this->_decode_headers ? $this->_decodeHeader($val) : $val; + } + //print_r($return); + return $return; + } + + /** + * This function splits the input based + * on the given boundary + * + * @param string Input to parse + * @return array Contains array of resulting mime parts + * @access private + */ + function _boundarySplit($input, $boundary) + { + $parts = array(); + + $bs_possible = substr($boundary, 2, -2); + $bs_check = '\"' . $bs_possible . '\"'; + + if ($boundary == $bs_check) { + $boundary = $bs_possible; + } + $tmp = preg_split("/--".preg_quote($boundary, '/')."((?=\s)|--)/", $input); + + $len = count($tmp) -1; + for ($i = 1; $i < $len; $i++) { + if (strlen(trim($tmp[$i]))) { + $parts[] = $tmp[$i]; + } + } + + // add the last part on if it does not end with the 'closing indicator' + if (!empty($tmp[$len]) && strlen(trim($tmp[$len])) && $tmp[$len][0] != '-') { + $parts[] = $tmp[$len]; + } + return $parts; + } + + /** + * Given a header, this function will decode it + * according to RFC2047. Probably not *exactly* + * conformant, but it does pass all the given + * examples (in RFC2047). + * + * @param string Input header value to decode + * @return string Decoded header value + * @access private + */ + function _decodeHeader($input) + { + // Remove white space between encoded-words + $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input); + + // For each encoded-word... + while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) { + + $encoded = $matches[1]; + $charset = $matches[2]; + $encoding = $matches[3]; + $text = $matches[4]; + + switch (strtolower($encoding)) { + case 'b': + $text = base64_decode($text); + break; + + case 'q': + $text = str_replace('_', ' ', $text); + preg_match_all('/=([a-f0-9]{2})/i', $text, $matches); + foreach($matches[1] as $value) + $text = str_replace('='.$value, chr(hexdec($value)), $text); + break; + } + + $input = str_replace($encoded, $this->_fromCharset($charset, $text), $input); + } + + return $input; + } + + /** + * Given a body string and an encoding type, + * this function will decode and return it. + * + * @param string Input body to decode + * @param string Encoding type to use. + * @return string Decoded body + * @access private + */ + function _decodeBody($input, $encoding = '7bit', $charset = '') + { + 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)); + break; + + case 'base64': + return $this->_fromCharset($charset, base64_decode($input)); + break; + + default: + return $input; + } + } + + /** + * Given a quoted-printable string, this + * function will decode and return it. + * + * @param string Input body to decode + * @return string Decoded body + * @access private + */ + function _quotedPrintableDecode($input) + { + // Remove soft line breaks + $input = preg_replace("/=\r?\n/", '', $input); + + // Replace encoded characters + $input = preg_replace('/=([a-f0-9]{2})/ie', "chr(hexdec('\\1'))", $input); + + return $input; + } + + /** + * Checks the input for uuencoded files and returns + * an array of them. Can be called statically, eg: + * + * $files =& Mail_mimeDecode::uudecode($some_text); + * + * It will check for the begin 666 ... end syntax + * however and won't just blindly decode whatever you + * pass it. + * + * @param string Input body to look for attahcments in + * @return array Decoded bodies, filenames and permissions + * @access public + * @author Unknown + */ + function &uudecode($input) + { + // Find all uuencoded sections + preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches); + + for ($j = 0; $j < count($matches[3]); $j++) { + + $str = $matches[3][$j]; + $filename = $matches[2][$j]; + $fileperm = $matches[1][$j]; + + $file = ''; + $str = preg_split("/\r?\n/", trim($str)); + $strlen = count($str); + + for ($i = 0; $i < $strlen; $i++) { + $pos = 1; + $d = 0; + $len=(int)(((ord(substr($str[$i],0,1)) -32) - ' ') & 077); + + while (($d + 3 <= $len) AND ($pos + 4 <= strlen($str[$i]))) { + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); + $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20); + $c3 = (ord(substr($str[$i],$pos+3,1)) ^ 0x20); + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); + + $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2)); + + $file .= chr(((($c2 - ' ') & 077) << 6) | (($c3 - ' ') & 077)); + + $pos += 4; + $d += 3; + } + + if (($d + 2 <= $len) && ($pos + 3 <= strlen($str[$i]))) { + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); + $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20); + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); + + $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2)); + + $pos += 3; + $d += 2; + } + + if (($d + 1 <= $len) && ($pos + 2 <= strlen($str[$i]))) { + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); + + } + } + $files[] = array('filename' => $filename, 'fileperm' => $fileperm, 'filedata' => $file); + } + + return $files; + } + + /** + * getSendArray() returns the arguments required for Mail::send() + * used to build the arguments for a mail::send() call + * + * Usage: + * $mailtext = Full email (for example generated by a template) + * $decoder = new Mail_mimeDecode($mailtext); + * $parts = $decoder->getSendArray(); + * if (!PEAR::isError($parts) { + * list($recipents,$headers,$body) = $parts; + * $mail = Mail::factory('smtp'); + * $mail->send($recipents,$headers,$body); + * } else { + * echo $parts->message; + * } + * @return mixed array of recipeint, headers,body or Pear_Error + * @access public + * @author Alan Knowles + */ + function getSendArray() + { + // prevent warning if this is not set + $this->_decode_headers = FALSE; + $headerlist =$this->_parseHeaders($this->_header); + $to = ""; + if (!$headerlist) { + return $this->raiseError("Message did not contain headers"); + } + foreach($headerlist as $item) { + $header[$item['name']] = $item['value']; + switch (strtolower($item['name'])) { + case "to": + case "cc": + case "bcc": + $to .= ",".$item['value']; + default: + break; + } + } + if ($to == "") { + return $this->raiseError("Message did not contain any recipents"); + } + $to = substr($to,1); + return array($to,$header,$this->_body); + } + + /** + * Returns a xml copy of the output of + * Mail_mimeDecode::decode. Pass the output in as the + * argument. This function can be called statically. Eg: + * + * $output = $obj->decode(); + * $xml = Mail_mimeDecode::getXML($output); + * + * The DTD used for this should have been in the package. Or + * alternatively you can get it from cvs, or here: + * http://www.phpguru.org/xmail/xmail.dtd. + * + * @param object Input to convert to xml. This should be the + * output of the Mail_mimeDecode::decode function + * @return string XML version of input + * @access public + */ + function getXML($input) + { + $crlf = "\r\n"; + $output = '' . $crlf . + '' . $crlf . + '' . $crlf . + Mail_mimeDecode::_getXML($input) . + ''; + + return $output; + } + + /** + * Function that does the actual conversion to xml. Does a single + * mimepart at a time. + * + * @param object Input to convert to xml. This is a mimepart object. + * It may or may not contain subparts. + * @param integer Number of tabs to indent + * @return string XML version of input + * @access private + */ + function _getXML($input, $indent = 1) + { + $htab = "\t"; + $crlf = "\r\n"; + $output = ''; + $headers = @(array)$input->headers; + + foreach ($headers as $hdr_name => $hdr_value) { + + // Multiple headers with this name + if (is_array($headers[$hdr_name])) { + for ($i = 0; $i < count($hdr_value); $i++) { + $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value[$i], $indent); + } + + // Only one header of this sort + } else { + $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value, $indent); + } + } + + if (!empty($input->parts)) { + for ($i = 0; $i < count($input->parts); $i++) { + $output .= $crlf . str_repeat($htab, $indent) . '' . $crlf . + Mail_mimeDecode::_getXML($input->parts[$i], $indent+1) . + str_repeat($htab, $indent) . '' . $crlf; + } + } elseif (isset($input->body)) { + $output .= $crlf . str_repeat($htab, $indent) . 'body . ']]>' . $crlf; + } + + return $output; + } + + /** + * Helper function to _getXML(). Returns xml of a header. + * + * @param string Name of header + * @param string Value of header + * @param integer Number of tabs to indent + * @return string XML version of input + * @access private + */ + function _getXML_helper($hdr_name, $hdr_value, $indent) + { + $htab = "\t"; + $crlf = "\r\n"; + $return = ''; + + $new_hdr_value = ($hdr_name != 'received') ? Mail_mimeDecode::_parseHeaderValue($hdr_value) : array('value' => $hdr_value); + $new_hdr_name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $hdr_name))); + + // Sort out any parameters + if (!empty($new_hdr_value['other'])) { + foreach ($new_hdr_value['other'] as $paramname => $paramvalue) { + $params[] = str_repeat($htab, $indent) . $htab . '' . $crlf . + str_repeat($htab, $indent) . $htab . $htab . '' . htmlspecialchars($paramname) . '' . $crlf . + str_repeat($htab, $indent) . $htab . $htab . '' . htmlspecialchars($paramvalue) . '' . $crlf . + str_repeat($htab, $indent) . $htab . '' . $crlf; + } + + $params = implode('', $params); + } else { + $params = ''; + } + + $return = str_repeat($htab, $indent) . '
' . $crlf . + str_repeat($htab, $indent) . $htab . '' . htmlspecialchars($new_hdr_name) . '' . $crlf . + str_repeat($htab, $indent) . $htab . '' . htmlspecialchars($new_hdr_value['value']) . '' . $crlf . + $params . + str_repeat($htab, $indent) . '
' . $crlf; + + 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 + * + * @param string debug message + * @return boolean always false as there was an error + * @access private + */ + function raiseError($message) { + ZLog::Write(LOGLEVEL_ERROR, "mimeDecode error: ". $message); + return false; + } +} // End of class \ No newline at end of file diff --git a/sources/include/stringstreamwrapper.php b/sources/include/stringstreamwrapper.php new file mode 100644 index 0000000..90b8ea4 --- /dev/null +++ b/sources/include/stringstreamwrapper.php @@ -0,0 +1,144 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class StringStreamWrapper { + const PROTOCOL = "stringstream"; + + private $stringstream; + private $position; + private $stringlength; + + /** + * Opens the stream + * The string to be streamed is passed over the context + * + * @param string $path Specifies the URL that was passed to the original function + * @param string $mode The mode used to open the file, as detailed for fopen() + * @param int $options Holds additional flags set by the streams API + * @param string $opened_path If the path is opened successfully, and STREAM_USE_PATH is set in options, + * opened_path should be set to the full path of the file/resource that was actually opened. + * + * @access public + * @return boolean + */ + public function stream_open($path, $mode, $options, &$opened_path) { + $contextOptions = stream_context_get_options($this->context); + if (!isset($contextOptions[self::PROTOCOL]['string'])) + return false; + + $this->position = 0; + + // this is our stream! + $this->stringstream = $contextOptions[self::PROTOCOL]['string']; + + $this->stringlength = strlen($this->stringstream); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("StringStreamWrapper::stream_open(): initialized stream length: %d", $this->stringlength)); + + return true; + } + + /** + * Reads from stream + * + * @param int $len amount of bytes to be read + * + * @access public + * @return string + */ + public function stream_read($len) { + $data = substr($this->stringstream, $this->position, $len); + $this->position += strlen($data); + return $data; + } + + /** + * Returns the current position on stream + * + * @access public + * @return int + */ + public function stream_tell() { + return $this->position; + } + + /** + * Indicates if 'end of file' is reached + * + * @access public + * @return boolean + */ + public function stream_eof() { + return ($this->position >= $this->stringlength); + } + + /** + * Retrieves information about a stream + * + * @access public + * @return array + */ + public function stream_stat() { + return array( + 7 => $this->stringlength, + 'size' => $this->stringlength, + ); + } + + /** + * Instantiates a StringStreamWrapper + * + * @param string $string The string to be wrapped + * + * @access public + * @return StringStreamWrapper + */ + static public function Open($string) { + $context = stream_context_create(array(self::PROTOCOL => array('string' => &$string))); + return fopen(self::PROTOCOL . "://",'r', false, $context); + } +} + +stream_wrapper_register(StringStreamWrapper::PROTOCOL, "StringStreamWrapper") + +?> \ No newline at end of file diff --git a/sources/include/z_RFC822.php b/sources/include/z_RFC822.php new file mode 100644 index 0000000..d5909e3 --- /dev/null +++ b/sources/include/z_RFC822.php @@ -0,0 +1,937 @@ + | +// | Chuck Hagenbuch | +// +-----------------------------------------------------------------------+ + +/** + * RFC 822 Email address list validation Utility + * + * What is it? + * + * This class will take an address string, and parse it into it's consituent + * parts, be that either addresses, groups, or combinations. Nested groups + * are not supported. The structure it returns is pretty straight forward, + * and is similar to that provided by the imap_rfc822_parse_adrlist(). Use + * print_r() to view the structure. + * + * How do I use it? + * + * $address_string = 'My Group: "Richard" (A comment), ted@example.com (Ted Bloggs), Barney;'; + * $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', true) + * print_r($structure); + * + * @author Richard Heyes + * @author Chuck Hagenbuch + * @version $Revision: 1.23 $ + * @license BSD + * @package Mail + */ +class Mail_RFC822 { + + /** + * The address being parsed by the RFC822 object. + * @var string $address + */ + var $address = ''; + + /** + * The default domain to use for unqualified addresses. + * @var string $default_domain + */ + var $default_domain = 'localhost'; + + /** + * Should we return a nested array showing groups, or flatten everything? + * @var boolean $nestGroups + */ + var $nestGroups = true; + + /** + * Whether or not to validate atoms for non-ascii characters. + * @var boolean $validate + */ + var $validate = true; + + /** + * The array of raw addresses built up as we parse. + * @var array $addresses + */ + var $addresses = array(); + + /** + * The final array of parsed address information that we build up. + * @var array $structure + */ + var $structure = array(); + + /** + * The current error message, if any. + * @var string $error + */ + var $error = null; + + /** + * An internal counter/pointer. + * @var integer $index + */ + var $index = null; + + /** + * The number of groups that have been found in the address list. + * @var integer $num_groups + * @access public + */ + var $num_groups = 0; + + /** + * A variable so that we can tell whether or not we're inside a + * Mail_RFC822 object. + * @var boolean $mailRFC822 + */ + var $mailRFC822 = true; + + /** + * A limit after which processing stops + * @var int $limit + */ + var $limit = null; + + /** + * Sets up the object. The address must either be set here or when + * calling parseAddressList(). One or the other. + * + * @access public + * @param string $address The address(es) to validate. + * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost. + * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. + * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. + * + * @return object Mail_RFC822 A new Mail_RFC822 object. + */ + function Mail_RFC822($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) + { + if (isset($address)) $this->address = $address; + if (isset($default_domain)) $this->default_domain = $default_domain; + if (isset($nest_groups)) $this->nestGroups = $nest_groups; + if (isset($validate)) $this->validate = $validate; + if (isset($limit)) $this->limit = $limit; + } + + /** + * Starts the whole process. The address must either be set here + * or when creating the object. One or the other. + * + * @access public + * @param string $address The address(es) to validate. + * @param string $default_domain Default domain/host etc. + * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. + * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. + * + * @return array A structured array of addresses. + */ + function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) + { + if (!isset($this) || !isset($this->mailRFC822)) { + $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit); + return $obj->parseAddressList(); + } + + if (isset($address)) $this->address = $address; + if (strlen(trim($this->address)) == 0) return array(); + if (isset($default_domain)) $this->default_domain = $default_domain; + if (isset($nest_groups)) $this->nestGroups = $nest_groups; + if (isset($validate)) $this->validate = $validate; + if (isset($limit)) $this->limit = $limit; + + $this->structure = array(); + $this->addresses = array(); + $this->error = null; + $this->index = null; + + // Unfold any long lines in $this->address. + $this->address = preg_replace('/\r?\n/', "\r\n", $this->address); + $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); + } + + // Validate each address individually. If we encounter an invalid + // address, stop iterating and return an error immediately. + foreach ($this->addresses as $address) { + $valid = $this->_validateAddress($address); + + if ($valid === false || isset($this->error)) { + //require_once 'PEAR.php'; + return $this->raiseError($this->error); + } + + if (!$this->nestGroups) { + $this->structure = array_merge($this->structure, $valid); + } else { + $this->structure[] = $valid; + } + } + + return $this->structure; + } + + /** + * Splits an address into separate addresses. + * + * @access private + * @param string $address The addresses to split. + * @return boolean Success or failure. + */ + function _splitAddresses($address) + { + if (!empty($this->limit) && count($this->addresses) == $this->limit) { + return ''; + } + + if ($this->_isGroup($address) && !isset($this->error)) { + $split_char = ';'; + $is_group = true; + } elseif (!isset($this->error)) { + $split_char = ','; + $is_group = false; + } elseif (isset($this->error)) { + return false; + } + + // Split the string based on the above ten or so lines. + $parts = explode($split_char, $address); + $string = $this->_splitCheck($parts, $split_char); + + // If a group... + if ($is_group) { + // If $string does not contain a colon outside of + // brackets/quotes etc then something's fubar. + + // First check there's a colon at all: + if (strpos($string, ':') === false) { + $this->error = 'Invalid address: ' . $string; + return false; + } + + // Now check it's outside of brackets/quotes: + if (!$this->_splitCheck(explode(':', $string), ':')) { + return false; + } + + // We must have a group at this point, so increase the counter: + $this->num_groups++; + } + + // $string now contains the first full address/group. + // Add to the addresses array. + $this->addresses[] = array( + 'address' => trim($string), + 'group' => $is_group + ); + + // Remove the now stored address from the initial line, the +1 + // is to account for the explode character. + $address = trim(substr($address, strlen($string) + 1)); + + // If the next char is a comma and this was a group, then + // there are more addresses, otherwise, if there are any more + // chars, then there is another address. + if ($is_group && substr($address, 0, 1) == ','){ + $address = trim(substr($address, 1)); + return $address; + + } elseif (strlen($address) > 0) { + return $address; + + } else { + return ''; + } + + // If you got here then something's off + return false; + } + + /** + * Checks for a group at the start of the string. + * + * @access private + * @param string $address The address to check. + * @return boolean Whether or not there is a group at the start of the string. + */ + function _isGroup($address) + { + // First comma not in quotes, angles or escaped: + $parts = explode(',', $address); + $string = $this->_splitCheck($parts, ','); + + // Now we have the first address, we can reliably check for a + // group by searching for a colon that's not escaped or in + // quotes or angle brackets. + if (count($parts = explode(':', $string)) > 1) { + $string2 = $this->_splitCheck($parts, ':'); + return ($string2 !== $string); + } else { + return false; + } + } + + /** + * A common function that will check an exploded string. + * + * @access private + * @param array $parts The exloded string. + * @param string $char The char that was exploded on. + * @return mixed False if the string contains unclosed quotes/brackets, or the string on success. + */ + function _splitCheck($parts, $char) + { + $string = $parts[0]; + + for ($i = 0; $i < count($parts); $i++) { + if ($this->_hasUnclosedQuotes($string) + || $this->_hasUnclosedBrackets($string, '<>') + || $this->_hasUnclosedBrackets($string, '[]') + || $this->_hasUnclosedBrackets($string, '()') + || substr($string, -1) == '\\') { + if (isset($parts[$i + 1])) { + $string = $string . $char . $parts[$i + 1]; + } else { + $this->error = 'Invalid address spec. Unclosed bracket or quotes'; + return false; + } + } else { + $this->index = $i; + break; + } + } + + return $string; + } + + /** + * Checks if a string has an 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. + */ + function _hasUnclosedQuotes($string) + { + $string = explode('"', $string); + $string_cnt = count($string); + + for ($i = 0; $i < (count($string) - 1); $i++) + if (substr($string[$i], -1) == '\\') + $string_cnt--; + + return ($string_cnt % 2 === 0); + } + + /** + * Checks if a string has an unclosed brackets or not. IMPORTANT: + * This function handles both angle brackets and square brackets; + * + * @access private + * @param string $string The string to check. + * @param string $chars The characters to check for. + * @return boolean True if there are unclosed brackets inside the string, false otherwise. + */ + function _hasUnclosedBrackets($string, $chars) + { + $num_angle_start = substr_count($string, $chars[0]); + $num_angle_end = substr_count($string, $chars[1]); + + $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]); + $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]); + + if ($num_angle_start < $num_angle_end) { + $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')'; + return false; + } else { + return ($num_angle_start > $num_angle_end); + } + } + + /** + * Sub function that is used only by hasUnclosedBrackets(). + * + * @access private + * @param string $string The string to check. + * @param integer &$num The number of occurences. + * @param string $char The character to count. + * @return integer The number of occurences of $char in $string, adjusted for backslashes. + */ + function _hasUnclosedBracketsSub($string, &$num, $char) + { + $parts = explode($char, $string); + for ($i = 0; $i < count($parts); $i++){ + if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i])) + $num--; + if (isset($parts[$i + 1])) + $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1]; + } + + return $num; + } + + /** + * Function to begin checking the address. + * + * @access private + * @param string $address The address to validate. + * @return mixed False on failure, or a structured array of address information on success. + */ + function _validateAddress($address) + { + $is_group = false; + $addresses = array(); + + if ($address['group']) { + $is_group = true; + + // Get the group part of the name + $parts = explode(':', $address['address']); + $groupname = $this->_splitCheck($parts, ':'); + $structure = array(); + + // And validate the group part of the name. + if (!$this->_validatePhrase($groupname)){ + $this->error = 'Group name did not validate.'; + return false; + } else { + // Don't include groups if we are not nesting + // them. This avoids returning invalid addresses. + if ($this->nestGroups) { + $structure = new stdClass; + $structure->groupname = $groupname; + } + } + + $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':'))); + } + + // If a group then split on comma and put into an array. + // Otherwise, Just put the whole address in an array. + if ($is_group) { + while (strlen($address['address']) > 0) { + $parts = explode(',', $address['address']); + $addresses[] = $this->_splitCheck($parts, ','); + $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ','))); + } + } else { + $addresses[] = $address['address']; + } + + // Check that $addresses is set, if address like this: + // Groupname:; + // Then errors were appearing. + if (!count($addresses)){ + $this->error = 'Empty group.'; + return false; + } + + // Trim the whitespace from all of the address strings. + array_map('trim', $addresses); + + // Validate each mailbox. + // Format could be one of: name + // geezer@domain.com + // geezer + // ... or any other format valid by RFC 822. + for ($i = 0; $i < count($addresses); $i++) { + if (!$this->validateMailbox($addresses[$i])) { + if (empty($this->error)) { + $this->error = 'Validation failed for: ' . $addresses[$i]; + } + return false; + } + } + + // Nested format + if ($this->nestGroups) { + if ($is_group) { + $structure->addresses = $addresses; + } else { + $structure = $addresses[0]; + } + + // Flat format + } else { + if ($is_group) { + $structure = array_merge($structure, $addresses); + } else { + $structure = $addresses; + } + } + + return $structure; + } + + /** + * Function to validate a phrase. + * + * @access private + * @param string $phrase The phrase to check. + * @return boolean Success or failure. + */ + function _validatePhrase($phrase) + { + // 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, ' '); + for ($i = 0; $i < $this->index + 1; $i++) + array_shift($parts); + } + + foreach ($phrase_parts as $part) { + // If quoted string: + if (substr($part, 0, 1) == '"') { + if (!$this->_validateQuotedString($part)) { + return false; + } + continue; + } + + // Otherwise it's an atom: + if (!$this->_validateAtom($part)) return false; + } + + return true; + } + + /** + * Function to validate an atom which from rfc822 is: + * atom = 1* + * + * If validation ($this->validate) has been turned off, then + * validateAtom() doesn't actually check anything. This is so that you + * can split a list of addresses up before encoding personal names + * (umlauts, etc.), for example. + * + * @access private + * @param string $atom The string to check. + * @return boolean Success or failure. + */ + function _validateAtom($atom) + { + if (!$this->validate) { + // 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; + } + + // Check for specials: + if (preg_match('/[][()<>@,;\\:". ]/', $atom)) { + return false; + } + + // Check for control characters (ASCII 0-31): + if (preg_match('/[\\x00-\\x1F]+/', $atom)) { + return false; + } + return true; + } + + /** + * Function to validate quoted string, which is: + * quoted-string = <"> *(qtext/quoted-pair) <"> + * + * @access private + * @param string $qstring The string to check + * @return boolean Success or failure. + */ + function _validateQuotedString($qstring) + { + // Leading and trailing " + $qstring = substr($qstring, 1, -1); + + // Perform check, removing quoted characters first. + return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring)); + } + + /** + * Function to validate a mailbox, which is: + * mailbox = addr-spec ; simple address + * / phrase route-addr ; name and route-addr + * + * @access public + * @param string &$mailbox The string to check. + * @return boolean Success or failure. + */ + function validateMailbox(&$mailbox) + { + // A couple of defaults. + $phrase = ''; + $comment = ''; + $comments = array(); + + // Catch any RFC822 comments and store them separately. + $_mailbox = $mailbox; + while (strlen(trim($_mailbox)) > 0) { + $parts = explode('(', $_mailbox); + $before_comment = $this->_splitCheck($parts, '('); + if ($before_comment != $_mailbox) { + // First char should be a (. + $comment = substr(str_replace($before_comment, '', $_mailbox), 1); + $parts = explode(')', $comment); + $comment = $this->_splitCheck($parts, ')'); + $comments[] = $comment; + + // +1 is for the trailing ) + $_mailbox = substr($_mailbox, strpos($_mailbox, $comment)+strlen($comment)+1); + } else { + break; + } + } + + foreach ($comments as $comment) { + $mailbox = str_replace("($comment)", '', $mailbox); + } + + $mailbox = trim($mailbox); + + // Check for name + route-addr + if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') { + $parts = explode('<', $mailbox); + $name = $this->_splitCheck($parts, '<'); + + $phrase = trim($name); + $route_addr = trim(substr($mailbox, strlen($name.'<'), -1)); + + //z-push fix for umlauts and other special chars + if (substr($phrase, 0, 1) != '"' && substr($phrase, -1) != '"') { + $phrase = '"'.$phrase.'"'; + } + + if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) { + + return false; + } + + // Only got addr-spec + } else { + // First snip angle brackets if present. + if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') { + $addr_spec = substr($mailbox, 1, -1); + } else { + $addr_spec = $mailbox; + } + + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } + + // Construct the object that will be returned. + $mbox = new stdClass(); + + // Add the phrase (even if empty) and comments + $mbox->personal = $phrase; + $mbox->comment = isset($comments) ? $comments : array(); + + if (isset($route_addr)) { + $mbox->mailbox = $route_addr['local_part']; + $mbox->host = $route_addr['domain']; + $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : ''; + } else { + $mbox->mailbox = $addr_spec['local_part']; + $mbox->host = $addr_spec['domain']; + } + + $mailbox = $mbox; + return true; + } + + /** + * This function validates a route-addr which is: + * route-addr = "<" [route] addr-spec ">" + * + * Angle brackets have already been removed at the point of + * getting to this function. + * + * @access private + * @param string $route_addr The string to check. + * @return mixed False on failure, or an array containing validated address/route information on success. + */ + function _validateRouteAddr($route_addr) + { + // Check for colon. + if (strpos($route_addr, ':') !== false) { + $parts = explode(':', $route_addr); + $route = $this->_splitCheck($parts, ':'); + } else { + $route = $route_addr; + } + + // If $route is same as $route_addr then the colon was in + // quotes or brackets or, of course, non existent. + if ($route === $route_addr){ + unset($route); + $addr_spec = $route_addr; + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } else { + // Validate route part. + if (($route = $this->_validateRoute($route)) === false) { + return false; + } + + $addr_spec = substr($route_addr, strlen($route . ':')); + + // Validate addr-spec part. + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } + + if (isset($route)) { + $return['adl'] = $route; + } else { + $return['adl'] = ''; + } + + $return = array_merge($return, $addr_spec); + return $return; + } + + /** + * Function to validate a route, which is: + * route = 1#("@" domain) ":" + * + * @access private + * @param string $route The string to check. + * @return mixed False on failure, or the validated $route on success. + */ + function _validateRoute($route) + { + // Split on comma. + $domains = explode(',', trim($route)); + + foreach ($domains as $domain) { + $domain = str_replace('@', '', trim($domain)); + if (!$this->_validateDomain($domain)) return false; + } + + return $route; + } + + /** + * Function to validate a domain, though this is not quite what + * you expect of a strict internet domain. + * + * domain = sub-domain *("." sub-domain) + * + * @access private + * @param string $domain The string to check. + * @return mixed False on failure, or the validated domain on success. + */ + function _validateDomain($domain) + { + // Note the different use of $subdomains and $sub_domains + $subdomains = explode('.', $domain); + + while (count($subdomains) > 0) { + $sub_domains[] = $this->_splitCheck($subdomains, '.'); + for ($i = 0; $i < $this->index + 1; $i++) + array_shift($subdomains); + } + + foreach ($sub_domains as $sub_domain) { + if (!$this->_validateSubdomain(trim($sub_domain))) + return false; + } + + // Managed to get here, so return input. + return $domain; + } + + /** + * Function to validate a subdomain: + * subdomain = domain-ref / domain-literal + * + * @access private + * @param string $subdomain The string to check. + * @return boolean Success or failure. + */ + function _validateSubdomain($subdomain) + { + if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){ + if (!$this->_validateDliteral($arr[1])) return false; + } else { + if (!$this->_validateAtom($subdomain)) return false; + } + + // Got here, so return successful. + return true; + } + + /** + * Function to validate a domain literal: + * domain-literal = "[" *(dtext / quoted-pair) "]" + * + * @access private + * @param string $dliteral The string to check. + * @return boolean Success or failure. + */ + function _validateDliteral($dliteral) + { + return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\'; + } + + /** + * Function to validate an addr-spec. + * + * addr-spec = local-part "@" domain + * + * @access private + * @param string $addr_spec The string to check. + * @return mixed False on failure, or the validated addr-spec on success. + */ + function _validateAddrSpec($addr_spec) + { + $addr_spec = trim($addr_spec); + + // Split on @ sign if there is one. + if (strpos($addr_spec, '@') !== false) { + $parts = explode('@', $addr_spec); + $local_part = $this->_splitCheck($parts, '@'); + $domain = substr($addr_spec, strlen($local_part . '@')); + + // No @ sign so assume the default domain. + } else { + $local_part = $addr_spec; + $domain = $this->default_domain; + } + + if (($local_part = $this->_validateLocalPart($local_part)) === false) return false; + if (($domain = $this->_validateDomain($domain)) === false) return false; + + // Got here so return successful. + return array('local_part' => $local_part, 'domain' => $domain); + } + + /** + * Function to validate the local part of an address: + * local-part = word *("." word) + * + * @access private + * @param string $local_part + * @return mixed False on failure, or the validated local part on success. + */ + function _validateLocalPart($local_part) + { + $parts = explode('.', $local_part); + $words = array(); + + // Split the local_part into words. + while (count($parts) > 0){ + $words[] = $this->_splitCheck($parts, '.'); + for ($i = 0; $i < $this->index + 1; $i++) { + array_shift($parts); + } + } + + // Validate each word. + foreach ($words as $word) { + // If this word contains an unquoted space, it is invalid. (6.2.4) + if (strpos($word, ' ') && $word[0] !== '"') + { + return false; + } + + if ($this->_validatePhrase(trim($word)) === false) return false; + } + + // Managed to get here, so return the input. + return $local_part; + } + + /** + * Returns an approximate count of how many addresses are in the + * given string. This is APPROXIMATE as it only splits based on a + * comma which has no preceding backslash. Could be useful as + * large amounts of addresses will end up producing *large* + * structures when used with parseAddressList(). + * + * @param string $data Addresses to count + * @return int Approximate count + */ + function approximateCount($data) + { + return count(preg_split('/(?@. This can be sufficient for most + * people. Optional stricter mode can be utilised which restricts + * mailbox characters allowed to alphanumeric, full stop, hyphen + * and underscore. + * + * @param string $data Address to check + * @param boolean $strict Optional stricter mode + * @return mixed False if it fails, an indexed array + * username/domain if it matches + */ + function isValidInetAddress($data, $strict = false) + { + $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i'; + if (preg_match($regex, trim($data), $matches)) { + return array($matches[1], $matches[2]); + } else { + return 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, "z_RFC822 error: ". $message); + return false; + } +} \ No newline at end of file diff --git a/sources/index.php b/sources/index.php new file mode 100644 index 0000000..182b8a4 --- /dev/null +++ b/sources/index.php @@ -0,0 +1,295 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +ob_start(null, 1048576); + +// ignore user abortions because this can lead to weird errors - see ZP-239 +ignore_user_abort(true); + +include_once('lib/exceptions/exceptions.php'); +include_once('lib/utils/utils.php'); +include_once('lib/utils/compat.php'); +include_once('lib/utils/timezoneutil.php'); +include_once('lib/core/zpushdefs.php'); +include_once('lib/core/stateobject.php'); +include_once('lib/core/interprocessdata.php'); +include_once('lib/core/pingtracking.php'); +include_once('lib/core/topcollector.php'); +include_once('lib/core/loopdetection.php'); +include_once('lib/core/asdevice.php'); +include_once('lib/core/statemanager.php'); +include_once('lib/core/devicemanager.php'); +include_once('lib/core/zpush.php'); +include_once('lib/core/zlog.php'); +include_once('lib/core/paddingfilter.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('lib/core/streamer.php'); +include_once('lib/core/streamimporter.php'); +include_once('lib/core/synccollections.php'); +include_once('lib/core/hierarchycache.php'); +include_once('lib/core/changesmemorywrapper.php'); +include_once('lib/core/syncparameters.php'); +include_once('lib/core/bodypreference.php'); +include_once('lib/core/contentparameters.php'); +include_once('lib/wbxml/wbxmldefs.php'); +include_once('lib/wbxml/wbxmldecoder.php'); +include_once('lib/wbxml/wbxmlencoder.php'); +include_once('lib/syncobjects/syncobject.php'); +include_once('lib/syncobjects/syncbasebody.php'); +include_once('lib/syncobjects/syncbaseattachment.php'); +include_once('lib/syncobjects/syncmailflags.php'); +include_once('lib/syncobjects/syncrecurrence.php'); +include_once('lib/syncobjects/syncappointment.php'); +include_once('lib/syncobjects/syncappointmentexception.php'); +include_once('lib/syncobjects/syncattachment.php'); +include_once('lib/syncobjects/syncattendee.php'); +include_once('lib/syncobjects/syncmeetingrequestrecurrence.php'); +include_once('lib/syncobjects/syncmeetingrequest.php'); +include_once('lib/syncobjects/syncmail.php'); +include_once('lib/syncobjects/syncnote.php'); +include_once('lib/syncobjects/synccontact.php'); +include_once('lib/syncobjects/syncfolder.php'); +include_once('lib/syncobjects/syncprovisioning.php'); +include_once('lib/syncobjects/synctaskrecurrence.php'); +include_once('lib/syncobjects/synctask.php'); +include_once('lib/syncobjects/syncoofmessage.php'); +include_once('lib/syncobjects/syncoof.php'); +include_once('lib/syncobjects/syncuserinformation.php'); +include_once('lib/syncobjects/syncdeviceinformation.php'); +include_once('lib/syncobjects/syncdevicepassword.php'); +include_once('lib/syncobjects/syncitemoperationsattachment.php'); +include_once('lib/syncobjects/syncsendmail.php'); +include_once('lib/syncobjects/syncsendmailsource.php'); +include_once('lib/syncobjects/syncvalidatecert.php'); +include_once('lib/syncobjects/syncresolverecipients.php'); +include_once('lib/syncobjects/syncresolverecipient.php'); +include_once('lib/syncobjects/syncresolverecipientsoptions.php'); +include_once('lib/syncobjects/syncresolverecipientsavailability.php'); +include_once('lib/syncobjects/syncresolverecipientscertificates.php'); +include_once('lib/syncobjects/syncresolverecipientspicture.php'); +include_once('lib/default/backend.php'); +include_once('lib/default/searchprovider.php'); +include_once('lib/request/request.php'); +include_once('lib/request/requestprocessor.php'); + +include_once('config.php'); +include_once('version.php'); + + + // Attempt to set maximum execution time + ini_set('max_execution_time', SCRIPT_TIMEOUT); + set_time_limit(SCRIPT_TIMEOUT); + + try { + // check config & initialize the basics + ZPush::CheckConfig(); + Request::Initialize(); + ZLog::Initialize(); + + 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())); + + // Stop here if this is an OPTIONS request + if (Request::IsMethodOPTIONS()) + throw new NoPostRequestException("Options request", NoPostRequestException::OPTIONS_REQUEST); + + ZPush::CheckAdvancedConfig(); + + // Process request headers and look for AS headers + Request::ProcessHeaders(); + + // Check required GET parameters + if(Request::IsMethodPOST() && (Request::GetCommandCode() === false || !Request::GetDeviceID() || !Request::GetDeviceType())) + throw new FatalException("Requested the Z-Push URL without the required GET parameters"); + + // Load the backend + $backend = ZPush::GetBackend(); + + // always request the authorization header + if (! Request::AuthenticationInfo() || !Request::GetGETUser()) + throw new AuthenticationRequiredException("Access denied. Please send authorisation information"); + + // check the provisioning information + if (PROVISIONING === true && Request::IsMethodPOST() && ZPush::CommandNeedsProvisioning(Request::GetCommandCode()) && + ((Request::WasPolicyKeySent() && Request::GetPolicyKey() == 0) || ZPush::GetDeviceManager()->ProvisioningRequired(Request::GetPolicyKey())) && + (LOOSE_PROVISIONING === false || + (LOOSE_PROVISIONING === true && Request::WasPolicyKeySent()))) + //TODO for AS 14 send a wbxml response + throw new ProvisioningRequiredException(); + + // most commands require an authenticated user + if (ZPush::CommandNeedsAuthentication(Request::GetCommandCode())) + RequestProcessor::Authenticate(); + + // Do the actual processing of the request + if (Request::IsMethodGET()) + throw new NoPostRequestException("This is the Z-Push location and can only be accessed by Microsoft ActiveSync-capable devices", NoPostRequestException::GET_REQUEST); + + // Do the actual request + header(ZPush::GetServerHeader()); + + // announce the supported AS versions (if not already sent to device) + if (ZPush::GetDeviceManager()->AnnounceASVersion()) { + $versions = ZPush::GetSupportedProtocolVersions(true); + ZLog::Write(LOGLEVEL_INFO, sprintf("Announcing latest AS version to device: %s", $versions)); + header("X-MS-RP: ". $versions); + } + + RequestProcessor::Initialize(); + if(!RequestProcessor::HandleRequest()) + throw new WBXMLException(ZLog::GetWBXMLDebugInfo()); + + // eventually the RequestProcessor wants to send other headers to the mobile + foreach (RequestProcessor::GetSpecialHeaders() as $header) + header($header); + + // stream the data + $len = ob_get_length(); + $data = ob_get_contents(); + ob_end_clean(); + + // log amount of data transferred + // TODO check $len when streaming more data (e.g. Attachments), as the data will be send chunked + ZPush::GetDeviceManager()->SentData($len); + + // Unfortunately, even though Z-Push can stream the data to the client + // with a chunked encoding, using chunked encoding breaks the progress bar + // on the PDA. So the data is de-chunk here, written a content-length header and + // data send as a 'normal' packet. If the output packet exceeds 1MB (see ob_start) + // then it will be sent as a chunked packet anyway because PHP will have to flush + // the buffer. + if(!headers_sent()) + header("Content-Length: $len"); + + // send vnd.ms-sync.wbxml content type header if there is no content + // otherwise text/html content type is added which might break some devices + if ($len == 0) + header("Content-Type: application/vnd.ms-sync.wbxml"); + + print $data; + + // destruct backend after all data is on the stream + $backend->Logoff(); + } + + catch (NoPostRequestException $nopostex) { + if ($nopostex->getCode() == NoPostRequestException::OPTIONS_REQUEST) { + header(ZPush::GetServerHeader()); + header(ZPush::GetSupportedProtocolVersions()); + header(ZPush::GetSupportedCommands()); + ZLog::Write(LOGLEVEL_INFO, $nopostex->getMessage()); + } + else if ($nopostex->getCode() == NoPostRequestException::GET_REQUEST) { + if (Request::GetUserAgent()) + ZLog::Write(LOGLEVEL_INFO, sprintf("User-agent: '%s'", Request::GetUserAgent())); + if (!headers_sent() && $nopostex->showLegalNotice()) + ZPush::PrintZPushLegal('GET not supported', $nopostex->getMessage()); + } + } + + catch (Exception $ex) { + if (Request::GetUserAgent()) + ZLog::Write(LOGLEVEL_INFO, sprintf("User-agent: '%s'", Request::GetUserAgent())); + $exclass = get_class($ex); + + if(!headers_sent()) { + if ($ex instanceof ZPushException) { + header('HTTP/1.1 '. $ex->getHTTPCodeString()); + foreach ($ex->getHTTPHeaders() as $h) + header($h); + } + // something really unexpected happened! + else + header('HTTP/1.1 500 Internal Server Error'); + } + else + ZLog::Write(LOGLEVEL_FATAL, "Exception: ($exclass) - headers were already sent. Message: ". $ex->getMessage()); + + if ($ex instanceof AuthenticationRequiredException) { + ZPush::PrintZPushLegal($exclass, sprintf('
%s
',$ex->getMessage())); + + // log the failed login attemt e.g. for fail2ban + if (defined('LOGAUTHFAIL') && LOGAUTHFAIL != false) + ZLog::Write(LOGLEVEL_WARN, sprintf("IP: %s failed to authenticate user '%s'", Request::GetRemoteAddr(), Request::GetAuthUser()? Request::GetAuthUser(): Request::GetGETUser() )); + } + + // This could be a WBXML problem.. try to get the complete request + else if ($ex instanceof WBXMLException) { + ZLog::Write(LOGLEVEL_FATAL, "Request could not be processed correctly due to a WBXMLException. Please report this."); + } + + // Try to output some kind of error information. This is only possible if + // the output had not started yet. If it has started already, we can't show the user the error, and + // the device will give its own (useless) error message. + else if (!($ex instanceof ZPushException) || $ex->showLegalNotice()) { + $cmdinfo = (Request::GetCommand())? sprintf(" processing command %s", Request::GetCommand()): ""; + $extrace = $ex->getTrace(); + $trace = (!empty($extrace))? "\n\nTrace:\n". print_r($extrace,1):""; + ZPush::PrintZPushLegal($exclass . $cmdinfo, sprintf('
%s
',$ex->getMessage() . $trace)); + } + + // Announce exception to process loop detection + if (ZPush::GetDeviceManager(false)) + ZPush::GetDeviceManager()->AnnounceProcessException($ex); + + // Announce exception if the TopCollector if available + ZPush::GetTopCollector()->AnnounceInformation(get_class($ex), true); + } + + // save device data if the DeviceManager is available + if (ZPush::GetDeviceManager(false)) + ZPush::GetDeviceManager()->Save(); + + // end gracefully + ZLog::Write(LOGLEVEL_DEBUG, '-------- End'); +?> \ No newline at end of file diff --git a/sources/lib/core/asdevice.php b/sources/lib/core/asdevice.php new file mode 100644 index 0000000..37a0ab3 --- /dev/null +++ b/sources/lib/core/asdevice.php @@ -0,0 +1,690 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class ASDevice extends StateObject { + const UNDEFINED = -1; + // content data + const FOLDERUUID = 1; + const FOLDERTYPE = 2; + const FOLDERSUPPORTEDFIELDS = 3; + const FOLDERSYNCSTATUS = 4; + + // expected values for not set member variables + protected $unsetdata = array( + 'useragenthistory' => array(), + 'hierarchyuuid' => false, + 'contentdata' => array(), + 'wipestatus' => SYNC_PROVISION_RWSTATUS_NA, + 'wiperequestedby' => false, + 'wiperequestedon' => false, + 'wipeactionon' => false, + 'lastupdatetime' => 0, + 'conversationmode' => false, + 'policies' => array(), + 'policykey' => self::UNDEFINED, + 'forcesave' => false, + 'asversion' => false, + 'ignoredmessages' => array(), + 'announcedASversion' => false, + ); + + static private $loadedData; + protected $newdevice; + protected $hierarchyCache; + protected $ignoredMessageIds; + + /** + * AS Device constructor + * + * @param string $devid + * @param string $devicetype + * @param string $getuser + * @param string $useragent + * + * @access public + * @return + */ + public function ASDevice($devid, $devicetype, $getuser, $useragent) { + $this->deviceid = $devid; + $this->devicetype = $devicetype; + list ($this->deviceuser, $this->domain) = Utils::SplitDomainUser($getuser); + $this->useragent = $useragent; + $this->firstsynctime = time(); + $this->newdevice = true; + $this->ignoredMessageIds = array(); + } + + /** + * initializes the ASDevice with previousily saved data + * + * @param mixed $stateObject the StateObject containing the device data + * @param boolean $semanticUpdate indicates if data relevant for all users should be cross checked (e.g. wipe requests) + * + * @access public + * @return + */ + public function SetData($stateObject, $semanticUpdate = true) { + if (!($stateObject instanceof StateObject) || !isset($stateObject->devices) || !is_array($stateObject->devices)) return; + + // is information about this device & user available? + if (isset($stateObject->devices[$this->deviceuser]) && $stateObject->devices[$this->deviceuser] instanceof ASDevice) { + // overwrite local data with data from the saved object + $this->SetDataArray($stateObject->devices[$this->deviceuser]->GetDataArray()); + $this->newdevice = false; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ASDevice data loaded for user: '%s'", $this->deviceuser)); + } + + // check if RWStatus from another user on same device may require action + if ($semanticUpdate && count($stateObject->devices) > 1) { + foreach ($stateObject->devices as $user=>$asuserdata) { + if ($user == $this->user) continue; + + // another user has a required action on this device + if (isset($asuserdata->wipeStatus) && $asuserdata->wipeStatus > SYNC_PROVISION_RWSTATUS_OK) { + ZLog::Write(LOGLEVEL_INFO, sprintf("User '%s' has requested a remote wipe for this device on '%s'", $asuserdata->wipeRequestBy, strftime("%Y-%m-%d %H:%M", $asuserdata->wipeRequstOn))); + + // reset status to PENDING if wipe was executed before + $this->wipeStatus = ($asuserdata->wipeStatus & SYNC_PROVISION_RWSTATUS_WIPED)?SYNC_PROVISION_RWSTATUS_PENDING:$asuserdata->wipeStatus; + $this->wipeRequestBy = $asuserdata->wipeRequestBy; + $this->wipeRequestOn = $asuserdata->wipeRequestOn; + $this->wipeActionOn = $asuserdata->wipeActionOn; + break; + } + } + } + + self::$loadedData = $stateObject; + $this->changed = false; + } + + /** + * Returns the current AS Device in it's StateObject + * If the data was not changed, it returns false (no need to update any data) + * + * @access public + * @return array/boolean + */ + public function GetData() { + if (! $this->changed) + return false; + + // device was updated + $this->lastupdatetime = time(); + unset($this->ignoredMessageIds); + + if (!isset(self::$loadedData) || !isset(self::$loadedData->devices) || !is_array(self::$loadedData->devices)) { + self::$loadedData = new StateObject(); + $devices = array(); + } + else + $devices = self::$loadedData->devices; + + $devices[$this->deviceuser] = $this; + + // check if RWStatus has to be updated so it can be updated for other users on same device + if (isset($this->wipeStatus) && $this->wipeStatus > SYNC_PROVISION_RWSTATUS_OK) { + foreach ($devices as $user=>$asuserdata) { + if ($user == $this->deviceuser) continue; + if (isset($this->wipeStatus)) $asuserdata->wipeStatus = $this->wipeStatus; + if (isset($this->wipeRequestBy)) $asuserdata->wipeRequestBy = $this->wipeRequestBy; + if (isset($this->wipeRequestOn)) $asuserdata->wipeRequestOn = $this->wipeRequestOn; + if (isset($this->wipeActionOn)) $asuserdata->wipeActionOn = $this->wipeActionOn; + $devices[$user] = $asuserdata; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Updated remote wipe status for user '%s' on the same device", $user)); + } + } + self::$loadedData->devices = $devices; + return self::$loadedData; + } + + /** + * Removes internal data from the object, so this data can not be exposed + * + * @access public + * @return boolean + */ + public function StripData() { + unset($this->changed); + unset($this->unsetdata); + unset($this->hierarchyCache); + unset($this->forceSave); + unset($this->newdevice); + unset($this->ignoredMessageIds); + + if (isset($this->ignoredmessages) && is_array($this->ignoredmessages)) { + $imessages = $this->ignoredmessages; + $unserializedMessage = array(); + foreach ($imessages as $im) { + $im->asobject = unserialize($im->asobject); + $im->asobject->StripData(); + $unserializedMessage[] = $im; + } + $this->ignoredmessages = $unserializedMessage; + } + + return true; + } + + /** + * Indicates if the object was just created + * + * @access public + * @return boolean + */ + public function IsNewDevice() { + return (isset($this->newdevice) && $this->newdevice === true); + } + + + /**---------------------------------------------------------------------------------------------------------- + * Non-standard Getter and Setter + */ + + /** + * Returns the user agent of this device + * + * @access public + * @return string + */ + public function GetDeviceUserAgent() { + if (!isset($this->useragent) || !$this->useragent) + return "unknown"; + + return $this->useragent; + } + + /** + * Returns the user agent history of this device + * + * @access public + * @return string + */ + public function GetDeviceUserAgentHistory() { + return $this->useragentHistory; + } + + /** + * Sets the useragent of the current request + * If this value is alreay available, no update is done + * + * @param string $useragent + * + * @access public + * @return boolean + */ + public function SetUserAgent($useragent) { + if ($useragent == $this->useragent || $useragent === false || $useragent === Request::UNKNOWN) + return true; + + // save the old user agent, if available + if ($this->useragent != "") { + // [] = changedate, previous user agent + $a = $this->useragentHistory; + + // only add if this agent was not seen before + if (! in_array(array(true, $this->useragent), $a)) { + $a[] = array(time(), $this->useragent); + $this->useragentHistory = $a; + } + } + $this->useragent = $useragent; + return true; + } + + /** + * Sets the current remote wipe status + * + * @param int $status + * @param string $requestedBy + * @access public + * @return int + */ + public function SetWipeStatus($status, $requestedBy = false) { + // force saving the updated information if there was a transition between the wiping status + if ($this->wipeStatus > SYNC_PROVISION_RWSTATUS_OK && $status > SYNC_PROVISION_RWSTATUS_OK) + $this->forceSave = true; + + if ($requestedBy != false) { + $this->wipeRequestedBy = $requestedBy; + $this->wipeRequestedOn = time(); + } + else { + $this->wipeActionOn = time(); + } + + $this->wipeStatus = $status; + + if ($this->wipeStatus > SYNC_PROVISION_RWSTATUS_PENDING) + ZLog::Write(LOGLEVEL_INFO, sprintf("ASDevice id '%s' was %s remote wiped on %s. Action requested by user '%s' on %s", + $this->deviceid, ($this->wipeStatus == SYNC_PROVISION_RWSTATUS_REQUESTED ? "requested to be": "sucessfully"), + strftime("%Y-%m-%d %H:%M", $this->wipeActionOn), $this->wipeRequestedBy, strftime("%Y-%m-%d %H:%M", $this->wipeRequestedOn))); + } + + /** + * Sets the deployed policy key + * + * @param int $policykey + * + * @access public + * @return + */ + public function SetPolicyKey($policykey) { + $this->policykey = $policykey; + if ($this->GetWipeStatus() == SYNC_PROVISION_RWSTATUS_NA) + $this->wipeStatus = SYNC_PROVISION_RWSTATUS_OK; + } + + /** + * Adds a messages which was ignored to the device data + * + * @param StateObject $ignoredMessage + * + * @access public + * @return boolean + */ + public function AddIgnoredMessage($ignoredMessage) { + // we should have all previousily ignored messages in an id array + if (count($this->ignoredMessages) != count($this->ignoredMessageIds)) { + foreach($this->ignoredMessages as $oldMessage) { + if (!isset($this->ignoredMessageIds[$oldMessage->folderid])) + $this->ignoredMessageIds[$oldMessage->folderid] = array(); + $this->ignoredMessageIds[$oldMessage->folderid][] = $oldMessage->id; + } + } + + // serialize the AS object - if available + if (isset($ignoredMessage->asobject)) + $ignoredMessage->asobject = serialize($ignoredMessage->asobject); + + // try not to add the same message several times + if (isset($ignoredMessage->folderid) && isset($ignoredMessage->id)) { + if (!isset($this->ignoredMessageIds[$ignoredMessage->folderid])) + $this->ignoredMessageIds[$ignoredMessage->folderid] = array(); + + if (in_array($ignoredMessage->id, $this->ignoredMessageIds[$ignoredMessage->folderid])) + $this->RemoveIgnoredMessage($ignoredMessage->folderid, $ignoredMessage->id); + + $this->ignoredMessageIds[$ignoredMessage->folderid][] = $ignoredMessage->id; + $msges = $this->ignoredMessages; + $msges[] = $ignoredMessage; + $this->ignoredMessages = $msges; + + return true; + } + else { + $msges = $this->ignoredMessages; + $msges[] = $ignoredMessage; + $this->ignoredMessages = $msges; + ZLog::Write(LOGLEVEL_WARN, "ASDevice->AddIgnoredMessage(): added message has no folder/id"); + return true; + } + } + + /** + * Removes message in the list of ignored messages + * + * @param string $folderid parent folder id of the message + * @param string $id message id + * + * @access public + * @return boolean + */ + public function RemoveIgnoredMessage($folderid, $id) { + // we should have all previousily ignored messages in an id array + if (count($this->ignoredMessages) != count($this->ignoredMessageIds)) { + foreach($this->ignoredMessages as $oldMessage) { + if (!isset($this->ignoredMessageIds[$oldMessage->folderid])) + $this->ignoredMessageIds[$oldMessage->folderid] = array(); + $this->ignoredMessageIds[$oldMessage->folderid][] = $oldMessage->id; + } + } + + $foundMessage = false; + // there are ignored messages in that folder + if (isset($this->ignoredMessageIds[$folderid])) { + // resync of a folder.. we should remove all previousily ignored messages + if ($id === false || in_array($id, $this->ignoredMessageIds[$folderid], true)) { + $ignored = $this->ignoredMessages; + $newMessages = array(); + foreach ($ignored as $im) { + if ($im->folderid == $folderid) { + if ($id === false || $im->id === $id) { + $foundMessage = true; + if (count($this->ignoredMessageIds[$folderid]) == 1) { + unset($this->ignoredMessageIds[$folderid]); + } + else { + unset($this->ignoredMessageIds[$folderid][array_search($id, $this->ignoredMessageIds[$folderid])]); + } + continue; + } + else + $newMessages[] = $im; + } + } + $this->ignoredMessages = $newMessages; + } + } + + return $foundMessage; + } + + /** + * Indicates if a message is in the list of ignored messages + * + * @param string $folderid parent folder id of the message + * @param string $id message id + * + * @access public + * @return boolean + */ + public function HasIgnoredMessage($folderid, $id) { + // we should have all previousily ignored messages in an id array + if (count($this->ignoredMessages) != count($this->ignoredMessageIds)) { + foreach($this->ignoredMessages as $oldMessage) { + if (!isset($this->ignoredMessageIds[$oldMessage->folderid])) + $this->ignoredMessageIds[$oldMessage->folderid] = array(); + $this->ignoredMessageIds[$oldMessage->folderid][] = $oldMessage->id; + } + } + + $foundMessage = false; + // there are ignored messages in that folder + if (isset($this->ignoredMessageIds[$folderid])) { + // resync of a folder.. we should remove all previousily ignored messages + if ($id === false || in_array($id, $this->ignoredMessageIds[$folderid], true)) { + $foundMessage = true; + } + } + + return $foundMessage; + } + + /**---------------------------------------------------------------------------------------------------------- + * HierarchyCache and ContentData operations + */ + + /** + * Sets the HierarchyCache + * The hierarchydata, can be: + * - false a new HierarchyCache is initialized + * - array() new HierarchyCache is initialized and data from GetHierarchy is loaded + * - string previousely serialized data is loaded + * + * @param string $hierarchydata (opt) + * + * @access public + * @return boolean + */ + public function SetHierarchyCache($hierarchydata = false) { + if ($hierarchydata !== false && $hierarchydata instanceof ChangesMemoryWrapper) { + $this->hierarchyCache = $hierarchydata; + $this->hierarchyCache->CopyOldState(); + } + else + $this->hierarchyCache = new ChangesMemoryWrapper(); + + if (is_array($hierarchydata)) + return $this->hierarchyCache->ImportFolders($hierarchydata); + return true; + } + + /** + * Returns serialized data of the HierarchyCache + * + * @access public + * @return string + */ + public function GetHierarchyCacheData() { + if (isset($this->hierarchyCache)) + return $this->hierarchyCache; + + ZLog::Write(LOGLEVEL_WARN, "ASDevice->GetHierarchyCacheData() has no data! HierarchyCache probably never initialized."); + return false; + } + + /** + * Returns the HierarchyCache Object + * + * @access public + * @return object HierarchyCache + */ + public function GetHierarchyCache() { + if (!isset($this->hierarchyCache)) + $this->SetHierarchyCache(); + + ZLog::Write(LOGLEVEL_DEBUG, "ASDevice->GetHierarchyCache(): ". $this->hierarchyCache->GetStat()); + return $this->hierarchyCache; + } + + /** + * Returns all known folderids + * + * @access public + * @return array + */ + public function GetAllFolderIds() { + if (isset($this->contentData) && is_array($this->contentData)) + return array_keys($this->contentData); + return array(); + } + + /** + * Returns a linked UUID for a folder id + * + * @param string $folderid (opt) if not set, Hierarchy UUID is returned + * + * @access public + * @return string + */ + public function GetFolderUUID($folderid = false) { + if ($folderid === false) + return (isset($this->hierarchyUuid) && $this->hierarchyUuid !== self::UNDEFINED) ? $this->hierarchyUuid : false; + else if (isset($this->contentData) && isset($this->contentData[$folderid]) && isset($this->contentData[$folderid][self::FOLDERUUID])) + return $this->contentData[$folderid][self::FOLDERUUID]; + return false; + } + + /** + * Link a UUID to a folder id + * If a boolean false UUID is sent, the relation is removed + * + * @param string $uuid + * @param string $folderid (opt) if not set Hierarchy UUID is linked + * + * @access public + * @return boolean + */ + public function SetFolderUUID($uuid, $folderid = false) { + if ($folderid === false) { + $this->hierarchyUuid = $uuid; + // when unsetting the hierarchycache, also remove saved contentdata and ignoredmessages + if ($folderid === false) { + $this->contentData = array(); + $this->ignoredMessageIds = array(); + $this->ignoredMessages = array(); + } + } + else { + + $contentData = $this->contentData; + if (!isset($contentData[$folderid]) || !is_array($contentData[$folderid])) + $contentData[$folderid] = array(); + + // check if the foldertype is set. This has to be available at this point, as generated during the first HierarchySync + if (!isset($contentData[$folderid][self::FOLDERTYPE])) + return false; + + if ($uuid) + $contentData[$folderid][self::FOLDERUUID] = $uuid; + else + $contentData[$folderid][self::FOLDERUUID] = false; + + $this->contentData = $contentData; + } + } + + /** + * Returns a foldertype for a folder already known to the mobile + * + * @param string $folderid + * + * @access public + * @return int/boolean returns false if the type is not set + */ + public function GetFolderType($folderid) { + if (isset($this->contentData) && isset($this->contentData[$folderid]) && + isset($this->contentData[$folderid][self::FOLDERTYPE]) ) + + return $this->contentData[$folderid][self::FOLDERTYPE]; + return false; + } + + /** + * Sets the foldertype of a folder id + * + * @param string $uuid + * @param string $folderid (opt) if not set Hierarchy UUID is linked + * + * @access public + * @return boolean true if the type was set or updated + */ + public function SetFolderType($folderid, $foldertype) { + $contentData = $this->contentData; + + if (!isset($contentData[$folderid]) || !is_array($contentData[$folderid])) + $contentData[$folderid] = array(); + if (!isset($contentData[$folderid][self::FOLDERTYPE]) || $contentData[$folderid][self::FOLDERTYPE] != $foldertype ) { + $contentData[$folderid][self::FOLDERTYPE] = $foldertype; + $this->contentData = $contentData; + return true; + } + return false; + } + + /** + * Gets the supported fields transmitted previousely by the device + * for a certain folder + * + * @param string $folderid + * + * @access public + * @return array/boolean false means no supportedFields are available + */ + public function GetSupportedFields($folderid) { + if (isset($this->contentData) && isset($this->contentData[$folderid]) && + isset($this->contentData[$folderid][self::FOLDERUUID]) && $this->contentData[$folderid][self::FOLDERUUID] !== false && + isset($this->contentData[$folderid][self::FOLDERSUPPORTEDFIELDS]) ) + + return $this->contentData[$folderid][self::FOLDERSUPPORTEDFIELDS]; + + return false; + } + + /** + * Sets the set of supported fields transmitted by the device for a certain folder + * + * @param string $folderid + * @param array $fieldlist supported fields + * + * @access public + * @return boolean + */ + public function SetSupportedFields($folderid, $fieldlist) { + $contentData = $this->contentData; + if (!isset($contentData[$folderid]) || !is_array($contentData[$folderid])) + $contentData[$folderid] = array(); + + $contentData[$folderid][self::FOLDERSUPPORTEDFIELDS] = $fieldlist; + $this->contentData = $contentData; + return true; + } + + /** + * Gets the current sync status of a certain folder + * + * @param string $folderid + * + * @access public + * @return mixed/boolean false means the status is not available + */ + public function GetFolderSyncStatus($folderid) { + if (isset($this->contentData) && isset($this->contentData[$folderid]) && + isset($this->contentData[$folderid][self::FOLDERUUID]) && $this->contentData[$folderid][self::FOLDERUUID] !== false && + isset($this->contentData[$folderid][self::FOLDERSYNCSTATUS]) ) + + return $this->contentData[$folderid][self::FOLDERSYNCSTATUS]; + + return false; + } + + /** + * Sets the current sync status of a certain folder + * + * @param string $folderid + * @param mixed $status if set to false the current status is deleted + * + * @access public + * @return boolean + */ + public function SetFolderSyncStatus($folderid, $status) { + $contentData = $this->contentData; + if (!isset($contentData[$folderid]) || !is_array($contentData[$folderid])) + $contentData[$folderid] = array(); + + if ($status !== false) { + $contentData[$folderid][self::FOLDERSYNCSTATUS] = $status; + } + else if (isset($contentData[$folderid][self::FOLDERSYNCSTATUS])) { + unset($contentData[$folderid][self::FOLDERSYNCSTATUS]); + } + + $this->contentData = $contentData; + return true; + } + +} + +?> \ No newline at end of file diff --git a/sources/lib/core/bodypreference.php b/sources/lib/core/bodypreference.php new file mode 100644 index 0000000..057d8f7 --- /dev/null +++ b/sources/lib/core/bodypreference.php @@ -0,0 +1,68 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class BodyPreference extends StateObject { + protected $unsetdata = array( 'truncationsize' => false, + 'allornone' => false, + 'preview' => false, + ); + + /** + * expected magic getters and setters + * + * GetTruncationSize() + SetTruncationSize() + * GetAllOrNone() + SetAllOrNone() + * GetPreview() + SetPreview() + */ + + /** + * Indicates if this object has values + * + * @access public + * @return boolean + */ + public function HasValues() { + return (count($this->data) > 0); + } +} +?> \ No newline at end of file diff --git a/sources/lib/core/changesmemorywrapper.php b/sources/lib/core/changesmemorywrapper.php new file mode 100644 index 0000000..057f029 --- /dev/null +++ b/sources/lib/core/changesmemorywrapper.php @@ -0,0 +1,349 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class ChangesMemoryWrapper extends HierarchyCache implements IImportChanges, IExportChanges { + const CHANGE = 1; + const DELETION = 2; + + private $changes; + private $step; + private $destinationImporter; + private $exportImporter; + + /** + * Constructor + * + * @access public + * @return + */ + public function ChangesMemoryWrapper() { + $this->changes = array(); + $this->step = 0; + parent::HierarchyCache(); + } + + /** + * Only used to load additional folder sync information for hierarchy changes + * + * @param array $state current state of additional hierarchy folders + * + * @access public + * @return boolean + */ + public function Config($state, $flags = 0) { + // we should never forward this changes to a backend + if (!isset($this->destinationImporter)) { + foreach($state as $addKey => $addFolder) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->Config(AdditionalFolders) : process folder '%s'", $addFolder->displayname)); + if (isset($addFolder->NoBackendFolder) && $addFolder->NoBackendFolder == true) { + $hasRights = ZPush::GetBackend()->Setup($addFolder->Store, true, $addFolder->serverid); + // delete the folder on the device + if (! $hasRights) { + // delete the folder only if it was an additional folder before, else ignore it + $synchedfolder = $this->GetFolder($addFolder->serverid); + if (isset($synchedfolder->NoBackendFolder) && $synchedfolder->NoBackendFolder == true) + $this->ImportFolderDeletion($addFolder->serverid, $addFolder->parentid); + continue; + } + } + // add folder to the device - if folder is already on the device, nothing will happen + $this->ImportFolderChange($addFolder); + } + + // look for folders which are currently on the device if there are now not to be synched anymore + $alreadyDeleted = $this->GetDeletedFolders(); + foreach ($this->ExportFolders(true) as $sid => $folder) { + // we are only looking at additional folders + if (isset($folder->NoBackendFolder)) { + // look if this folder is still in the list of additional folders and was not already deleted (e.g. missing permissions) + if (!array_key_exists($sid, $state) && !array_key_exists($sid, $alreadyDeleted)) { + ZLog::Write(LOGLEVEL_INFO, sprintf("ChangesMemoryWrapper->Config(AdditionalFolders) : previously synchronized folder '%s' is not to be synched anymore. Sending delete to mobile.", $folder->displayname)); + $this->ImportFolderDeletion($folder->serverid, $folder->parentid); + } + } + } + } + return true; + } + + + /** + * Implement interfaces which are never used + */ + public function GetState() { return false;} + public function LoadConflicts($contentparameters, $state) { return true; } + public function ConfigContentParameters($contentparameters) { return true; } + public function ImportMessageReadFlag($id, $flags) { return true; } + public function ImportMessageMove($id, $newfolder) { return true; } + + /**---------------------------------------------------------------------------------------------------------- + * IImportChanges & destination importer + */ + + /** + * Sets an importer where incoming changes should be sent to + * + * @param IImportChanges $importer message to be changed + * + * @access public + * @return boolean + */ + public function SetDestinationImporter(&$importer) { + $this->destinationImporter = $importer; + } + + /** + * Imports a message change, which is imported into memory + * + * @param string $id id of message which is changed + * @param SyncObject $message message to be changed + * + * @access public + * @return boolean + */ + public function ImportMessageChange($id, $message) { + $this->changes[] = array(self::CHANGE, $id); + return true; + } + + /** + * Imports a message deletion, which is imported into memory + * + * @param string $id id of message which is deleted + * + * @access public + * @return boolean + */ + public function ImportMessageDeletion($id) { + $this->changes[] = array(self::DELETION, $id); + return true; + } + + /** + * Checks if a message id is flagged as changed + * + * @param string $id message id + * + * @access public + * @return boolean + */ + public function IsChanged($id) { + return (array_search(array(self::CHANGE, $id), $this->changes) === false) ? false:true; + } + + /** + * Checks if a message id is flagged as deleted + * + * @param string $id message id + * + * @access public + * @return boolean + */ + public function IsDeleted($id) { + return (array_search(array(self::DELETION, $id), $this->changes) === false) ? false:true; + } + + /** + * Imports a folder change + * + * @param SyncFolder $folder folder to be changed + * + * @access public + * @return boolean + */ + public function ImportFolderChange($folder) { + // if the destinationImporter is set, then this folder should be processed by another importer + // instead of being loaded in memory. + if (isset($this->destinationImporter)) { + // normally the $folder->type is not set, but we need this value to check if the change operation is permitted + // e.g. system folders can normally not be changed - set the type from cache and let the destinationImporter decide + if (!isset($folder->type)) { + $cacheFolder = $this->GetFolder($folder->serverid); + $folder->type = $cacheFolder->type; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->ImportFolderChange(): Set foldertype for folder '%s' from cache as it was not sent: '%s'", $folder->displayname, $folder->type)); + } + + $ret = $this->destinationImporter->ImportFolderChange($folder); + + // if the operation was sucessfull, update the HierarchyCache + if ($ret) { + // for folder creation, the serverid is not set and has to be updated before + if (!isset($folder->serverid) || $folder->serverid == "") + $folder->serverid = $ret; + + $this->AddFolder($folder); + } + return $ret; + } + // load into memory + else { + if (isset($folder->serverid)) { + // The Zarafa HierarchyExporter exports all kinds of changes for folders (e.g. update no. of unread messages in a folder). + // These changes are not relevant for the mobiles, as something changes but the relevant displayname and parentid + // stay the same. These changes will be dropped and are not sent! + $cacheFolder = $this->GetFolder($folder->serverid); + if ($folder->equals($this->GetFolder($folder->serverid))) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->ImportFolderChange(): Change for folder '%s' will not be sent as modification is not relevant.", $folder->displayname)); + return false; + } + + // load this change into memory + $this->changes[] = array(self::CHANGE, $folder); + + // HierarchyCache: already add/update the folder so changes are not sent twice (if exported twice) + $this->AddFolder($folder); + return true; + } + return false; + } + } + + /** + * Imports a folder deletion + * + * @param string $id + * @param string $parent (opt) the parent id of the folders + * + * @access public + * @return boolean + */ + public function ImportFolderDeletion($id, $parent = false) { + // if the forwarder is set, then this folder should be processed by another importer + // instead of being loaded in mem. + if (isset($this->destinationImporter)) { + $ret = $this->destinationImporter->ImportFolderDeletion($id, $parent); + + // if the operation was sucessfull, update the HierarchyCache + if ($ret) + $this->DelFolder($id); + + return $ret; + } + else { + // if this folder is not in the cache, the change does not need to be streamed to the mobile + if ($this->GetFolder($id)) { + + // load this change into memory + $this->changes[] = array(self::DELETION, $id, $parent); + + // HierarchyCache: delete the folder so changes are not sent twice (if exported twice) + $this->DelFolder($id); + return true; + } + } + } + + + /**---------------------------------------------------------------------------------------------------------- + * IExportChanges & destination importer + */ + + /** + * Initializes the Exporter where changes are synchronized to + * + * @param IImportChanges $importer + * + * @access public + * @return boolean + */ + public function InitializeExporter(&$importer) { + $this->exportImporter = $importer; + $this->step = 0; + return true; + } + + /** + * Returns the amount of changes to be exported + * + * @access public + * @return int + */ + public function GetChangeCount() { + return count($this->changes); + } + + /** + * Synchronizes a change. Only HierarchyChanges will be Synchronized() + * + * @access public + * @return array + */ + public function Synchronize() { + if($this->step < count($this->changes) && isset($this->exportImporter)) { + + $change = $this->changes[$this->step]; + + if ($change[0] == self::CHANGE) { + if (! $this->GetFolder($change[1]->serverid, true)) + $change[1]->flags = SYNC_NEWMESSAGE; + + $this->exportImporter->ImportFolderChange($change[1]); + } + // deletion + else { + $this->exportImporter->ImportFolderDeletion($change[1], $change[2]); + } + $this->step++; + + // return progress array + return array("steps" => count($this->changes), "progress" => $this->step); + } + else + return false; + } + + /** + * Initializes a few instance variables + * called after unserialization + * + * @access public + * @return array + */ + public function __wakeup() { + $this->changes = array(); + $this->step = 0; + } +} + +?> \ No newline at end of file diff --git a/sources/lib/core/contentparameters.php b/sources/lib/core/contentparameters.php new file mode 100644 index 0000000..8d68ff1 --- /dev/null +++ b/sources/lib/core/contentparameters.php @@ -0,0 +1,141 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class ContentParameters extends StateObject { + protected $unsetdata = array( 'contentclass' => false, + 'foldertype' => '', + 'conflict' => false, + 'deletesasmoves' => true, + 'filtertype' => false, + 'truncation' => false, + 'rtftruncation' => false, + 'mimesupport' => false, + 'conversationmode' => false, + ); + + private $synckeyChanged = false; + + /** + * Expected magic getters and setters + * + * GetContentClass() + SetContentClass() + * GetConflict() + SetConflict() + * GetDeletesAsMoves() + SetDeletesAsMoves() + * GetFilterType() + SetFilterType() + * GetTruncation() + SetTruncation + * GetRTFTruncation() + SetRTFTruncation() + * GetMimeSupport () + SetMimeSupport() + * GetMimeTruncation() + SetMimeTruncation() + * GetConversationMode() + SetConversationMode() + */ + + /** + * Overwrite StateObject->__call so we are able to handle ContentParameters->BodyPreference() + * + * @access public + * @return mixed + */ + public function __call($name, $arguments) { + if ($name === "BodyPreference") + return $this->BodyPreference($arguments[0]); + + return parent::__call($name, $arguments); + } + + + /** + * Instantiates/returns the bodypreference object for a type + * + * @param int $type + * + * @access public + * @return int/boolean returns false if value is not defined + */ + public function BodyPreference($type) { + if (!isset($this->bodypref)) + $this->bodypref = array(); + + if (isset($this->bodypref[$type])) + return $this->bodypref[$type]; + else { + $asb = new BodyPreference(); + $arr = (array)$this->bodypref; + $arr[$type] = $asb; + $this->bodypref = $arr; + return $asb; + } + } + + /** + * Returns available body preference objects + * + * @access public + * @return array/boolean returns false if the client's body preference is not available + */ + public function GetBodyPreference() { + if (!isset($this->bodypref) || !(is_array($this->bodypref) || empty($this->bodypref))) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ContentParameters->GetBodyPreference(): bodypref is empty or not set")); + return false; + } + return array_keys($this->bodypref); + } + + /** + * Called before the StateObject is serialized + * + * @access protected + * @return boolean + */ + protected function preSerialize() { + parent::preSerialize(); + + if ($this->changed === true && $this->synckeyChanged) + $this->lastsynctime = time(); + + return true; + } +} + +?> \ No newline at end of file diff --git a/sources/lib/core/devicemanager.php b/sources/lib/core/devicemanager.php new file mode 100644 index 0000000..9654963 --- /dev/null +++ b/sources/lib/core/devicemanager.php @@ -0,0 +1,841 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class DeviceManager { + // broken message indicators + const MSG_BROKEN_UNKNOWN = 1; + const MSG_BROKEN_CAUSINGLOOP = 2; + const MSG_BROKEN_SEMANTICERR = 4; + + const FLD_SYNC_INITIALIZED = 1; + const FLD_SYNC_INPROGRESS = 2; + const FLD_SYNC_COMPLETED = 4; + + private $device; + private $deviceHash; + private $statemachine; + private $stateManager; + private $incomingData = 0; + private $outgoingData = 0; + + private $windowSize; + private $latestFolder; + + private $loopdetection; + private $hierarchySyncRequired; + + /** + * Constructor + * + * @access public + */ + public function DeviceManager() { + $this->statemachine = ZPush::GetStateMachine(); + $this->deviceHash = false; + $this->devid = Request::GetDeviceID(); + $this->windowSize = array(); + $this->latestFolder = false; + $this->hierarchySyncRequired = false; + + // only continue if deviceid is set + if ($this->devid) { + $this->device = new ASDevice($this->devid, Request::GetDeviceType(), Request::GetGETUser(), Request::GetUserAgent()); + $this->loadDeviceData(); + + ZPush::GetTopCollector()->SetUserAgent($this->device->GetDeviceUserAgent()); + } + else + throw new FatalNotImplementedException("Can not proceed without a device id."); + + $this->loopdetection = new LoopDetection(); + $this->loopdetection->ProcessLoopDetectionInit(); + $this->loopdetection->ProcessLoopDetectionPreviousConnectionFailed(); + + $this->stateManager = new StateManager(); + $this->stateManager->SetDevice($this->device); + } + + /** + * Returns the StateManager for the current device + * + * @access public + * @return StateManager + */ + public function GetStateManager() { + return $this->stateManager; + } + + /**---------------------------------------------------------------------------------------------------------- + * Device operations + */ + + /** + * Announces amount of transmitted data to the DeviceManager + * + * @param int $datacounter + * + * @access public + * @return boolean + */ + public function SentData($datacounter) { + // TODO save this somewhere + $this->incomingData = Request::GetContentLength(); + $this->outgoingData = $datacounter; + } + + /** + * Called at the end of the request + * Statistics about received/sent data is saved here + * + * @access public + * @return boolean + */ + public function Save() { + // TODO save other stuff + + // check if previousily ignored messages were synchronized for the current folder + // on multifolder operations of AS14 this is done by setLatestFolder() + if ($this->latestFolder !== false) + $this->checkBrokenMessages($this->latestFolder); + + // update the user agent and AS version on the device + $this->device->SetUserAgent(Request::GetUserAgent()); + $this->device->SetASVersion(Request::GetProtocolVersion()); + + // data to be saved + $data = $this->device->GetData(); + if ($data && Request::IsValidDeviceID()) { + ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->Save(): Device data changed"); + + try { + // check if this is the first time the device data is saved and it is authenticated. If so, link the user to the device id + if ($this->device->IsNewDevice() && RequestProcessor::isUserAuthenticated()) { + ZLog::Write(LOGLEVEL_INFO, sprintf("Linking device ID '%s' to user '%s'", $this->devid, $this->device->GetDeviceUser())); + $this->statemachine->LinkUserDevice($this->device->GetDeviceUser(), $this->devid); + } + + if (RequestProcessor::isUserAuthenticated() || $this->device->GetForceSave() ) { + $this->statemachine->SetState($data, $this->devid, IStateMachine::DEVICEDATA); + ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->Save(): Device data saved"); + } + } + catch (StateNotFoundException $snfex) { + ZLog::Write(LOGLEVEL_ERROR, "DeviceManager->Save(): Exception: ". $snfex->getMessage()); + } + } + + // remove old search data + $oldpid = $this->loopdetection->ProcessLoopDetectionGetOutdatedSearchPID(); + if ($oldpid) { + ZPush::GetBackend()->GetSearchProvider()->TerminateSearch($oldpid); + } + + // we terminated this process + if ($this->loopdetection) + $this->loopdetection->ProcessLoopDetectionTerminate(); + + return true; + } + + /** + * Newer mobiles send extensive device informations with the Settings command + * These informations are saved in the ASDevice + * + * @param SyncDeviceInformation $deviceinformation + * + * @access public + * @return boolean + */ + public function SaveDeviceInformation($deviceinformation) { + ZLog::Write(LOGLEVEL_DEBUG, "Saving submitted device information"); + + // set the user agent + if (isset($deviceinformation->useragent)) + $this->device->SetUserAgent($deviceinformation->useragent); + + // save other informations + foreach (array("model", "imei", "friendlyname", "os", "oslanguage", "phonenumber", "mobileoperator", "enableoutboundsms") as $info) { + if (isset($deviceinformation->$info) && $deviceinformation->$info != "") { + $this->device->__set("device".$info, $deviceinformation->$info); + } + } + return true; + } + + /**---------------------------------------------------------------------------------------------------------- + * Provisioning operations + */ + + /** + * Checks if the sent policykey matches the latest policykey + * saved for the device + * + * @param string $policykey + * @param boolean $noDebug (opt) by default, debug message is shown + * + * @access public + * @return boolean + */ + public function ProvisioningRequired($policykey, $noDebug = false) { + $this->loadDeviceData(); + + // check if a remote wipe is required + if ($this->device->GetWipeStatus() > SYNC_PROVISION_RWSTATUS_OK) { + ZLog::Write(LOGLEVEL_INFO, sprintf("DeviceManager->ProvisioningRequired('%s'): YES, remote wipe requested", $policykey)); + return true; + } + + $p = ( ($this->device->GetWipeStatus() != SYNC_PROVISION_RWSTATUS_NA && $policykey != $this->device->GetPolicyKey()) || + Request::WasPolicyKeySent() && $this->device->GetPolicyKey() == ASDevice::UNDEFINED ); + if (!$noDebug || $p) + ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->ProvisioningRequired('%s') saved device key '%s': %s", $policykey, $this->device->GetPolicyKey(), Utils::PrintAsString($p))); + return $p; + } + + /** + * Generates a new Policykey + * + * @access public + * @return int + */ + public function GenerateProvisioningPolicyKey() { + return mt_rand(100000000, 999999999); + } + + /** + * Attributes a provisioned policykey to a device + * + * @param int $policykey + * + * @access public + * @return boolean status + */ + public function SetProvisioningPolicyKey($policykey) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->SetPolicyKey('%s')", $policykey)); + return $this->device->SetPolicyKey($policykey); + } + + /** + * Builds a Provisioning SyncObject with policies + * + * @access public + * @return SyncProvisioning + */ + public function GetProvisioningObject() { + $p = new SyncProvisioning(); + // TODO load systemwide Policies + $p->Load($this->device->GetPolicies()); + return $p; + } + + /** + * Returns the status of the remote wipe policy + * + * @access public + * @return int returns the current status of the device - SYNC_PROVISION_RWSTATUS_* + */ + public function GetProvisioningWipeStatus() { + return $this->device->GetWipeStatus(); + } + + /** + * Updates the status of the remote wipe + * + * @param int $status - SYNC_PROVISION_RWSTATUS_* + * + * @access public + * @return boolean could fail if trying to update status to a wipe status which was not requested before + */ + public function SetProvisioningWipeStatus($status) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->SetProvisioningWipeStatus() change from '%d' to '%d'",$this->device->GetWipeStatus(), $status)); + + if ($status > SYNC_PROVISION_RWSTATUS_OK && !($this->device->GetWipeStatus() > SYNC_PROVISION_RWSTATUS_OK)) { + ZLog::Write(LOGLEVEL_ERROR, "Not permitted to update remote wipe status to a higher value as remote wipe was not initiated!"); + return false; + } + $this->device->SetWipeStatus($status); + return true; + } + + + /**---------------------------------------------------------------------------------------------------------- + * LEGACY AS 1.0 and WRAPPER operations + */ + + /** + * Returns a wrapped Importer & Exporter to use the + * HierarchyChache + * + * @see ChangesMemoryWrapper + * @access public + * @return object HierarchyCache + */ + public function GetHierarchyChangesWrapper() { + return $this->device->GetHierarchyCache(); + } + + /** + * Initializes the HierarchyCache for legacy syncs + * this is for AS 1.0 compatibility: + * save folder information synched with GetHierarchy() + * + * @param string $folders Array with folder information + * + * @access public + * @return boolean + */ + public function InitializeFolderCache($folders) { + $this->stateManager->SetDevice($this->device); + return $this->stateManager->InitializeFolderCache($folders); + } + + /** + * Returns the ActiveSync folder type for a FolderID + * + * @param string $folderid + * + * @access public + * @return int/boolean boolean if no type is found + */ + public function GetFolderTypeFromCacheById($folderid) { + return $this->device->GetFolderType($folderid); + } + + /** + * Returns a FolderID of default classes + * this is for AS 1.0 compatibility: + * this information was made available during GetHierarchy() + * + * @param string $class The class requested + * + * @access public + * @return string + * @throws NoHierarchyCacheAvailableException + */ + public function GetFolderIdFromCacheByClass($class) { + $folderidforClass = false; + // look at the default foldertype for this class + $type = ZPush::getDefaultFolderTypeFromFolderClass($class); + + if ($type && $type > SYNC_FOLDER_TYPE_OTHER && $type < SYNC_FOLDER_TYPE_USER_MAIL) { + $folderids = $this->device->GetAllFolderIds(); + foreach ($folderids as $folderid) { + if ($type == $this->device->GetFolderType($folderid)) { + $folderidforClass = $folderid; + break; + } + } + + // Old Palm Treos always do initial sync for calendar and contacts, even if they are not made available by the backend. + // We need to fake these folderids, allowing a fake sync/ping, even if they are not supported by the backend + // if the folderid would be available, they would already be returned in the above statement + if ($folderidforClass == false && ($type == SYNC_FOLDER_TYPE_APPOINTMENT || $type == SYNC_FOLDER_TYPE_CONTACT)) + $folderidforClass = SYNC_FOLDER_TYPE_DUMMY; + } + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->GetFolderIdFromCacheByClass('%s'): '%s' => '%s'", $class, $type, $folderidforClass)); + return $folderidforClass; + } + + /** + * Returns a FolderClass for a FolderID which is known to the mobile + * + * @param string $folderid + * + * @access public + * @return int + * @throws NoHierarchyCacheAvailableException, NotImplementedException + */ + public function GetFolderClassFromCacheByID($folderid) { + //TODO check if the parent folder exists and is also beeing synchronized + $typeFromCache = $this->device->GetFolderType($folderid); + if ($typeFromCache === false) + throw new NoHierarchyCacheAvailableException(sprintf("Folderid '%s' is not fully synchronized on the device", $folderid)); + + $class = ZPush::GetFolderClassFromFolderType($typeFromCache); + if ($class === false) + throw new NotImplementedException(sprintf("Folderid '%s' is saved to be of type '%d' but this type is not implemented", $folderid, $typeFromCache)); + + return $class; + } + + /** + * Checks if the message should be streamed to a mobile + * Should always be called before a message is sent to the mobile + * Returns true if there is something wrong and the content could break the + * synchronization + * + * @param string $id message id + * @param SyncObject &$message the method could edit the message to change the flags + * + * @access public + * @return boolean returns true if the message should NOT be send! + */ + public function DoNotStreamMessage($id, &$message) { + $folderid = $this->getLatestFolder(); + + if (isset($message->parentid)) + $folder = $message->parentid; + + // message was identified to be causing a loop + if ($this->loopdetection->IgnoreNextMessage(true, $id, $folderid)) { + $this->AnnounceIgnoredMessage($folderid, $id, $message, self::MSG_BROKEN_CAUSINGLOOP); + return true; + } + + // message is semantically incorrect + if (!$message->Check(true)) { + $this->AnnounceIgnoredMessage($folderid, $id, $message, self::MSG_BROKEN_SEMANTICERR); + return true; + } + + // check if this message is broken + if ($this->device->HasIgnoredMessage($folderid, $id)) { + // reset the flags so the message is always streamed with + $message->flags = false; + + // track the broken message in the loop detection + $this->loopdetection->SetBrokenMessage($folderid, $id); + } + return false; + } + + /** + * Removes device information about a broken message as it is been removed from the mobile. + * + * @param string $id message id + * + * @access public + * @return boolean + */ + public function RemoveBrokenMessage($id) { + $folderid = $this->getLatestFolder(); + if ($this->device->RemoveIgnoredMessage($folderid, $id)) { + ZLog::Write(LOGLEVEL_INFO, sprintf("DeviceManager->RemoveBrokenMessage('%s', '%s'): cleared data about previously ignored message", $folderid, $id)); + return true; + } + return false; + } + + /** + * Amount of items to me synchronized + * + * @param string $folderid + * @param string $type + * @param int $queuedmessages; + * @access public + * @return int + */ + public function GetWindowSize($folderid, $type, $uuid, $statecounter, $queuedmessages) { + if (isset($this->windowSize[$folderid])) + $items = $this->windowSize[$folderid]; + else + $items = (defined("SYNC_MAX_ITEMS")) ? SYNC_MAX_ITEMS : 100; + + if (defined("SYNC_MAX_ITEMS") && SYNC_MAX_ITEMS < $items) { + if ($queuedmessages > SYNC_MAX_ITEMS) + ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->GetWindowSize() overwriting max itmes requested of %d by %d forced in configuration.", $items, SYNC_MAX_ITEMS)); + $items = SYNC_MAX_ITEMS; + } + + $this->setLatestFolder($folderid); + + // detect if this is a loop condition + if ($this->loopdetection->Detect($folderid, $type, $uuid, $statecounter, $items, $queuedmessages)) + $items = ($items == 0) ? 0: 1+($this->loopdetection->IgnoreNextMessage(false)?1:0) ; + + if ($items >= 0 && $items <= 2) + ZLog::Write(LOGLEVEL_WARN, sprintf("Mobile loop detected! Messages sent to the mobile will be restricted to %d items in order to identify the conflict", $items)); + + return $items; + } + + /** + * Sets the amount of items the device is requesting + * + * @param string $folderid + * @param int $maxItems + * + * @access public + * @return boolean + */ + public function SetWindowSize($folderid, $maxItems) { + $this->windowSize[$folderid] = $maxItems; + + return true; + } + + /** + * Sets the supported fields transmitted by the device for a certain folder + * + * @param string $folderid + * @param array $fieldlist supported fields + * + * @access public + * @return boolean + */ + public function SetSupportedFields($folderid, $fieldlist) { + return $this->device->SetSupportedFields($folderid, $fieldlist); + } + + /** + * Gets the supported fields transmitted previousely by the device + * for a certain folder + * + * @param string $folderid + * + * @access public + * @return array/boolean + */ + public function GetSupportedFields($folderid) { + return $this->device->GetSupportedFields($folderid); + } + + /** + * Removes all linked states of a specific folder. + * During next request the folder is resynchronized. + * + * @param string $folderid + * + * @access public + * @return boolean + */ + public function ForceFolderResync($folderid) { + ZLog::Write(LOGLEVEL_INFO, sprintf("DeviceManager->ForceFolderResync('%s'): folder resync", $folderid)); + + // delete folder states + StateManager::UnLinkState($this->device, $folderid); + + return true; + } + + /** + * Removes all linked states from a device. + * During next requests a full resync is triggered. + * + * @access public + * @return boolean + */ + public function ForceFullResync() { + ZLog::Write(LOGLEVEL_INFO, "Full device resync requested"); + + // delete hierarchy states + StateManager::UnLinkState($this->device, false); + + // delete all other uuids + foreach ($this->device->GetAllFolderIds() as $folderid) + $uuid = StateManager::UnLinkState($this->device, $folderid); + + return true; + } + + /** + * Indicates if the hierarchy should be resynchronized + * e.g. during PING + * + * @access public + * @return boolean + */ + public function IsHierarchySyncRequired() { + // check if a hierarchy sync might be necessary + if ($this->device->GetFolderUUID(false) === false) + $this->hierarchySyncRequired = true; + + return $this->hierarchySyncRequired; + } + + /** + * Indicates if a full hierarchy resync should be triggered due to loops + * + * @access public + * @return boolean + */ + public function IsHierarchyFullResyncRequired() { + // check for potential process loops like described in ZP-5 + return $this->loopdetection->ProcessLoopDetectionIsHierarchyResyncRequired(); + } + + /** + * Adds an Exceptions to the process tracking + * + * @param Exception $exception + * + * @access public + * @return boolean + */ + public function AnnounceProcessException($exception) { + return $this->loopdetection->ProcessLoopDetectionAddException($exception); + } + + /** + * Adds a non-ok status for a folderid to the process tracking. + * On 'false' a hierarchy status is assumed + * + * @access public + * @return boolean + */ + public function AnnounceProcessStatus($folderid, $status) { + return $this->loopdetection->ProcessLoopDetectionAddStatus($folderid, $status); + } + + /** + * Announces that the current process is a push connection to the process loop + * detection and to the Top collector + * + * @access public + * @return boolean + */ + public function AnnounceProcessAsPush() { + ZLog::Write(LOGLEVEL_DEBUG, "Announce process as PUSH connection"); + + return $this->loopdetection->ProcessLoopDetectionSetAsPush() && ZPush::GetTopCollector()->SetAsPushConnection(); + } + + /** + * Checks if the given counter for a certain uuid+folderid was already exported or modified. + * This is called when a heartbeat request found changes to make sure that the same + * changes are not exported twice, as during the heartbeat there could have been a normal + * sync request. + * + * @param string $folderid folder id + * @param string $uuid synkkey + * @param string $counter synckey counter + * + * @access public + * @return boolean indicating if an uuid+counter were exported (with changes) before + */ + public function CheckHearbeatStateIntegrity($folderid, $uuid, $counter) { + return $this->loopdetection->IsSyncStateObsolete($folderid, $uuid, $counter); + } + + /** + * Marks a syncstate as obsolete for Heartbeat, as e.g. an import was started using it. + * + * @param string $folderid folder id + * @param string $uuid synkkey + * @param string $counter synckey counter + * + * @access public + * @return + */ + public function SetHeartbeatStateIntegrity($folderid, $uuid, $counter) { + return $this->loopdetection->SetSyncStateUsage($folderid, $uuid, $counter); + } + + /** + * Sets the current status of the folder + * + * @param string $folderid folder id + * @param int $statusflag current status: DeviceManager::FLD_SYNC_INITIALIZED, DeviceManager::FLD_SYNC_INPROGRESS, DeviceManager::FLD_SYNC_COMPLETED + * + * @access public + * @return + */ + public function SetFolderSyncStatus($folderid, $statusflag) { + $currentStatus = $this->device->GetFolderSyncStatus($folderid); + + // status available or just initialized + if (isset($currentStatus[ASDevice::FOLDERSYNCSTATUS]) || $statusflag == self::FLD_SYNC_INITIALIZED) { + // only update if there is a change + if ($statusflag !== $currentStatus[ASDevice::FOLDERSYNCSTATUS] && $statusflag != self::FLD_SYNC_COMPLETED) { + $this->device->SetFolderSyncStatus($folderid, array(ASDevice::FOLDERSYNCSTATUS => $statusflag)); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SetFolderSyncStatus(): set %s for %s", $statusflag, $folderid)); + } + // if completed, remove the status + else if ($statusflag == self::FLD_SYNC_COMPLETED) { + $this->device->SetFolderSyncStatus($folderid, false); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SetFolderSyncStatus(): completed for %s", $folderid)); + } + } + + return true; + } + + /** + * Indicates if the device needs an AS version update + * + * @access public + * @return boolean + */ + public function AnnounceASVersion() { + $latest = ZPush::GetSupportedASVersion(); + $announced = $this->device->GetAnnouncedASversion(); + $this->device->SetAnnouncedASversion($latest); + + return ($announced != $latest); + } + + /**---------------------------------------------------------------------------------------------------------- + * private DeviceManager methods + */ + + /** + * Loads devicedata from the StateMachine and loads it into the device + * + * @access public + * @return boolean + */ + private function loadDeviceData() { + if (!Request::IsValidDeviceID()) + return false; + try { + $deviceHash = $this->statemachine->GetStateHash($this->devid, IStateMachine::DEVICEDATA); + if ($deviceHash != $this->deviceHash) { + if ($this->deviceHash) + ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->loadDeviceData(): Device data was changed, reloading"); + $this->device->SetData($this->statemachine->GetState($this->devid, IStateMachine::DEVICEDATA)); + $this->deviceHash = $deviceHash; + } + } + catch (StateNotFoundException $snfex) { + $this->hierarchySyncRequired = true; + } + return true; + } + + /** + * Called when a SyncObject is not being streamed to the mobile. + * The user can be informed so he knows about this issue + * + * @param string $folderid id of the parent folder (may be false if unknown) + * @param string $id message id + * @param SyncObject $message the broken message + * @param string $reason (self::MSG_BROKEN_UNKNOWN, self::MSG_BROKEN_CAUSINGLOOP, self::MSG_BROKEN_SEMANTICERR) + * + * @access public + * @return boolean + */ + public function AnnounceIgnoredMessage($folderid, $id, SyncObject $message, $reason = self::MSG_BROKEN_UNKNOWN) { + if ($folderid === false) + $folderid = $this->getLatestFolder(); + + $class = get_class($message); + + $brokenMessage = new StateObject(); + $brokenMessage->id = $id; + $brokenMessage->folderid = $folderid; + $brokenMessage->ASClass = $class; + $brokenMessage->folderid = $folderid; + $brokenMessage->reasonCode = $reason; + $brokenMessage->reasonString = 'unknown cause'; + $brokenMessage->timestamp = time(); + $brokenMessage->asobject = $message; + $brokenMessage->reasonString = ZLog::GetLastMessage(LOGLEVEL_WARN); + + $this->device->AddIgnoredMessage($brokenMessage); + + ZLog::Write(LOGLEVEL_ERROR, sprintf("Ignored broken message (%s). Reason: '%s' Folderid: '%s' message id '%s'", $class, $reason, $folderid, $id)); + return true; + } + + /** + * Called when a SyncObject was streamed to the mobile. + * If the message could not be sent before this data is obsolete + * + * @param string $folderid id of the parent folder + * @param string $id message id + * + * @access public + * @return boolean returns true if the message was ignored before + */ + private function announceAcceptedMessage($folderid, $id) { + if ($this->device->RemoveIgnoredMessage($folderid, $id)) { + ZLog::Write(LOGLEVEL_INFO, sprintf("DeviceManager->announceAcceptedMessage('%s', '%s'): cleared previously ignored message as message is sucessfully streamed",$folderid, $id)); + return true; + } + return false; + } + + /** + * Checks if there were broken messages streamed to the mobile. + * If the sync completes/continues without further erros they are marked as accepted + * + * @param string $folderid folderid which is to be checked + * + * @access private + * @return boolean + */ + private function checkBrokenMessages($folderid) { + // check for correctly synchronized messages of the folder + foreach($this->loopdetection->GetSyncedButBeforeIgnoredMessages($folderid) as $okID) { + $this->announceAcceptedMessage($folderid, $okID); + } + return true; + } + + /** + * Setter for the latest folder id + * on multi-folder operations of AS 14 this is used to set the new current folder id + * + * @param string $folderid the current folder + * + * @access private + * @return boolean + */ + private function setLatestFolder($folderid) { + // this is a multi folder operation + // check on ignoredmessages before discaring the folderid + if ($this->latestFolder !== false) + $this->checkBrokenMessages($this->latestFolder); + + $this->latestFolder = $folderid; + + return true; + } + + /** + * Getter for the latest folder id + * + * @access private + * @return string $folderid the current folder + */ + private function getLatestFolder() { + return $this->latestFolder; + } +} + +?> \ No newline at end of file diff --git a/sources/lib/core/hierarchycache.php b/sources/lib/core/hierarchycache.php new file mode 100644 index 0000000..983430a --- /dev/null +++ b/sources/lib/core/hierarchycache.php @@ -0,0 +1,216 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class HierarchyCache { + private $changed = false; + protected $cacheById; + private $cacheByIdOld; + + /** + * Constructor of the HierarchyCache + * + * @access public + * @return + */ + public function HierarchyCache() { + $this->cacheById = array(); + $this->cacheByIdOld = $this->cacheById; + $this->changed = true; + } + + /** + * Indicates if the cache was changed + * + * @access public + * @return boolean + */ + public function IsStateChanged() { + return $this->changed; + } + + /** + * Copy current CacheById to memory + * + * @access public + * @return boolean + */ + public function CopyOldState() { + $this->cacheByIdOld = $this->cacheById; + return true; + } + + /** + * Returns the SyncFolder object for a folder id + * If $oldstate is set, then the data from the previous state is returned + * + * @param string $serverid + * @param boolean $oldstate (optional) by default false + * + * @access public + * @return SyncObject/boolean false if not found + */ + public function GetFolder($serverid, $oldState = false) { + if (!$oldState && array_key_exists($serverid, $this->cacheById)) { + return $this->cacheById[$serverid]; + } + else if ($oldState && array_key_exists($serverid, $this->cacheByIdOld)) { + return $this->cacheByIdOld[$serverid]; + } + return false; + } + + /** + * Adds a folder to the HierarchyCache + * + * @param SyncObject $folder + * + * @access public + * @return boolean + */ + public function AddFolder($folder) { + ZLog::Write(LOGLEVEL_DEBUG, "HierarchyCache: AddFolder() serverid: {$folder->serverid} displayname: {$folder->displayname}"); + + // on update the $folder does most of the times not contain a type + // we copy the value in this case to the new $folder object + if (isset($this->cacheById[$folder->serverid]) && (!isset($folder->type) || $folder->type == false) && isset($this->cacheById[$folder->serverid]->type)) { + $folder->type = $this->cacheById[$folder->serverid]->type; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HierarchyCache: AddFolder() is an update: used type '%s' from old object", $folder->type)); + } + + // add/update + $this->cacheById[$folder->serverid] = $folder; + $this->changed = true; + + return true; + } + + /** + * Removes a folder to the HierarchyCache + * + * @param string $serverid id of folder to be removed + * + * @access public + * @return boolean + */ + public function DelFolder($serverid) { + $ftype = $this->GetFolder($serverid); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HierarchyCache: DelFolder() serverid: '%s' - type: '%s'", $serverid, $ftype->type)); + unset($this->cacheById[$serverid]); + $this->changed = true; + return true; + } + + /** + * Imports a folder array to the HierarchyCache + * + * @param array $folders folders to the HierarchyCache + * + * @access public + * @return boolean + */ + public function ImportFolders($folders) { + if (!is_array($folders)) + return false; + + $this->cacheById = array(); + + foreach ($folders as $folder) { + if (!isset($folder->type)) + continue; + $this->AddFolder($folder); + } + return true; + } + + /** + * Exports all folders from the HierarchyCache + * + * @param boolean $oldstate (optional) by default false + * + * @access public + * @return array + */ + public function ExportFolders($oldstate = false) { + if ($oldstate === false) + return $this->cacheById; + else + return $this->cacheByIdOld; + } + + /** + * Returns all folder objects which were deleted in this operation + * + * @access public + * @return array with SyncFolder objects + */ + public function GetDeletedFolders() { + // diffing the OldCacheById with CacheById we know if folders were deleted + return array_diff_key($this->cacheByIdOld, $this->cacheById); + } + + /** + * Returns some statistics about the HierarchyCache + * + * @access public + * @return string + */ + public function GetStat() { + return sprintf("HierarchyCache is %s - Cached objects: %d", ((isset($this->cacheById))?"up":"down"), ((isset($this->cacheById))?count($this->cacheById):"0")); + } + + /** + * Returns objects which should be persistent + * called before serialization + * + * @access public + * @return array + */ + public function __sleep() { + return array("cacheById"); + } + +} + +?> \ No newline at end of file diff --git a/sources/lib/core/interprocessdata.php b/sources/lib/core/interprocessdata.php new file mode 100644 index 0000000..5a5cefc --- /dev/null +++ b/sources/lib/core/interprocessdata.php @@ -0,0 +1,297 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +abstract class InterProcessData { + const CLEANUPTIME = 1; + + static protected $devid; + static protected $pid; + static protected $user; + static protected $start; + protected $type; + protected $allocate; + private $mutexid; + private $memid; + + /** + * Constructor + * + * @access public + */ + public function InterProcessData() { + if (!isset($this->type) || !isset($this->allocate)) + throw new FatalNotImplementedException(sprintf("Class InterProcessData can not be initialized. Subclass %s did not initialize type and allocable memory.", get_class($this))); + + if ($this->InitSharedMem()) + ZLog::Write(LOGLEVEL_DEBUG, sprintf("%s(): Initialized mutexid %s and memid %s.", get_class($this), $this->mutexid, $this->memid)); + } + + /** + * Initializes internal parameters + * + * @access public + * @return boolean + */ + public function InitializeParams() { + if (!isset(self::$devid)) { + self::$devid = Request::GetDeviceID(); + self::$pid = @getmypid(); + self::$user = Request::GetAuthUser(); + self::$start = time(); + } + return true; + } + + /** + * Allocates shared memory + * + * @access private + * @return boolean + */ + private function InitSharedMem() { + // shared mem general "turn off switch" + if (defined("USE_SHARED_MEM") && USE_SHARED_MEM === false) { + ZLog::Write(LOGLEVEL_INFO, "InterProcessData::InitSharedMem(): the usage of shared memory for Z-Push has been disabled. Check your config for 'USE_SHARED_MEM'."); + return false; + } + + if (!function_exists('sem_get') || !function_exists('shm_attach') || !function_exists('sem_acquire')|| !function_exists('shm_get_var')) { + ZLog::Write(LOGLEVEL_INFO, "InterProcessData::InitSharedMem(): PHP libraries for the use shared memory are not available. Functionalities like z-push-top or loop detection are not available. Check your php packages."); + return false; + } + + // Create mutex + $this->mutexid = @sem_get($this->type, 1); + if ($this->mutexid === false) { + ZLog::Write(LOGLEVEL_ERROR, "InterProcessData::InitSharedMem(): could not aquire semaphore"); + return false; + } + + // Attach shared memory + $this->memid = shm_attach($this->type+10, $this->allocate); + if ($this->memid === false) { + ZLog::Write(LOGLEVEL_ERROR, "InterProcessData::InitSharedMem(): could not attach shared memory"); + @sem_remove($this->mutexid); + $this->mutexid = false; + return false; + } + + // TODO mem cleanup has to be implemented + //$this->setInitialCleanTime(); + + return true; + } + + /** + * Removes and detaches shared memory + * + * @access private + * @return boolean + */ + private function RemoveSharedMem() { + if ((isset($this->mutexid) && $this->mutexid !== false) && (isset($this->memid) && $this->memid !== false)) { + @sem_acquire($this->mutexid); + $memid = $this->memid; + $this->memid = false; + @sem_release($this->mutexid); + + @sem_remove($this->mutexid); + @shm_remove($memid); + @shm_detach($memid); + + $this->mutexid = false; + + return true; + } + return false; + } + + /** + * Reinitializes shared memory by removing, detaching and re-allocating it + * + * @access public + * @return boolean + */ + public function ReInitSharedMem() { + return ($this->RemoveSharedMem() && $this->InitSharedMem()); + } + + /** + * Cleans up the shared memory block + * + * @access public + * @return boolean + */ + public function Clean() { + $stat = false; + + // exclusive block + if ($this->blockMutex()) { + $cleanuptime = ($this->hasData(1)) ? $this->getData(1) : false; + + // TODO implement Shared Memory cleanup + + $this->releaseMutex(); + } + // end exclusive block + + return $stat; + } + + /** + * Indicates if the shared memory is active + * + * @access public + * @return boolean + */ + public function IsActive() { + return ((isset($this->mutexid) && $this->mutexid !== false) && (isset($this->memid) && $this->memid !== false)); + } + + /** + * Blocks the class mutex + * Method blocks until mutex is available! + * ATTENTION: make sure that you *always* release a blocked mutex! + * + * @access protected + * @return boolean + */ + protected function blockMutex() { + if ((isset($this->mutexid) && $this->mutexid !== false) && (isset($this->memid) && $this->memid !== false)) + return @sem_acquire($this->mutexid); + + return false; + } + + /** + * Releases the class mutex + * After the release other processes are able to block the mutex themselfs + * + * @access protected + * @return boolean + */ + protected function releaseMutex() { + if ((isset($this->mutexid) && $this->mutexid !== false) && (isset($this->memid) && $this->memid !== false)) + return @sem_release($this->mutexid); + + return false; + } + + /** + * Indicates if the requested variable is available in shared memory + * + * @param int $id int indicating the variable + * + * @access protected + * @return boolean + */ + protected function hasData($id = 2) { + if ((isset($this->mutexid) && $this->mutexid !== false) && (isset($this->memid) && $this->memid !== false)) { + if (function_exists("shm_has_var")) + return @shm_has_var($this->memid, $id); + else { + $some = $this->getData($id); + return isset($some); + } + } + return false; + } + + /** + * Returns the requested variable from shared memory + * + * @param int $id int indicating the variable + * + * @access protected + * @return mixed + */ + protected function getData($id = 2) { + if ((isset($this->mutexid) && $this->mutexid !== false) && (isset($this->memid) && $this->memid !== false)) + return @shm_get_var($this->memid, $id); + + return ; + } + + /** + * Writes the transmitted variable to shared memory + * Subclasses may never use an id < 2! + * + * @param mixed $data data which should be saved into shared memory + * @param int $id int indicating the variable (bigger than 2!) + * + * @access protected + * @return boolean + */ + protected function setData($data, $id = 2) { + if ((isset($this->mutexid) && $this->mutexid !== false) && (isset($this->memid) && $this->memid !== false)) + return @shm_put_var($this->memid, $id, $data); + + return false; + } + + /** + * Sets the time when the shared memory block was created + * + * @access private + * @return boolean + */ + private function setInitialCleanTime() { + $stat = false; + + // exclusive block + if ($this->blockMutex()) { + + if ($this->hasData(1) == false) + $stat = $this->setData(time(), 1); + + $this->releaseMutex(); + } + // end exclusive block + + return $stat; + } + +} + +?> \ No newline at end of file diff --git a/sources/lib/core/loopdetection.php b/sources/lib/core/loopdetection.php new file mode 100644 index 0000000..225d853 --- /dev/null +++ b/sources/lib/core/loopdetection.php @@ -0,0 +1,936 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class LoopDetection extends InterProcessData { + const INTERPROCESSLD = "ipldkey"; + const BROKENMSGS = "bromsgs"; + static private $processident; + static private $processentry; + private $ignore_messageid; + private $broken_message_uuid; + private $broken_message_counter; + + + /** + * Constructor + * + * @access public + */ + public function LoopDetection() { + // initialize super parameters + $this->allocate = 1024000; // 1 MB + $this->type = 1337; + parent::__construct(); + + $this->ignore_messageid = false; + } + + /** + * PROCESS LOOP DETECTION + */ + + /** + * Adds the process entry to the process stack + * + * @access public + * @return boolean + */ + public function ProcessLoopDetectionInit() { + return $this->updateProcessStack(); + } + + /** + * Marks the process entry as termineted successfully on the process stack + * + * @access public + * @return boolean + */ + public function ProcessLoopDetectionTerminate() { + // just to be sure that the entry is there + self::GetProcessEntry(); + + self::$processentry['end'] = time(); + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->ProcessLoopDetectionTerminate()"); + return $this->updateProcessStack(); + } + + /** + * Returns a unique identifier for the internal process tracking + * + * @access public + * @return string + */ + public static function GetProcessIdentifier() { + if (!isset(self::$processident)) + self::$processident = sprintf('%04x%04', mt_rand(0, 0xffff), mt_rand(0, 0xffff)); + + return self::$processident; + } + + /** + * Returns a unique entry with informations about the current process + * + * @access public + * @return array + */ + public static function GetProcessEntry() { + if (!isset(self::$processentry)) { + self::$processentry = array(); + self::$processentry['id'] = self::GetProcessIdentifier(); + self::$processentry['pid'] = self::$pid; + self::$processentry['time'] = self::$start; + self::$processentry['cc'] = Request::GetCommandCode(); + } + + return self::$processentry; + } + + /** + * Adds an Exceptions to the process tracking + * + * @param Exception $exception + * + * @access public + * @return boolean + */ + public function ProcessLoopDetectionAddException($exception) { + // generate entry if not already there + self::GetProcessEntry(); + + if (!isset(self::$processentry['stat'])) + self::$processentry['stat'] = array(); + + self::$processentry['stat'][get_class($exception)] = $exception->getCode(); + + $this->updateProcessStack(); + return true; + } + + /** + * Adds a folderid and connected status code to the process tracking + * + * @param string $folderid + * @param int $status + * + * @access public + * @return boolean + */ + public function ProcessLoopDetectionAddStatus($folderid, $status) { + // generate entry if not already there + self::GetProcessEntry(); + + if ($folderid === false) + $folderid = "hierarchy"; + + if (!isset(self::$processentry['stat'])) + self::$processentry['stat'] = array(); + + self::$processentry['stat'][$folderid] = $status; + + $this->updateProcessStack(); + + return true; + } + + /** + * Marks the current process as a PUSH connection + * + * @access public + * @return boolean + */ + public function ProcessLoopDetectionSetAsPush() { + // generate entry if not already there + self::GetProcessEntry(); + self::$processentry['push'] = true; + + return $this->updateProcessStack(); + } + + /** + * Indicates if a full Hierarchy Resync is necessary + * + * In some occasions the mobile tries to sync a folder with an invalid/not-existing ID. + * In these cases a status exception like SYNC_STATUS_FOLDERHIERARCHYCHANGED is returned + * so the mobile executes a FolderSync expecting that some action is taken on that folder (e.g. remove). + * + * If the FolderSync is not doing anything relevant, then the Sync is attempted again + * resulting in the same error and looping between these two processes. + * + * This method checks if in the last process stack a Sync and FolderSync were triggered to + * catch the loop at the 2nd interaction (Sync->FolderSync->Sync->FolderSync => ReSync) + * Ticket: https://jira.zarafa.com/browse/ZP-5 + * + * @access public + * @return boolean + * + */ + public function ProcessLoopDetectionIsHierarchyResyncRequired() { + $seenFailed = array(); + $seenFolderSync = false; + + $lookback = self::$start - 600; // look at the last 5 min + foreach ($this->getProcessStack() as $se) { + if ($se['time'] > $lookback && $se['time'] < (self::$start-1)) { + // look for sync command + if (isset($se['stat']) && ($se['cc'] == ZPush::COMMAND_SYNC || $se['cc'] == ZPush::COMMAND_PING)) { + foreach($se['stat'] as $key => $value) { + if (!isset($seenFailed[$key])) + $seenFailed[$key] = 0; + $seenFailed[$key]++; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->ProcessLoopDetectionIsHierarchyResyncRequired(): seen command with Exception or folderid '%s' and code '%s'", $key, $value )); + } + } + // look for FolderSync command with previous failed commands + if ($se['cc'] == ZPush::COMMAND_FOLDERSYNC && !empty($seenFailed) && $se['id'] != self::GetProcessIdentifier()) { + // a full folderresync was already triggered + if (isset($se['stat']) && isset($se['stat']['hierarchy']) && $se['stat']['hierarchy'] == SYNC_FSSTATUS_SYNCKEYERROR) { + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->ProcessLoopDetectionIsHierarchyResyncRequired(): a full FolderReSync was already requested. Resetting fail counter."); + $seenFailed = array(); + } + else { + $seenFolderSync = true; + if (!empty($seenFailed)) + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->ProcessLoopDetectionIsHierarchyResyncRequired(): seen FolderSync after other failing command"); + } + } + } + } + + $filtered = array(); + foreach ($seenFailed as $k => $count) { + if ($count>1) + $filtered[] = $k; + } + + if ($seenFolderSync && !empty($filtered)) { + ZLog::Write(LOGLEVEL_INFO, "LoopDetection->ProcessLoopDetectionIsHierarchyResyncRequired(): Potential loop detected. Full hierarchysync indicated."); + return true; + } + + return false; + } + + /** + * Indicates if a previous process could not be terminated + * + * Checks if there is an end time for the last entry on the stack + * + * @access public + * @return boolean + * + */ + public function ProcessLoopDetectionPreviousConnectionFailed() { + $stack = $this->getProcessStack(); + if (count($stack) > 1) { + $se = $stack[0]; + if (!isset($se['end']) && $se['cc'] != ZPush::COMMAND_PING && !isset($se['push']) ) { + // there is no end time + ZLog::Write(LOGLEVEL_ERROR, sprintf("LoopDetection->ProcessLoopDetectionPreviousConnectionFailed(): Command '%s' at %s with pid '%d' terminated unexpectedly or is still running.", Utils::GetCommandFromCode($se['cc']), Utils::GetFormattedTime($se['time']), $se['pid'])); + ZLog::Write(LOGLEVEL_ERROR, "Please check your logs for this PID and errors like PHP-Fatals or Apache segmentation faults and report your results to the Z-Push dev team."); + } + } + } + + /** + * Gets the PID of an outdated search process + * + * Returns false if there isn't any process + * + * @access public + * @return boolean + * + */ + public function ProcessLoopDetectionGetOutdatedSearchPID() { + $stack = $this->getProcessStack(); + if (count($stack) > 1) { + $se = $stack[0]; + if ($se['cc'] == ZPush::COMMAND_SEARCH) { + return $se['pid']; + } + } + return false; + } + + /** + * Inserts or updates the current process entry on the stack + * + * @access private + * @return boolean + */ + private function updateProcessStack() { + // initialize params + $this->InitializeParams(); + if ($this->blockMutex()) { + $loopdata = ($this->hasData()) ? $this->getData() : array(); + + // check and initialize the array structure + $this->checkArrayStructure($loopdata, self::INTERPROCESSLD); + + $stack = $loopdata[self::$devid][self::$user][self::INTERPROCESSLD]; + + // insert/update current process entry + $nstack = array(); + $updateentry = self::GetProcessEntry(); + $found = false; + + foreach ($stack as $entry) { + if ($entry['id'] != $updateentry['id']) { + $nstack[] = $entry; + } + else { + $nstack[] = $updateentry; + $found = true; + } + } + + if (!$found) + $nstack[] = $updateentry; + + if (count($nstack) > 10) + $nstack = array_slice($nstack, -10, 10); + + // update loop data + $loopdata[self::$devid][self::$user][self::INTERPROCESSLD] = $nstack; + $ok = $this->setData($loopdata); + + $this->releaseMutex(); + } + // end exclusive block + + return true; + } + + /** + * Returns the current process stack + * + * @access private + * @return array + */ + private function getProcessStack() { + // initialize params + $this->InitializeParams(); + $stack = array(); + + if ($this->blockMutex()) { + $loopdata = ($this->hasData()) ? $this->getData() : array(); + + // check and initialize the array structure + $this->checkArrayStructure($loopdata, self::INTERPROCESSLD); + + $stack = $loopdata[self::$devid][self::$user][self::INTERPROCESSLD]; + + $this->releaseMutex(); + } + // end exclusive block + + return $stack; + } + + /** + * TRACKING OF BROKEN MESSAGES + * if a previousily ignored message is streamed again to the device it's tracked here + * + * There are two outcomes: + * - next uuid counter is higher than current -> message is fixed and successfully synchronized + * - next uuid counter is the same or uuid changed -> message is still broken + */ + + /** + * Adds a message to the tracking of broken messages + * Being tracked means that a broken message was streamed to the device. + * We save the latest uuid and counter so if on the next sync the counter is higher + * the message was accepted by the device. + * + * @param string $folderid the parent folder of the message + * @param string $id the id of the message + * + * @access public + * @return boolean + */ + public function SetBrokenMessage($folderid, $id) { + if ($folderid == false || !isset($this->broken_message_uuid) || !isset($this->broken_message_counter) || $this->broken_message_uuid == false || $this->broken_message_counter == false) + return false; + + $ok = false; + $brokenkey = self::BROKENMSGS ."-". $folderid; + + // initialize params + $this->InitializeParams(); + if ($this->blockMutex()) { + $loopdata = ($this->hasData()) ? $this->getData() : array(); + + // check and initialize the array structure + $this->checkArrayStructure($loopdata, $brokenkey); + + $brokenmsgs = $loopdata[self::$devid][self::$user][$brokenkey]; + + $brokenmsgs[$id] = array('uuid' => $this->broken_message_uuid, 'counter' => $this->broken_message_counter); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->SetBrokenMessage('%s', '%s'): tracking broken message", $folderid, $id)); + + // update data + $loopdata[self::$devid][self::$user][$brokenkey] = $brokenmsgs; + $ok = $this->setData($loopdata); + + $this->releaseMutex(); + } + // end exclusive block + + return $ok; + } + + /** + * Gets a list of all ids of a folder which were tracked and which were + * accepted by the device from the last sync. + * + * @param string $folderid the parent folder of the message + * @param string $id the id of the message + * + * @access public + * @return array + */ + public function GetSyncedButBeforeIgnoredMessages($folderid) { + if ($folderid == false || !isset($this->broken_message_uuid) || !isset($this->broken_message_counter) || $this->broken_message_uuid == false || $this->broken_message_counter == false) + return array(); + + $brokenkey = self::BROKENMSGS ."-". $folderid; + $removeIds = array(); + $okIds = array(); + + // initialize params + $this->InitializeParams(); + if ($this->blockMutex()) { + $loopdata = ($this->hasData()) ? $this->getData() : array(); + + // check and initialize the array structure + $this->checkArrayStructure($loopdata, $brokenkey); + + $brokenmsgs = $loopdata[self::$devid][self::$user][$brokenkey]; + + if (!empty($brokenmsgs)) { + foreach ($brokenmsgs as $id => $data) { + // previously broken message was sucessfully synced! + if ($data['uuid'] == $this->broken_message_uuid && $data['counter'] < $this->broken_message_counter) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->GetSyncedButBeforeIgnoredMessages('%s'): message '%s' was successfully synchronized", $folderid, $id)); + $okIds[] = $id; + } + + // if the uuid has changed this is old data which should also be removed + if ($data['uuid'] != $this->broken_message_uuid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->GetSyncedButBeforeIgnoredMessages('%s'): stored message id '%s' for uuid '%s' is obsolete", $folderid, $id, $data['uuid'])); + $removeIds[] = $id; + } + } + + // remove data + foreach (array_merge($okIds,$removeIds) as $id) { + unset($brokenmsgs[$id]); + } + + if (empty($brokenmsgs) && isset($loopdata[self::$devid][self::$user][$brokenkey])) { + unset($loopdata[self::$devid][self::$user][$brokenkey]); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->GetSyncedButBeforeIgnoredMessages('%s'): removed folder from tracking of ignored messages", $folderid)); + } + else { + // update data + $loopdata[self::$devid][self::$user][$brokenkey] = $brokenmsgs; + } + $ok = $this->setData($loopdata); + } + + $this->releaseMutex(); + } + // end exclusive block + + return $okIds; + } + + /** + * Marks a SyncState as "already used", e.g. when an import process started. + * This is most critical for DiffBackends, as an imported message would be exported again + * in the heartbeat if the notification is triggered before the import is complete. + * + * @param string $folderid folder id + * @param string $uuid synkkey + * @param string $counter synckey counter + * + * @access public + * @return boolean + */ + public function SetSyncStateUsage($folderid, $uuid, $counter) { + // initialize params + $this->InitializeParams(); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->SetSyncStateUsage(): uuid: %s counter: %d", $uuid, $counter)); + + // exclusive block + if ($this->blockMutex()) { + $loopdata = ($this->hasData()) ? $this->getData() : array(); + // check and initialize the array structure + $this->checkArrayStructure($loopdata, $folderid); + $current = $loopdata[self::$devid][self::$user][$folderid]; + + + // update the usage flag + $current["usage"] = $counter; + + // update loop data + $loopdata[self::$devid][self::$user][$folderid] = $current; + $ok = $this->setData($loopdata); + + $this->releaseMutex(); + } + // end exclusive block + } + + /** + * Checks if the given counter for a certain uuid+folderid was exported before. + * Returns also true if the counter are the same but previously there were + * changes to be exported. + * + * @param string $folderid folder id + * @param string $uuid synkkey + * @param string $counter synckey counter + * + * @access public + * @return boolean indicating if an uuid+counter were exported (with changes) before + */ + public function IsSyncStateObsolete($folderid, $uuid, $counter) { + // initialize params + $this->InitializeParams(); + + $obsolete = false; + + // exclusive block + if ($this->blockMutex()) { + $loopdata = ($this->hasData()) ? $this->getData() : array(); + $this->releaseMutex(); + // end exclusive block + + // check and initialize the array structure + $this->checkArrayStructure($loopdata, $folderid); + + $current = $loopdata[self::$devid][self::$user][$folderid]; + + if (!empty($current)) { + if (!isset($current["uuid"]) || $current["uuid"] != $uuid) { + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->IsSyncStateObsolete(): yes, uuid changed or not set"); + $obsolete = true; + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->IsSyncStateObsolete(): check uuid counter: %d - last known counter: %d with %d queued objects", $counter, $current["count"], $current["queued"])); + + if ($current["uuid"] == $uuid && ($current["count"] > $counter || ($current["count"] == $counter && $current["queued"] > 0) || (isset($current["usage"]) && $current["usage"] >= $counter))) { + $usage = isset($current["usage"]) ? sprintf(" - counter %d already expired",$current["usage"]) : ""; + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->IsSyncStateObsolete(): yes, counter already processed". $usage); + $obsolete = true; + } + } + } + } + + return $obsolete; + } + + /** + * MESSAGE LOOP DETECTION + */ + + /** + * Loop detection mechanism + * + * 1. request counter is higher than the previous counter (somehow default) + * 1.1) standard situation -> do nothing + * 1.2) loop information exists + * 1.2.1) request counter < maxCounter AND no ignored data -> continue in loop mode + * 1.2.2) request counter < maxCounter AND ignored data -> we have already encountered issue, return to normal + * + * 2. request counter is the same as the previous, but no data was sent on the last request (standard situation) + * + * 3. request counter is the same as the previous and last time objects were sent (loop!) + * 3.1) no loop was detected before, entereing loop mode -> save loop data, loopcount = 1 + * 3.2) loop was detected before, but are gone -> loop resolved + * 3.3) loop was detected before, continuing in loop mode -> this is probably the broken element,loopcount++, + * 3.3.1) item identified, loopcount >= 3 -> ignore item, set ignoredata flag + * + * @param string $folderid the current folder id to be worked on + * @param string $type the type of that folder (Email, Calendar, Contact, Task) + * @param string $uuid the synkkey + * @param string $counter the synckey counter + * @param string $maxItems the current amount of items to be sent to the mobile + * @param string $queuedMessages the amount of messages which were found by the exporter + * + * @access public + * @return boolean when returning true if a loop has been identified + */ + public function Detect($folderid, $type, $uuid, $counter, $maxItems, $queuedMessages) { + $this->broken_message_uuid = $uuid; + $this->broken_message_counter = $counter; + + // if an incoming loop is already detected, do nothing + if ($maxItems === 0 && $queuedMessages > 0) { + ZPush::GetTopCollector()->AnnounceInformation("Incoming loop!", true); + return true; + } + + // initialize params + $this->InitializeParams(); + + $loop = false; + + // exclusive block + if ($this->blockMutex()) { + $loopdata = ($this->hasData()) ? $this->getData() : array(); + + // check and initialize the array structure + $this->checkArrayStructure($loopdata, $folderid); + + $current = $loopdata[self::$devid][self::$user][$folderid]; + + // completely new/unknown UUID + if (empty($current)) + $current = array("type" => $type, "uuid" => $uuid, "count" => $counter-1, "queued" => $queuedMessages); + + // old UUID in cache - the device requested a new state!! + else if (isset($current['type']) && $current['type'] == $type && isset($current['uuid']) && $current['uuid'] != $uuid ) { + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): UUID changed for folder"); + + // some devices (iPhones) may request new UUIDs after broken items were sent several times + if (isset($current['queued']) && $current['queued'] > 0 && + (isset($current['maxCount']) && $current['count']+1 < $current['maxCount'] || $counter == 1)) { + + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): UUID changed and while items where sent to device - forcing loop mode"); + $loop = true; // force loop mode + $current['queued'] = $queuedMessages; + } + else { + $current['queued'] = 0; + } + + // set new data, unset old loop information + $current["uuid"] = $uuid; + $current['count'] = $counter; + unset($current['loopcount']); + unset($current['ignored']); + unset($current['maxCount']); + unset($current['potential']); + } + + // see if there are values + if (isset($current['uuid']) && $current['uuid'] == $uuid && + isset($current['type']) && $current['type'] == $type && + isset($current['count'])) { + + // case 1 - standard, during loop-resolving & resolving + if ($current['count'] < $counter) { + + // case 1.1 + $current['count'] = $counter; + $current['queued'] = $queuedMessages; + if (isset($current["usage"]) && $current["usage"] < $current['count']) + unset($current["usage"]); + + // case 1.2 + if (isset($current['maxCount'])) { + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): case 1.2 detected"); + + // case 1.2.1 + // broken item not identified yet + if (!isset($current['ignored']) && $counter < $current['maxCount']) { + $loop = true; // continue in loop-resolving + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): case 1.2.1 detected"); + } + // case 1.2.2 - if there were any broken items they should be gone, return to normal + else { + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): case 1.2.2 detected"); + unset($current['loopcount']); + unset($current['ignored']); + unset($current['maxCount']); + unset($current['potential']); + } + } + } + + // case 2 - same counter, but there were no changes before and are there now + else if ($current['count'] == $counter && $current['queued'] == 0 && $queuedMessages > 0) { + $current['queued'] = $queuedMessages; + if (isset($current["usage"]) && $current["usage"] < $current['count']) + unset($current["usage"]); + } + + // case 3 - same counter, changes sent before, hanging loop and ignoring + else if ($current['count'] == $counter && $current['queued'] > 0) { + + if (!isset($current['loopcount'])) { + // case 3.1) we have just encountered a loop! + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): case 3.1 detected - loop detected, init loop mode"); + $current['loopcount'] = 1; + // the MaxCount is the max number of messages exported before + $current['maxCount'] = $counter + (($maxItems < $queuedMessages)? $maxItems: $queuedMessages); + $loop = true; // loop mode!! + } + else if ($queuedMessages == 0) { + // case 3.2) there was a loop before but now the changes are GONE + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): case 3.2 detected - changes gone - clearing loop data"); + $current['queued'] = 0; + unset($current['loopcount']); + unset($current['ignored']); + unset($current['maxCount']); + unset($current['potential']); + } + else { + // case 3.3) still looping the same message! Increase counter + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): case 3.3 detected - in loop mode, increase loop counter"); + $current['loopcount']++; + + // case 3.3.1 - we got our broken item! + if ($current['loopcount'] >= 3 && isset($current['potential'])) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->Detect(): case 3.3.1 detected - broken item should be next, attempt to ignore it - id '%s'", $current['potential'])); + $this->ignore_messageid = $current['potential']; + } + $current['maxCount'] = $counter + $queuedMessages; + $loop = true; // loop mode!! + } + } + + } + if (isset($current['loopcount'])) + ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->Detect(): loop data: loopcount(%d), maxCount(%d), queued(%d), ignored(%s)", $current['loopcount'], $current['maxCount'], $current['queued'], (isset($current['ignored'])?$current['ignored']:'false'))); + + // update loop data + $loopdata[self::$devid][self::$user][$folderid] = $current; + $ok = $this->setData($loopdata); + + $this->releaseMutex(); + } + // end exclusive block + + if ($loop == true && $this->ignore_messageid == false) { + ZPush::GetTopCollector()->AnnounceInformation("Loop detection", true); + } + + return $loop; + } + + /** + * Indicates if the next messages should be ignored (not be sent to the mobile!) + * + * @param string $messageid (opt) id of the message which is to be exported next + * @param string $folderid (opt) parent id of the message + * @param boolean $markAsIgnored (opt) to peek without setting the next message to be + * ignored, set this value to false + * @access public + * @return boolean + */ + public function IgnoreNextMessage($markAsIgnored = true, $messageid = false, $folderid = false) { + // as the next message id is not available at all point this method is called, we use different indicators. + // potentialbroken indicates that we know that the broken message should be exported next, + // alltho we do not know for sure as it's export message orders can change + // if the $messageid is available and matches then we are sure and only then really ignore it + + $potentialBroken = false; + $realBroken = false; + if (Request::GetCommandCode() == ZPush::COMMAND_SYNC && $this->ignore_messageid !== false) + $potentialBroken = true; + + if ($messageid !== false && $this->ignore_messageid == $messageid) + $realBroken = true; + + // this call is just to know what should be happening + // no further actions necessary + if ($markAsIgnored === false) { + return $potentialBroken; + } + + // we should really do something here + + // first we check if we are in the loop mode, if so, + // we update the potential broken id message so we loop count the same message + + $changedData = false; + // exclusive block + if ($this->blockMutex()) { + $loopdata = ($this->hasData()) ? $this->getData() : array(); + + // check and initialize the array structure + $this->checkArrayStructure($loopdata, $folderid); + + $current = $loopdata[self::$devid][self::$user][$folderid]; + + // we found our broken message! + if ($realBroken) { + $this->ignore_messageid = false; + $current['ignored'] = $messageid; + $changedData = true; + + // check if this message was broken before - here we know that it still is and remove it from the tracking + $brokenkey = self::BROKENMSGS ."-". $folderid; + if (isset($loopdata[self::$devid][self::$user][$brokenkey]) && isset($loopdata[self::$devid][self::$user][$brokenkey][$messageid])) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->IgnoreNextMessage(): previously broken message '%s' is still broken and will not be tracked anymore", $messageid)); + unset($loopdata[self::$devid][self::$user][$brokenkey][$messageid]); + } + } + // not the broken message yet + else { + // update potential id if looping on an item + if (isset($current['loopcount'])) { + $current['potential'] = $messageid; + + // this message should be the broken one, but is not!! + // we should reset the loop count because this is certainly not the broken one + if ($potentialBroken) { + $current['loopcount'] = 1; + ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->IgnoreNextMessage(): this should be the broken one, but is not! Resetting loop count."); + } + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->IgnoreNextMessage(): Loop mode, potential broken message id '%s'", $current['potential'])); + + $changedData = true; + } + } + + // update loop data + if ($changedData == true) { + $loopdata[self::$devid][self::$user][$folderid] = $current; + $ok = $this->setData($loopdata); + } + + $this->releaseMutex(); + } + // end exclusive block + + if ($realBroken) + ZPush::GetTopCollector()->AnnounceInformation("Broken message ignored", true); + + return $realBroken; + } + + /** + * Clears loop detection data + * + * @param string $user (opt) user which data should be removed - user can not be specified without + * @param string $devid (opt) device id which data to be removed + * + * @return boolean + * @access public + */ + public function ClearData($user = false, $devid = false) { + $stat = true; + $ok = false; + + // exclusive block + if ($this->blockMutex()) { + $loopdata = ($this->hasData()) ? $this->getData() : array(); + + if ($user == false && $devid == false) + $loopdata = array(); + elseif ($user == false && $devid != false) + $loopdata[$devid] = array(); + elseif ($user != false && $devid != false) + $loopdata[$devid][$user] = array(); + elseif ($user != false && $devid == false) { + ZLog::Write(LOGLEVEL_WARN, sprintf("Not possible to reset loop detection data for user '%s' without a specifying a device id", $user)); + $stat = false; + } + + if ($stat) + $ok = $this->setData($loopdata); + + $this->releaseMutex(); + } + // end exclusive block + + return $stat && $ok; + } + + /** + * Returns loop detection data for a user and device + * + * @param string $user + * @param string $devid + * + * @return array/boolean returns false if data not available + * @access public + */ + public function GetCachedData($user, $devid) { + // exclusive block + if ($this->blockMutex()) { + $loopdata = ($this->hasData()) ? $this->getData() : array(); + $this->releaseMutex(); + } + // end exclusive block + if (isset($loopdata) && isset($loopdata[$devid]) && isset($loopdata[$devid][$user])) + return $loopdata[$devid][$user]; + + return false; + } + + /** + * Builds an array structure for the loop detection data + * + * @param array $loopdata reference to the topdata array + * + * @access private + * @return + */ + private function checkArrayStructure(&$loopdata, $folderid) { + if (!isset($loopdata) || !is_array($loopdata)) + $loopdata = array(); + + if (!isset($loopdata[self::$devid])) + $loopdata[self::$devid] = array(); + + if (!isset($loopdata[self::$devid][self::$user])) + $loopdata[self::$devid][self::$user] = array(); + + if (!isset($loopdata[self::$devid][self::$user][$folderid])) + $loopdata[self::$devid][self::$user][$folderid] = array(); + } +} + +?> \ No newline at end of file diff --git a/sources/lib/core/paddingfilter.php b/sources/lib/core/paddingfilter.php new file mode 100644 index 0000000..d5a7a12 --- /dev/null +++ b/sources/lib/core/paddingfilter.php @@ -0,0 +1,101 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +/* Define our filter class + * + * Usage: stream_filter_append($stream, 'padding.X'); + * where X is a number a stream will be padded to be + * multiple of (e.g. padding.3 will pad the stream + * to be multiple of 3 which is useful in base64 + * encoding). + * + * */ +class padding_filter extends php_user_filter { + private $padding = 4; // default padding + + /** + * This method is called whenever data is read from or written to the attached stream + * + * @see php_user_filter::filter() + * + * @param resource $in + * @param resource $out + * @param int $consumed + * @param boolean $closing + * + * @access public + * @return int + * + */ + function filter($in, $out, &$consumed, $closing) { + while ($bucket = stream_bucket_make_writeable($in)) { + if ($this->padding != 0 && $bucket->datalen < 8192) { + $bucket->data .= str_pad($bucket->data, $this->padding, 0x0); + } + $consumed += ($this->padding != 0 && $bucket->datalen < 8192) ? ($bucket->datalen + $this->padding) : $bucket->datalen; + stream_bucket_append($out, $bucket); + } + return PSFS_PASS_ON; + } + + /** + * Called when creating the filter + * + * @see php_user_filter::onCreate() + * + * @access public + * @return boolean + */ + function onCreate() { + $delim = strrpos($this->filtername, '.'); + if ($delim !== false) { + $padding = substr($this->filtername, $delim + 1); + if (is_numeric($padding)) + $this->padding = $padding; + } + return true; + } +} + +stream_filter_register("padding.*", "padding_filter"); +?> \ No newline at end of file diff --git a/sources/lib/core/pingtracking.php b/sources/lib/core/pingtracking.php new file mode 100644 index 0000000..47e751f --- /dev/null +++ b/sources/lib/core/pingtracking.php @@ -0,0 +1,156 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class PingTracking extends InterProcessData { + + /** + * Constructor + * + * @access public + */ + public function PingTracking() { + // initialize super parameters + $this->allocate = 512000; // 500 KB + $this->type = 2; + parent::__construct(); + + $this->initPing(); + } + + /** + * Destructor + * Used to remove the current ping data from shared memory + * + * @access public + */ + public function __destruct() { + // exclusive block + if ($this->blockMutex()) { + $pings = $this->getData(); + + // check if our ping is still in the list + if (isset($pings[self::$devid][self::$user][self::$pid])) { + unset($pings[self::$devid][self::$user][self::$pid]); + $stat = $this->setData($pings); + } + + $this->releaseMutex(); + } + // end exclusive block + } + + /** + * Initialized the current request + * + * @access public + * @return boolean + */ + protected function initPing() { + $stat = false; + + // initialize params + $this->InitializeParams(); + + // exclusive block + if ($this->blockMutex()) { + $pings = ($this->hasData()) ? $this->getData() : array(); + + // set the start time for the current process + $this->checkArrayStructure($pings); + $pings[self::$devid][self::$user][self::$pid] = self::$start; + $stat = $this->setData($pings); + $this->releaseMutex(); + } + // end exclusive block + + return $stat; + } + + /** + * Checks if there are newer ping requests for the same device & user so + * the current process could be terminated + * + * @access public + * @return boolean true if the current process is obsolete + */ + public function DoForcePingTimeout() { + $pings = false; + // exclusive block + if ($this->blockMutex()) { + $pings = $this->getData(); + $this->releaseMutex(); + } + // end exclusive block + + // check if there is another (and newer) active ping connection + if (is_array($pings) && isset($pings[self::$devid][self::$user]) && count($pings[self::$devid][self::$user]) > 1) { + foreach ($pings[self::$devid][self::$user] as $pid=>$starttime) + if ($starttime > self::$start) + return true; + } + + return false; + } + + /** + * Builds an array structure for the concurrent ping connection detection + * + * @param array $array reference to the ping data array + * + * @access private + * @return + */ + private function checkArrayStructure(&$array) { + if (!is_array($array)) + $array = array(); + + if (!isset($array[self::$devid])) + $array[self::$devid] = array(); + + if (!isset($array[self::$devid][self::$user])) + $array[self::$devid][self::$user] = array(); + + } +} + +?> \ No newline at end of file diff --git a/sources/lib/core/statemanager.php b/sources/lib/core/statemanager.php new file mode 100644 index 0000000..28d5a47 --- /dev/null +++ b/sources/lib/core/statemanager.php @@ -0,0 +1,540 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class StateManager { + const FIXEDHIERARCHYCOUNTER = 99999; + + // backend storage types + const BACKENDSTORAGE_PERMANENT = 1; + const BACKENDSTORAGE_STATE = 2; + + private $statemachine; + private $device; + private $hierarchyOperation = false; + private $deleteOldStates = false; + + private $foldertype; + private $uuid; + private $oldStateCounter; + private $newStateCounter; + private $synchedFolders; + + + /** + * Constructor + * + * @access public + */ + public function StateManager() { + $this->statemachine = ZPush::GetStateMachine(); + $this->hierarchyOperation = ZPush::HierarchyCommand(Request::GetCommandCode()); + $this->deleteOldStates = (Request::GetCommandCode() === ZPush::COMMAND_SYNC || $this->hierarchyOperation); + $this->synchedFolders = array(); + } + + /** + * Prevents the StateMachine from removing old states + * + * @access public + * @return void + */ + public function DoNotDeleteOldStates() { + $this->deleteOldStates = false; + } + + /** + * Sets an ASDevice for the Statemanager to work with + * + * @param ASDevice $device + * + * @access public + * @return boolean + */ + public function SetDevice(&$device) { + $this->device = $device; + return true; + } + + /** + * Returns an array will all synchronized folderids + * + * @access public + * @return array + */ + public function GetSynchedFolders() { + $synched = array(); + foreach ($this->device->GetAllFolderIds() as $folderid) { + $uuid = $this->device->GetFolderUUID($folderid); + if ($uuid) + $synched[] = $folderid; + } + return $synched; + } + + /** + * Returns a folder state (SyncParameters) for a folder id + * + * @param $folderid + * + * @access public + * @return SyncParameters + */ + public function GetSynchedFolderState($folderid) { + // new SyncParameters are cached + if (isset($this->synchedFolders[$folderid])) + return $this->synchedFolders[$folderid]; + + $uuid = $this->device->GetFolderUUID($folderid); + if ($uuid) { + try { + $data = $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::FOLDERDATA, $uuid); + if ($data !== false) { + $this->synchedFolders[$folderid] = $data; + } + } + catch (StateNotFoundException $ex) { } + } + + if (!isset($this->synchedFolders[$folderid])) + $this->synchedFolders[$folderid] = new SyncParameters(); + + return $this->synchedFolders[$folderid]; + } + + /** + * Saves a folder state - SyncParameters object + * + * @param SyncParamerters $spa + * + * @access public + * @return boolean + */ + public function SetSynchedFolderState($spa) { + // make sure the current uuid is linked on the device for the folder. + // if not, old states will be automatically removed and the new ones linked + self::LinkState($this->device, $spa->GetUuid(), $spa->GetFolderId()); + + $spa->SetReferencePolicyKey($this->device->GetPolicyKey()); + + return $this->statemachine->SetState($spa, $this->device->GetDeviceId(), IStateMachine::FOLDERDATA, $spa->GetUuid()); + } + + /** + * Gets the new sync key for a specified sync key. The new sync state must be + * associated to this sync key when calling SetSyncState() + * + * @param string $synckey + * + * @access public + * @return string + */ + function GetNewSyncKey($synckey) { + if(!isset($synckey) || $synckey == "0" || $synckey == false) { + $this->uuid = $this->getNewUuid(); + $this->newStateCounter = 1; + } + else { + list($uuid, $counter) = self::ParseStateKey($synckey); + $this->uuid = $uuid; + $this->newStateCounter = $counter + 1; + } + + return self::BuildStateKey($this->uuid, $this->newStateCounter); + } + + /** + * Gets the state for a specified synckey (uuid + counter) + * + * @param string $synckey + * + * @access public + * @return string + * @throws StateInvalidException, StateNotFoundException + */ + public function GetSyncState($synckey) { + // No sync state for sync key '0' + if($synckey == "0") { + $this->oldStateCounter = 0; + return ""; + } + + // Check if synckey is allowed and set uuid and counter + list($this->uuid, $this->oldStateCounter) = self::ParseStateKey($synckey); + + // make sure the hierarchy cache is in place + if ($this->hierarchyOperation) + $this->loadHierarchyCache(); + + // the state machine will discard any sync states before this one, as they are no longer required + return $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::DEFTYPE, $this->uuid, $this->oldStateCounter, $this->deleteOldStates); + } + + /** + * Writes the sync state to a new synckey + * + * @param string $synckey + * @param string $syncstate + * @param string $folderid (opt) the synckey is associated with the folder - should always be set when performing CONTENT operations + * + * @access public + * @return boolean + * @throws StateInvalidException + */ + public function SetSyncState($synckey, $syncstate, $folderid = false) { + $internalkey = self::BuildStateKey($this->uuid, $this->newStateCounter); + if ($this->oldStateCounter != 0 && $synckey != $internalkey) + throw new StateInvalidException(sprintf("Unexpected synckey value oldcounter: '%s' synckey: '%s' internal key: '%s'", $this->oldStateCounter, $synckey, $internalkey)); + + // make sure the hierarchy cache is also saved + if ($this->hierarchyOperation) + $this->saveHierarchyCache(); + + // announce this uuid to the device, while old uuid/states should be deleted + self::LinkState($this->device, $this->uuid, $folderid); + + return $this->statemachine->SetState($syncstate, $this->device->GetDeviceId(), IStateMachine::DEFTYPE, $this->uuid, $this->newStateCounter); + } + + /** + * Gets the failsave sync state for the current synckey + * + * @access public + * @return array/boolean false if not available + */ + public function GetSyncFailState() { + if (!$this->uuid) + return false; + + try { + return $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::FAILSAVE, $this->uuid, $this->oldStateCounter, $this->deleteOldStates); + } + catch (StateNotFoundException $snfex) { + return false; + } + } + + /** + * Writes the failsave sync state for the current (old) synckey + * + * @param mixed $syncstate + * + * @access public + * @return boolean + */ + public function SetSyncFailState($syncstate) { + if ($this->oldStateCounter == 0) + return false; + + return $this->statemachine->SetState($syncstate, $this->device->GetDeviceId(), IStateMachine::FAILSAVE, $this->uuid, $this->oldStateCounter); + } + + /** + * Gets the backendstorage data + * + * @param int $type permanent or state related storage + * + * @access public + * @return mixed + * @throws StateNotYetAvailableException, StateNotFoundException + */ + public function GetBackendStorage($type = self::BACKENDSTORAGE_PERMANENT) { + if ($type == self::BACKENDSTORAGE_STATE) { + if (!$this->uuid) + throw new StateNotYetAvailableException(); + + return $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, $this->uuid, $this->oldStateCounter, $this->deleteOldStates); + } + else { + return $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, false, $this->device->GetFirstSyncTime()); + } + } + + /** + * Writes the backendstorage data + * + * @param mixed $data + * @param int $type permanent or state related storage + * + * @access public + * @return int amount of bytes saved + * @throws StateNotYetAvailableException, StateNotFoundException + */ + public function SetBackendStorage($data, $type = self::BACKENDSTORAGE_PERMANENT) { + if ($type == self::BACKENDSTORAGE_STATE) { + if (!$this->uuid) + throw new StateNotYetAvailableException(); + + // TODO serialization should be done in the StateMachine + return $this->statemachine->SetState($data, $this->device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, $this->uuid, $this->newStateCounter); + } + else { + return $this->statemachine->SetState($data, $this->device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, false, $this->device->GetFirstSyncTime()); + } + } + + /** + * Initializes the HierarchyCache for legacy syncs + * this is for AS 1.0 compatibility: + * save folder information synched with GetHierarchy() + * handled by StateManager + * + * @param string $folders Array with folder information + * + * @access public + * @return boolean + */ + public function InitializeFolderCache($folders) { + if (!is_array($folders)) + return false; + + if (!isset($this->device)) + throw new FatalException("ASDevice not initialized"); + + // redeclare this operation as hierarchyOperation + $this->hierarchyOperation = true; + + // as there is no hierarchy uuid, we have to create one + $this->uuid = $this->getNewUuid(); + $this->newStateCounter = self::FIXEDHIERARCHYCOUNTER; + + // initialize legacy HierarchCache + $this->device->SetHierarchyCache($folders); + + // force saving the hierarchy cache! + return $this->saveHierarchyCache(true); + } + + + /**---------------------------------------------------------------------------------------------------------- + * static StateManager methods + */ + + /** + * Links a folderid to the a UUID + * Old states are removed if an folderid is linked to a new UUID + * assisting the StateMachine to get rid of old data. + * + * @param ASDevice $device + * @param string $uuid the uuid to link to + * @param string $folderid (opt) if not set, hierarchy state is linked + * + * @access public + * @return boolean + */ + static public function LinkState(&$device, $newUuid, $folderid = false) { + $savedUuid = $device->GetFolderUUID($folderid); + // delete 'old' states! + if ($savedUuid != $newUuid) { + // remove states but no need to notify device + self::UnLinkState($device, $folderid, false); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("StateManager::linkState(#ASDevice, '%s','%s'): linked to uuid '%s'.", $newUuid, (($folderid === false)?'HierarchyCache':$folderid), $newUuid)); + return $device->SetFolderUUID($newUuid, $folderid); + } + return true; + } + + /** + * UnLinks all states from a folder id + * Old states are removed assisting the StateMachine to get rid of old data. + * The UUID is then removed from the device + * + * @param ASDevice $device + * @param string $folderid + * @param boolean $removeFromDevice indicates if the device should be + * notified that the state was removed + * @param boolean $retrieveUUIDFromDevice indicates if the UUID should be retrieved from + * device. If not true this parameter will be used as UUID. + * + * @access public + * @return boolean + */ + static public function UnLinkState(&$device, $folderid, $removeFromDevice = true, $retrieveUUIDFromDevice = true) { + if ($retrieveUUIDFromDevice === true) + $savedUuid = $device->GetFolderUUID($folderid); + else + $savedUuid = $retrieveUUIDFromDevice; + + if ($savedUuid) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("StateManager::UnLinkState('%s'): saved state '%s' will be deleted.", $folderid, $savedUuid)); + ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::DEFTYPE, $savedUuid, self::FIXEDHIERARCHYCOUNTER *2); + ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::FOLDERDATA, $savedUuid); // CPO + ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::FAILSAVE, $savedUuid, self::FIXEDHIERARCHYCOUNTER *2); + ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, $savedUuid, self::FIXEDHIERARCHYCOUNTER *2); + + // remove all messages which could not be synched before + $device->RemoveIgnoredMessage($folderid, false); + + if ($folderid === false && $savedUuid !== false) + ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::HIERARCHY, $savedUuid, self::FIXEDHIERARCHYCOUNTER *2); + } + // delete this id from the uuid cache + if ($removeFromDevice) + return $device->SetFolderUUID(false, $folderid); + else + return true; + } + + /** + * Parses a SyncKey and returns UUID and counter + * + * @param string $synckey + * + * @access public + * @return array uuid, counter + * @throws StateInvalidException + */ + static public function ParseStateKey($synckey) { + $matches = array(); + if(!preg_match('/^\{([0-9A-Za-z-]+)\}([0-9]+)$/', $synckey, $matches)) + throw new StateInvalidException(sprintf("SyncKey '%s' is invalid", $synckey)); + + return array($matches[1], (int)$matches[2]); + } + + /** + * Builds a SyncKey from a UUID and counter + * + * @param string $uuid + * @param int $counter + * + * @access public + * @return string syncKey + * @throws StateInvalidException + */ + static public function BuildStateKey($uuid, $counter) { + if(!preg_match('/^([0-9A-Za-z-]+)$/', $uuid, $matches)) + throw new StateInvalidException(sprintf("UUID '%s' is invalid", $uuid)); + + return "{" . $uuid . "}" . $counter; + } + + + /**---------------------------------------------------------------------------------------------------------- + * private StateManager methods + */ + + /** + * Loads the HierarchyCacheState and initializes the HierarchyChache + * if this is an hierarchy operation + * + * @access private + * @return boolean + * @throws StateNotFoundException + */ + private function loadHierarchyCache() { + if (!$this->hierarchyOperation) + return false; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("StateManager->loadHierarchyCache(): '%s-%s-%s-%d'", $this->device->GetDeviceId(), $this->uuid, IStateMachine::HIERARCHY, $this->oldStateCounter)); + + // check if a full hierarchy sync might be necessary + if ($this->device->GetFolderUUID(false) === false) { + self::UnLinkState($this->device, false, false, $this->uuid); + throw new StateNotFoundException("No hierarchy UUID linked to device. Requesting folder resync."); + } + + $hierarchydata = $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::HIERARCHY, $this->uuid , $this->oldStateCounter, $this->deleteOldStates); + $this->device->SetHierarchyCache($hierarchydata); + return true; + } + + /** + * Saves the HierarchyCacheState of the HierarchyChache + * if this is an hierarchy operation + * + * @param boolean $forceLoad indicates if the cache should be saved also if not a hierary operation + * + * @access private + * @return boolean + * @throws StateInvalidException + */ + private function saveHierarchyCache($forceSaving = false) { + if (!$this->hierarchyOperation && !$forceSaving) + return false; + + // link the hierarchy cache again, if the UUID does not match the UUID saved in the devicedata + if (($this->uuid != $this->device->GetFolderUUID() || $forceSaving) ) + self::LinkState($this->device, $this->uuid); + + // check all folders and deleted folders to update data of ASDevice and delete old states + $hc = $this->device->getHierarchyCache(); + foreach ($hc->GetDeletedFolders() as $delfolder) + self::UnLinkState($this->device, $delfolder->serverid); + + foreach ($hc->ExportFolders() as $folder) + $this->device->SetFolderType($folder->serverid, $folder->type); + + return $this->statemachine->SetState($this->device->GetHierarchyCacheData(), $this->device->GetDeviceId(), IStateMachine::HIERARCHY, $this->uuid, $this->newStateCounter); + } + + /** + * Generates a new UUID + * + * @access private + * @return string + */ + private function getNewUuid() { + return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), + mt_rand( 0, 0x0fff ) | 0x4000, + mt_rand( 0, 0x3fff ) | 0x8000, + mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) ); + } +} +?> \ No newline at end of file diff --git a/sources/lib/core/stateobject.php b/sources/lib/core/stateobject.php new file mode 100644 index 0000000..3a280ab --- /dev/null +++ b/sources/lib/core/stateobject.php @@ -0,0 +1,268 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class StateObject implements Serializable { + private $SO_internalid; + protected $data = array(); + protected $unsetdata = array(); + protected $changed = false; + + /** + * Returns the unique id of that data object + * + * @access public + * @return array + */ + public function GetID() { + if (!isset($this->SO_internalid)) + $this->SO_internalid = sprintf('%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)); + + return $this->SO_internalid; + } + + /** + * Returns the internal array which contains all data of this object + * + * @access public + * @return array + */ + public function GetDataArray() { + return $this->data; + } + + /** + * Sets the internal array which contains all data of this object + * + * @param array $data the data to be written + * @param boolean $markAsChanged (opt) indicates if the object should be marked as "changed", default false + * + * @access public + * @return array + */ + public function SetDataArray($data, $markAsChanged = false) { + $this->data = $data; + $this->changed = $markAsChanged; + } + + /** + * Indicates if the data contained in this object was modified + * + * @access public + * @return array + */ + public function IsDataChanged() { + return $this->changed; + } + + /** + * PHP magic to set an instance variable + * + * @access public + * @return + */ + public function __set($name, $value) { + $lname = strtolower($name); + if (isset($this->data[$lname]) && is_scalar($value) && !is_array($value) && $this->data[$lname] === $value) + return false; + + $this->data[$lname] = $value; + $this->changed = true; + } + + /** + * PHP magic to get an instance variable + * if the variable was not set previousely, the value of the + * Unsetdata array is returned + * + * @access public + * @return + */ + public function __get($name) { + $lname = strtolower($name); + + if (array_key_exists($lname, $this->data)) + return $this->data[$lname]; + + if (isset($this->unsetdata) && is_array($this->unsetdata) && array_key_exists($lname, $this->unsetdata)) + return $this->unsetdata[$lname]; + + return null; + } + + /** + * PHP magic to check if an instance variable is set + * + * @access public + * @return + */ + public function __isset($name) { + return isset($this->data[strtolower($name)]); + } + + /** + * PHP magic to remove an instance variable + * + * @access public + * @return + */ + public function __unset($name) { + if (isset($this->$name)) { + unset($this->data[strtolower($name)]); + $this->changed = true; + } + } + + /** + * PHP magic to implement any getter, setter, has and delete operations + * on an instance variable. + * Methods like e.g. "SetVariableName($x)" and "GetVariableName()" are supported + * + * @access public + * @return mixed + */ + public function __call($name, $arguments) { + $name = strtolower($name); + $operator = substr($name, 0,3); + $var = substr($name,3); + + if ($operator == "set" && count($arguments) == 1){ + $this->$var = $arguments[0]; + return true; + } + + if ($operator == "set" && count($arguments) == 2 && $arguments[1] === false){ + $this->data[$var] = $arguments[0]; + return true; + } + + // getter without argument = return variable, null if not set + if ($operator == "get" && count($arguments) == 0) { + return $this->$var; + } + + // getter with one argument = return variable if set, else the argument + else if ($operator == "get" && count($arguments) == 1) { + if (isset($this->$var)) { + return $this->$var; + } + else + return $arguments[0]; + } + + if ($operator == "has" && count($arguments) == 0) + return isset($this->$var); + + if ($operator == "del" && count($arguments) == 0) { + unset($this->$var); + return true; + } + + throw new FatalNotImplementedException(sprintf("StateObject->__call('%s'): not implemented. op: {$operator} args:". count($arguments), $name)); + } + + /** + * Method to serialize a StateObject + * + * @access public + * @return array + */ + public function serialize() { + // perform tasks just before serialization + $this->preSerialize(); + + return serialize(array($this->SO_internalid,$this->data)); + } + + /** + * Method to unserialize a StateObject + * + * @access public + * @return array + * @throws StateInvalidException + */ + public function unserialize($data) { + // throw a StateInvalidException if unserialize fails + ini_set('unserialize_callback_func', 'StateObject::ThrowStateInvalidException'); + + list($this->SO_internalid, $this->data) = unserialize($data); + + // perform tasks just after unserialization + $this->postUnserialize(); + return true; + } + + /** + * Called before the StateObject is serialized + * + * @access protected + * @return boolean + */ + protected function preSerialize() { + // make sure the object has an id before serialization + $this->GetID(); + + return true; + } + + /** + * Called after the StateObject was unserialized + * + * @access protected + * @return boolean + */ + protected function postUnserialize() { + return true; + } + + /** + * Callback function for failed unserialize + * + * @access public + * @throws StateInvalidException + */ + public static function ThrowStateInvalidException() { + throw new StateInvalidException("Unserialization failed as class was not found or not compatible"); + } +} + +?> \ No newline at end of file diff --git a/sources/lib/core/streamer.php b/sources/lib/core/streamer.php new file mode 100644 index 0000000..6dc8e81 --- /dev/null +++ b/sources/lib/core/streamer.php @@ -0,0 +1,465 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class Streamer implements Serializable { + const STREAMER_VAR = 1; + const STREAMER_ARRAY = 2; + const STREAMER_TYPE = 3; + const STREAMER_PROP = 4; + const STREAMER_TYPE_DATE = 1; + const STREAMER_TYPE_HEX = 2; + const STREAMER_TYPE_DATE_DASHES = 3; + const STREAMER_TYPE_STREAM = 4; + const STREAMER_TYPE_IGNORE = 5; + const STREAMER_TYPE_SEND_EMPTY = 6; + const STREAMER_TYPE_NO_CONTAINER = 7; + const STREAMER_TYPE_COMMA_SEPARATED = 8; + const STREAMER_TYPE_SEMICOLON_SEPARATED = 9; + const STREAMER_TYPE_MULTIPART = 10; + + protected $mapping; + public $flags; + public $content; + + /** + * Constructor + * + * @param array $mapping internal mapping of variables + * @access public + */ + function Streamer($mapping) { + $this->mapping = $mapping; + $this->flags = false; + } + + /** + * Decodes the WBXML from a WBXMLdecoder until we reach the same depth level of WBXML. + * This means that if there are multiple objects at this level, then only the first is + * decoded SubOjects are auto-instantiated and decoded using the same functionality + * + * @param WBXMLDecoder $decoder + * + * @access public + */ + public function Decode(&$decoder) { + while(1) { + $entity = $decoder->getElement(); + + if($entity[EN_TYPE] == EN_TYPE_STARTTAG) { + if(! ($entity[EN_FLAGS] & EN_FLAGS_CONTENT)) { + $map = $this->mapping[$entity[EN_TAG]]; + if (isset($map[self::STREAMER_ARRAY])) { + $this->$map[self::STREAMER_VAR] = array(); + } else if(!isset($map[self::STREAMER_TYPE])) { + $this->$map[self::STREAMER_VAR] = ""; + } + else if ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE || $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE_DASHES ) { + $this->$map[self::STREAMER_VAR] = ""; + } + else if (isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_SEND_EMPTY) + $this->$map[self::STREAMER_VAR] = ""; + continue; + } + // Found a start tag + if(!isset($this->mapping[$entity[EN_TAG]])) { + // This tag shouldn't be here, abort + ZLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("Tag '%s' unexpected in type XML type '%s'", $entity[EN_TAG], get_class($this))); + return false; + } + else { + $map = $this->mapping[$entity[EN_TAG]]; + + // Handle an array + if(isset($map[self::STREAMER_ARRAY])) { + while(1) { + //do not get start tag for an array without a container + if (!(isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_NO_CONTAINER)) { + if(!$decoder->getElementStartTag($map[self::STREAMER_ARRAY])) + break; + } + if(isset($map[self::STREAMER_TYPE])) { + $decoded = new $map[self::STREAMER_TYPE]; + + $decoded->Decode($decoder); + } + else { + $decoded = $decoder->getElementContent(); + } + + if(!isset($this->$map[self::STREAMER_VAR])) + $this->$map[self::STREAMER_VAR] = array($decoded); + else + array_push($this->$map[self::STREAMER_VAR], $decoded); + + if(!$decoder->getElementEndTag()) //end tag of a container element + return false; + + if (isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_NO_CONTAINER) { + $e = $decoder->peek(); + //go back to the initial while if another block of no container elements is found + if ($e[EN_TYPE] == EN_TYPE_STARTTAG) { + continue 2; + } + //break on end tag because no container elements block end is reached + if ($e[EN_TYPE] == EN_TYPE_ENDTAG) + break; + if (empty($e)) + break; + } + } + //do not get end tag for an array without a container + if (!(isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_NO_CONTAINER)) { + if(!$decoder->getElementEndTag()) //end tag of container + return false; + } + } + else { // Handle single value + if(isset($map[self::STREAMER_TYPE])) { + // Complex type, decode recursively + if($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE || $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE_DASHES) { + $decoded = $this->parseDate($decoder->getElementContent()); + if(!$decoder->getElementEndTag()) + return false; + } + else if($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_HEX) { + $decoded = hex2bin($decoder->getElementContent()); + if(!$decoder->getElementEndTag()) + return false; + } + // explode comma or semicolon strings into arrays + else if($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_COMMA_SEPARATED || $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_SEMICOLON_SEPARATED) { + $glue = ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_COMMA_SEPARATED)?", ":"; "; + $decoded = explode($glue, $decoder->getElementContent()); + if(!$decoder->getElementEndTag()) + return false; + } + else { + $subdecoder = new $map[self::STREAMER_TYPE](); + if($subdecoder->Decode($decoder) === false) + return false; + + $decoded = $subdecoder; + + if(!$decoder->getElementEndTag()) { + ZLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("No end tag for '%s'", $entity[EN_TAG])); + return false; + } + } + } + else { + // Simple type, just get content + $decoded = $decoder->getElementContent(); + + if($decoded === false) { + // the tag is declared to have content, but no content is available. + // set an empty content + $decoded = ""; + } + + if(!$decoder->getElementEndTag()) { + ZLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("Unable to get end tag for '%s'", $entity[EN_TAG])); + return false; + } + } + // $decoded now contains data object (or string) + $this->$map[self::STREAMER_VAR] = $decoded; + } + } + } + else if($entity[EN_TYPE] == EN_TYPE_ENDTAG) { + $decoder->ungetElement($entity); + break; + } + else { + ZLog::Write(LOGLEVEL_WBXMLSTACK, "Unexpected content in type"); + break; + } + } + } + + /** + * Encodes this object and any subobjects - output is ordered according to mapping + * + * @param WBXMLEncoder $encoder + * + * @access public + */ + public function Encode(&$encoder) { + // A return value if anything was streamed. We need for empty tags. + $streamed = false; + foreach($this->mapping as $tag => $map) { + if(isset($this->$map[self::STREAMER_VAR])) { + // Variable is available + if(is_object($this->$map[self::STREAMER_VAR])) { + // Subobjects can do their own encoding + if ($this->$map[self::STREAMER_VAR] instanceof Streamer) { + $encoder->startTag($tag); + $res = $this->$map[self::STREAMER_VAR]->Encode($encoder); + $encoder->endTag(); + // nothing was streamed in previous encode but it should be streamed empty anyway + if (!$res && isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_SEND_EMPTY) + $encoder->startTag($tag, false, true); + } + else + ZLog::Write(LOGLEVEL_ERROR, sprintf("Streamer->Encode(): parameter '%s' of object %s is not of type Streamer", $map[self::STREAMER_VAR], get_class($this))); + } + // Array of objects + else if(isset($map[self::STREAMER_ARRAY])) { + if (empty($this->$map[self::STREAMER_VAR]) && isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_SEND_EMPTY) { + $encoder->startTag($tag, false, true); + } + else { + // Outputs array container (eg Attachments) + // Do not output start and end tag when type is STREAMER_TYPE_NO_CONTAINER + if (!isset($map[self::STREAMER_PROP]) || $map[self::STREAMER_PROP] != self::STREAMER_TYPE_NO_CONTAINER) + $encoder->startTag($tag); + + foreach ($this->$map[self::STREAMER_VAR] as $element) { + if(is_object($element)) { + $encoder->startTag($map[self::STREAMER_ARRAY]); // Outputs object container (eg Attachment) + $element->Encode($encoder); + $encoder->endTag(); + } + else { + if(strlen($element) == 0) + // Do not output empty items. Not sure if we should output an empty tag with $encoder->startTag($map[self::STREAMER_ARRAY], false, true); + ; + else { + $encoder->startTag($map[self::STREAMER_ARRAY]); + $encoder->content($element); + $encoder->endTag(); + $streamed = true; + } + } + } + + if (!isset($map[self::STREAMER_PROP]) || $map[self::STREAMER_PROP] != self::STREAMER_TYPE_NO_CONTAINER) + $encoder->endTag(); + } + } + else { + if(isset($map[self::STREAMER_TYPE]) && $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_IGNORE) { + continue; + } + + if ($encoder->getMultipart() && isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_MULTIPART) { + $encoder->addBodypartStream($this->$map[self::STREAMER_VAR]); + $encoder->startTag(SYNC_ITEMOPERATIONS_PART); + $encoder->content(count($encoder->getBodypartsCount())); + $encoder->endTag(); + continue; + } + + // Simple type + if(!isset($map[self::STREAMER_TYPE]) && strlen($this->$map[self::STREAMER_VAR]) == 0) { + // send empty tags + if (isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_SEND_EMPTY) + $encoder->startTag($tag, false, true); + + // Do not output empty items. See above: $encoder->startTag($tag, false, true); + continue; + } else + $encoder->startTag($tag); + + if(isset($map[self::STREAMER_TYPE]) && ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE || $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE_DASHES)) { + if($this->$map[self::STREAMER_VAR] != 0) // don't output 1-1-1970 + $encoder->content($this->formatDate($this->$map[self::STREAMER_VAR], $map[self::STREAMER_TYPE])); + } + else if(isset($map[self::STREAMER_TYPE]) && $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_HEX) { + $encoder->content(strtoupper(bin2hex($this->$map[self::STREAMER_VAR]))); + } + else if(isset($map[self::STREAMER_TYPE]) && $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_STREAM) { + //encode stream with base64 + $stream = $this->$map[self::STREAMER_VAR]; + $stat = fstat($stream); + // the padding size muss be calculated for the entire stream, + // the base64 filter seems to process 8192 byte chunks correctly itself + $padding = (isset($stat['size']) && $stat['size'] > 8192) ? ($stat['size'] % 3) : 0; + + $paddingfilter = stream_filter_append($stream, 'padding.'.$padding); + $base64filter = stream_filter_append($stream, 'convert.base64-encode'); + $d = ""; + while (!feof($stream)) { + $d .= fgets($stream, 4096); + } + $encoder->content($d); + stream_filter_remove($base64filter); + stream_filter_remove($paddingfilter); + } + // implode comma or semicolon arrays into a string + else if(isset($map[self::STREAMER_TYPE]) && is_array($this->$map[self::STREAMER_VAR]) && + ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_COMMA_SEPARATED || $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_SEMICOLON_SEPARATED)) { + $glue = ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_COMMA_SEPARATED)?", ":"; "; + $encoder->content(implode($glue, $this->$map[self::STREAMER_VAR])); + } + else { + $encoder->content($this->$map[self::STREAMER_VAR]); + } + $encoder->endTag(); + $streamed = true; + } + } + } + // Output our own content + if(isset($this->content)) + $encoder->content($this->content); + + return $streamed; + } + + /** + * Removes not necessary data from the object + * + * @access public + * @return boolean + */ + public function StripData() { + foreach ($this->mapping as $k=>$v) { + if (isset($this->$v[self::STREAMER_VAR])) { + if (is_object($this->$v[self::STREAMER_VAR]) && method_exists($this->$v[self::STREAMER_VAR], "StripData") ) { + $this->$v[self::STREAMER_VAR]->StripData(); + } + else if (isset($v[self::STREAMER_ARRAY]) && !empty($this->$v[self::STREAMER_VAR])) { + foreach ($this->$v[self::STREAMER_VAR] as $element) { + if (is_object($element) && method_exists($element, "StripData") ) { + $element->StripData(); + } + } + } + } + } + unset($this->mapping); + + return true; + } + + /** + * Method to serialize a Streamer and respective SyncObject + * + * @access public + * @return array + */ + public function serialize() { + $values = array(); + foreach ($this->mapping as $k=>$v) { + if (isset($this->$v[self::STREAMER_VAR])) + $values[$v[self::STREAMER_VAR]] = serialize($this->$v[self::STREAMER_VAR]); + } + + return serialize($values); + } + + /** + * Method to unserialize a Streamer and respective SyncObject + * + * @access public + * @return array + */ + public function unserialize($data) { + $class = get_class($this); + $this->$class(); + $values = unserialize($data); + foreach ($values as $k=>$v) + $this->$k = unserialize($v); + + return true; + } + + /**---------------------------------------------------------------------------------------------------------- + * Private methods for conversion + */ + + /** + * Formats a timestamp + * Oh yeah, this is beautiful. Exchange outputs date fields differently in calendar items + * and emails. We could just always send one or the other, but unfortunately nokia's 'Mail for + * exchange' depends on this quirk. So we have to send a different date type depending on where + * it's used. Sigh. + * + * @param long $ts + * @param int $type + * + * @access private + * @return string + */ + private function formatDate($ts, $type) { + if($type == self::STREAMER_TYPE_DATE) + return gmstrftime("%Y%m%dT%H%M%SZ", $ts); + else if($type == self::STREAMER_TYPE_DATE_DASHES) + return gmstrftime("%Y-%m-%dT%H:%M:%S.000Z", $ts); + } + + /** + * Transforms an AS timestamp into a unix timestamp + * + * @param string $ts + * + * @access private + * @return long + */ + function parseDate($ts) { + if(preg_match("/(\d{4})[^0-9]*(\d{2})[^0-9]*(\d{2})(T(\d{2})[^0-9]*(\d{2})[^0-9]*(\d{2})(.\d+)?Z){0,1}$/", $ts, $matches)) { + if ($matches[1] >= 2038){ + $matches[1] = 2038; + $matches[2] = 1; + $matches[3] = 18; + $matches[5] = $matches[6] = $matches[7] = 0; + } + + if (!isset($matches[5])) $matches[5] = 0; + if (!isset($matches[6])) $matches[6] = 0; + if (!isset($matches[7])) $matches[7] = 0; + + return gmmktime($matches[5], $matches[6], $matches[7], $matches[2], $matches[3], $matches[1]); + } + return 0; + } +} + +?> \ No newline at end of file diff --git a/sources/lib/core/streamimporter.php b/sources/lib/core/streamimporter.php new file mode 100644 index 0000000..83ba64c --- /dev/null +++ b/sources/lib/core/streamimporter.php @@ -0,0 +1,261 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class ImportChangesStream implements IImportChanges { + private $encoder; + private $objclass; + private $seenObjects; + private $importedMsgs; + private $checkForIgnoredMessages; + + /** + * Constructor of the StreamImporter + * + * @param WBXMLEncoder $encoder Objects are streamed to this encoder + * @param SyncObject $class SyncObject class (only these are accepted when streaming content messages) + * + * @access public + */ + public function ImportChangesStream(&$encoder, $class) { + $this->encoder = &$encoder; + $this->objclass = $class; + $this->classAsString = (is_object($class))?get_class($class):''; + $this->seenObjects = array(); + $this->importedMsgs = 0; + $this->checkForIgnoredMessages = true; + } + + /** + * Implement interface - never used + */ + public function Config($state, $flags = 0) { return true; } + public function ConfigContentParameters($contentparameters) { return true; } + public function GetState() { return false;} + public function LoadConflicts($contentparameters, $state) { return true; } + + /** + * Imports a single message + * + * @param string $id + * @param SyncObject $message + * + * @access public + * @return boolean + */ + public function ImportMessageChange($id, $message) { + // ignore other SyncObjects + if(!($message instanceof $this->classAsString)) + return false; + + // prevent sending the same object twice in one request + if (in_array($id, $this->seenObjects)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Object '%s' discarded! Object already sent in this request.", $id)); + return true; + } + + $this->importedMsgs++; + $this->seenObjects[] = $id; + + // checks if the next message may cause a loop or is broken + if (ZPush::GetDeviceManager()->DoNotStreamMessage($id, $message)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesStream->ImportMessageChange('%s'): message ignored and requested to be removed from mobile", $id)); + + // this is an internal operation & should not trigger an update in the device manager + $this->checkForIgnoredMessages = false; + $stat = $this->ImportMessageDeletion($id); + $this->checkForIgnoredMessages = true; + + return $stat; + } + + if ($message->flags === false || $message->flags === SYNC_NEWMESSAGE) + $this->encoder->startTag(SYNC_ADD); + else { + // on update of an SyncEmail we only export the flags + if($message instanceof SyncMail && isset($message->flag) && $message->flag instanceof SyncMailFlags) { + $newmessage = new SyncMail(); + $newmessage->read = $message->read; + $newmessage->flag = $message->flag; + if (isset($message->lastverbexectime)) $newmessage->lastverbexectime = $message->lastverbexectime; + if (isset($message->lastverbexecuted)) $newmessage->lastverbexecuted = $message->lastverbexecuted; + $message = $newmessage; + unset($newmessage); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesStream->ImportMessageChange('%s'): SyncMail message updated. Message content is striped, only flags are streamed.", $id)); + } + + $this->encoder->startTag(SYNC_MODIFY); + } + $this->encoder->startTag(SYNC_SERVERENTRYID); + $this->encoder->content($id); + $this->encoder->endTag(); + $this->encoder->startTag(SYNC_DATA); + $message->Encode($this->encoder); + $this->encoder->endTag(); + $this->encoder->endTag(); + + return true; + } + + /** + * Imports a deletion + * + * @param string $id + * + * @access public + * @return boolean + */ + public function ImportMessageDeletion($id) { + if ($this->checkForIgnoredMessages) { + ZPush::GetDeviceManager()->RemoveBrokenMessage($id); + } + + $this->importedMsgs++; + $this->encoder->startTag(SYNC_REMOVE); + $this->encoder->startTag(SYNC_SERVERENTRYID); + $this->encoder->content($id); + $this->encoder->endTag(); + $this->encoder->endTag(); + + return true; + } + + /** + * Imports a change in 'read' flag + * Can only be applied to SyncMail (Email) requests + * + * @param string $id + * @param int $flags - read/unread + * + * @access public + * @return boolean + */ + public function ImportMessageReadFlag($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_READ); + $this->encoder->content($flags); + $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 + * + * @param string $id + * @param int $flags read/unread + * + * @access public + * @return boolean + */ + public function ImportMessageMove($id, $newfolder) { + return true; + } + + /** + * Imports a change on a folder + * + * @param object $folder SyncFolder + * + * @access public + * @return string id of the folder + */ + public function ImportFolderChange($folder) { + // checks if the next message may cause a loop or is broken + if (ZPush::GetDeviceManager(false) && ZPush::GetDeviceManager()->DoNotStreamMessage($folder->serverid, $folder)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesStream->ImportFolderChange('%s'): folder ignored as requested by DeviceManager.", $folder->serverid)); + return true; + } + + // send a modify flag if the folder is already known on the device + if (isset($folder->flags) && $folder->flags === SYNC_NEWMESSAGE) + $this->encoder->startTag(SYNC_FOLDERHIERARCHY_ADD); + else + $this->encoder->startTag(SYNC_FOLDERHIERARCHY_UPDATE); + + $folder->Encode($this->encoder); + $this->encoder->endTag(); + + return true; + } + + /** + * Imports a folder deletion + * + * @param string $id + * @param string $parent id + * + * @access public + * @return boolean + */ + public function ImportFolderDeletion($id, $parent = false) { + $this->encoder->startTag(SYNC_FOLDERHIERARCHY_REMOVE); + $this->encoder->startTag(SYNC_FOLDERHIERARCHY_SERVERENTRYID); + $this->encoder->content($id); + $this->encoder->endTag(); + $this->encoder->endTag(); + + return true; + } + + /** + * Returns the number of messages which were changed, deleted and had changed read status + * + * @access public + * @return int + */ + public function GetImportedMessages() { + return $this->importedMsgs; + } +} +?> \ No newline at end of file diff --git a/sources/lib/core/synccollections.php b/sources/lib/core/synccollections.php new file mode 100644 index 0000000..54f7a99 --- /dev/null +++ b/sources/lib/core/synccollections.php @@ -0,0 +1,714 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncCollections implements Iterator { + const ERROR_NO_COLLECTIONS = 1; + const ERROR_WRONG_HIERARCHY = 2; + const OBSOLETE_CONNECTION = 3; + + private $stateManager; + + private $collections = array(); + private $addparms = array(); + private $changes = array(); + private $saveData = true; + + private $refPolicyKey = false; + private $refLifetime = false; + + private $globalWindowSize; + private $lastSyncTime; + + private $waitingTime = 0; + + + /** + * Constructor + */ + public function SyncCollections() { + } + + /** + * Sets the StateManager for this object + * If this is not done and a method needs it, the StateManager will be + * requested from the DeviceManager + * + * @param StateManager $statemanager + * + * @access public + * @return + */ + public function SetStateManager($statemanager) { + $this->stateManager = $statemanager; + } + + /** + * Loads all collections known for the current device + * + * @param boolean $overwriteLoaded (opt) overwrites Collection with saved state if set to true + * @param boolean $loadState (opt) indicates if the collection sync state should be loaded, default true + * @param boolean $checkPermissions (opt) if set to true each folder will pass + * through a backend->Setup() to check permissions. + * If this fails a StatusException will be thrown. + * + * @access public + * @throws StatusException with SyncCollections::ERROR_WRONG_HIERARCHY if permission check fails + * @throws StateNotFoundException if the sync state can not be found ($loadState = true) + * @return boolean + */ + public function LoadAllCollections($overwriteLoaded = false, $loadState = false, $checkPermissions = false) { + $this->loadStateManager(); + + // this operation should not remove old state counters + $this->stateManager->DoNotDeleteOldStates(); + + $invalidStates = false; + foreach($this->stateManager->GetSynchedFolders() as $folderid) { + if ($overwriteLoaded === false && isset($this->collections[$folderid])) + continue; + + // Load Collection! + if (! $this->LoadCollection($folderid, $loadState, $checkPermissions)) + $invalidStates = true; + } + + if ($invalidStates) + throw new StateInvalidException("Invalid states found while loading collections. Forcing sync"); + + return true; + } + + /** + * Loads all collections known for the current device + * + * @param string $folderid folder id to be loaded + * @param boolean $loadState (opt) indicates if the collection sync state should be loaded, default true + * @param boolean $checkPermissions (opt) if set to true each folder will pass + * through a backend->Setup() to check permissions. + * If this fails a StatusException will be thrown. + * + * @access public + * @throws StatusException with SyncCollections::ERROR_WRONG_HIERARCHY if permission check fails + * @throws StateNotFoundException if the sync state can not be found ($loadState = true) + * @return boolean + */ + public function LoadCollection($folderid, $loadState = false, $checkPermissions = false) { + $this->loadStateManager(); + + try { + // Get SyncParameters for the folder from the state + $spa = $this->stateManager->GetSynchedFolderState($folderid); + + // TODO remove resync of folders for < Z-Push 2 beta4 users + // this forces a resync of all states previous to Z-Push 2 beta4 + if (! $spa instanceof SyncParameters) + throw new StateInvalidException("Saved state are not of type SyncParameters"); + } + catch (StateInvalidException $sive) { + // in case there is something wrong with the state, just stop here + // later when trying to retrieve the SyncParameters nothing will be found + + // we also generate a fake change, so a sync on this folder is triggered + $this->changes[$folderid] = 1; + + return false; + } + + // if this is an additional folder the backend has to be setup correctly + if ($checkPermissions === true && ! ZPush::GetBackend()->Setup(ZPush::GetAdditionalSyncFolderStore($spa->GetFolderId()))) + throw new StatusException(sprintf("SyncCollections->LoadCollection(): could not Setup() the backend for folder id '%s'", $spa->GetFolderId()), self::ERROR_WRONG_HIERARCHY); + + // add collection to object + $addStatus = $this->AddCollection($spa); + + // load the latest known syncstate if requested + if ($addStatus && $loadState === true) + $this->addparms[$folderid]["state"] = $this->stateManager->GetSyncState($spa->GetLatestSyncKey()); + + return $addStatus; + } + + /** + * Saves a SyncParameters Object + * + * @param SyncParamerts $spa + * + * @access public + * @return boolean + */ + public function SaveCollection($spa) { + if (! $this->saveData || !$spa->HasFolderId()) + return false; + + if ($spa->IsDataChanged()) { + $this->loadStateManager(); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->SaveCollection(): Data of folder '%s' changed", $spa->GetFolderId())); + + // save new windowsize + if (isset($this->globalWindowSize)) + $spa->SetWindowSize($this->globalWindowSize); + + // update latest lifetime + if (isset($this->refLifetime)) + $spa->SetReferenceLifetime($this->refLifetime); + + return $this->stateManager->SetSynchedFolderState($spa); + } + return false; + } + + /** + * Adds a SyncParameters object to the current list of collections + * + * @param SyncParameters $spa + * + * @access public + * @return boolean + */ + public function AddCollection($spa) { + if (! $spa->HasFolderId()) + return false; + + $this->collections[$spa->GetFolderId()] = $spa; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->AddCollection(): Folder id '%s' : ref. PolicyKey '%s', ref. Lifetime '%s', last sync at '%s'", $spa->GetFolderId(), $spa->GetReferencePolicyKey(), $spa->GetReferenceLifetime(), $spa->GetLastSyncTime())); + if ($spa->HasLastSyncTime() && $spa->GetLastSyncTime() > $this->lastSyncTime) { + $this->lastSyncTime = $spa->GetLastSyncTime(); + + // use SyncParameters PolicyKey as reference if available + if ($spa->HasReferencePolicyKey()) + $this->refPolicyKey = $spa->GetReferencePolicyKey(); + + // use SyncParameters LifeTime as reference if available + if ($spa->HasReferenceLifetime()) + $this->refLifetime = $spa->GetReferenceLifetime(); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->AddCollection(): Updated reference PolicyKey '%s', reference Lifetime '%s', Last sync at '%s'", $this->refPolicyKey, $this->refLifetime, $this->lastSyncTime)); + } + + return true; + } + + /** + * Returns a previousily added or loaded SyncParameters object for a folderid + * + * @param SyncParameters $spa + * + * @access public + * @return SyncParameters / boolean false if no SyncParameters object is found for folderid + */ + public function GetCollection($folderid) { + if (isset($this->collections[$folderid])) + return $this->collections[$folderid]; + else + return false; + } + + /** + * Indicates if there are any loaded CPOs + * + * @access public + * @return boolean + */ + public function HasCollections() { + return ! empty($this->collections); + } + + /** + * Add a non-permanent key/value pair for a SyncParameters object + * + * @param SyncParameters $spa target SyncParameters + * @param string $key + * @param mixed $value + * + * @access public + * @return boolean + */ + public function AddParameter($spa, $key, $value) { + if (!$spa->HasFolderId()) + return false; + + $folderid = $spa->GetFolderId(); + if (!isset($this->addparms[$folderid])) + $this->addparms[$folderid] = array(); + + $this->addparms[$folderid][$key] = $value; + return true; + } + + /** + * Returns a previousily set non-permanent value for a SyncParameters object + * + * @param SyncParameters $spa target SyncParameters + * @param string $key + * + * @access public + * @return mixed returns 'null' if nothing set + */ + public function GetParameter($spa, $key) { + if (!$spa->HasFolderId()) + return null; + + if (isset($this->addparms[$spa->GetFolderId()]) && isset($this->addparms[$spa->GetFolderId()][$key])) + return $this->addparms[$spa->GetFolderId()][$key]; + else + return null; + } + + /** + * Returns the latest known PolicyKey to be used as reference + * + * @access public + * @return int/boolen returns false if nothing found in collections + */ + public function GetReferencePolicyKey() { + return $this->refPolicyKey; + } + + /** + * Sets a global window size which should be used for all collections + * in a case of a heartbeat and/or partial sync + * + * @param int $windowsize + * + * @access public + * @return boolean + */ + public function SetGlobalWindowSize($windowsize) { + $this->globalWindowSize = $windowsize; + return true; + } + + /** + * Returns the global window size which should be used for all collections + * in a case of a heartbeat and/or partial sync + * + * @access public + * @return int/boolean returns false if not set or not available + */ + public function GetGlobalWindowSize() { + if (!isset($this->globalWindowSize)) + return false; + + return $this->globalWindowSize; + } + + /** + * Sets the lifetime for heartbeat or ping connections + * + * @param int $lifetime time in seconds + * + * @access public + * @return boolean + */ + public function SetLifetime($lifetime) { + $this->refLifetime = $lifetime; + return true; + } + + /** + * Sets the lifetime for heartbeat or ping connections + * previousily set or saved in a collection + * + * @access public + * @return int returns 600 as default if nothing set or not available + */ + public function GetLifetime() { + if (!isset( $this->refLifetime) || $this->refLifetime === false) + return 600; + + return $this->refLifetime; + } + + /** + * Returns the timestamp of the last synchronization for all + * loaded collections + * + * @access public + * @return int timestamp + */ + public function GetLastSyncTime() { + return $this->lastSyncTime; + } + + /** + * Checks if the currently known collections for changes for $lifetime seconds. + * If the backend provides a ChangesSink the sink will be used. + * If not every $interval seconds an exporter will be configured for each + * folder to perform GetChangeCount(). + * + * @param int $lifetime (opt) total lifetime to wait for changes / default 600s + * @param int $interval (opt) time between blocking operations of sink or polling / default 30s + * @param boolean $onlyPingable (opt) only check for folders which have the PingableFlag + * + * @access public + * @return boolean indicating if changes were found + * @throws StatusException with code SyncCollections::ERROR_NO_COLLECTIONS if no collections available + * with code SyncCollections::ERROR_WRONG_HIERARCHY if there were errors getting changes + */ + public function CheckForChanges($lifetime = 600, $interval = 30, $onlyPingable = false) { + $classes = array(); + foreach ($this->collections as $folderid => $spa){ + if ($onlyPingable && $spa->GetPingableFlag() !== true) + continue; + + if (!isset($classes[$spa->GetContentClass()])) + $classes[$spa->GetContentClass()] = 0; + $classes[$spa->GetContentClass()] += 1; + } + if (empty($classes)) + $checkClasses = "policies only"; + else if (array_sum($classes) > 4) { + $checkClasses = ""; + foreach($classes as $class=>$count) { + if ($count == 1) + $checkClasses .= sprintf("%s ", $class); + else + $checkClasses .= sprintf("%s(%d) ", $class, $count); + } + } + else + $checkClasses = implode(" ", array_keys($classes)); + + $pingTracking = new PingTracking(); + $this->changes = array(); + $changesAvailable = false; + + ZPush::GetDeviceManager()->AnnounceProcessAsPush(); + ZPush::GetTopCollector()->AnnounceInformation(sprintf("lifetime %ds", $lifetime), true); + ZLog::Write(LOGLEVEL_INFO, sprintf("SyncCollections->CheckForChanges(): Waiting for %s changes... (lifetime %d seconds)", (empty($classes))?'policy':'store', $lifetime)); + + // use changes sink where available + $changesSink = false; + $forceRealExport = 0; + // do not create changessink if there are no folders + if (!empty($classes) && ZPush::GetBackend()->HasChangesSink()) { + $changesSink = true; + + // initialize all possible folders + foreach ($this->collections as $folderid => $spa) { + if ($onlyPingable && $spa->GetPingableFlag() !== true) + continue; + + // switch user store if this is a additional folder and initialize sink + ZPush::GetBackend()->Setup(ZPush::GetAdditionalSyncFolderStore($folderid)); + if (! ZPush::GetBackend()->ChangesSinkInitialize($folderid)) + throw new StatusException(sprintf("Error initializing ChangesSink for folder id '%s'", $folderid), self::ERROR_WRONG_HIERARCHY); + } + } + + // wait for changes + $started = time(); + $endat = time() + $lifetime; + while(($now = time()) < $endat) { + // how long are we waiting for changes + $this->waitingTime = $now-$started; + + $nextInterval = $interval; + // we should not block longer than the lifetime + if ($endat - $now < $nextInterval) + $nextInterval = $endat - $now; + + // 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)) + // the hierarchysync forces provisioning + throw new StatusException("SyncCollections->CheckForChanges(): PolicyKey changed. Provisioning required.", self::ERROR_WRONG_HIERARCHY); + + // Check if a hierarchy sync is necessary + if (ZPush::GetDeviceManager()->IsHierarchySyncRequired()) + throw new StatusException("SyncCollections->CheckForChanges(): HierarchySync required.", self::ERROR_WRONG_HIERARCHY); + + // Check if there are newer requests + // If so, this process should be terminated if more than 60 secs to go + if ($pingTracking->DoForcePingTimeout()) { + // do not update CPOs because another process has already read them! + $this->saveData = false; + + // more than 60 secs to go? + if (($now + 60) < $endat) { + ZPush::GetTopCollector()->AnnounceInformation(sprintf("Forced timeout after %ds", ($now-$started)), true); + throw new StatusException(sprintf("SyncCollections->CheckForChanges(): Timeout forced after %ss from %ss due to other process", ($now-$started), $lifetime), self::OBSOLETE_CONNECTION); + } + } + + // Use changes sink if available + if ($changesSink) { + // in some occasions we do realize a full export to see if there are pending changes + // every 5 minutes this is also done to see if there were "missed" notifications + if (SINK_FORCERECHECK !== false && $forceRealExport+SINK_FORCERECHECK <= $now) { + if ($this->CountChanges($onlyPingable)) { + ZLog::Write(LOGLEVEL_DEBUG, "SyncCollections->CheckForChanges(): Using ChangesSink but found relevant changes on regular export"); + return true; + } + $forceRealExport = $now; + } + + ZPush::GetTopCollector()->AnnounceInformation(sprintf("Sink %d/%ds on %s", ($now-$started), $lifetime, $checkClasses)); + $notifications = ZPush::GetBackend()->ChangesSink($nextInterval); + + $validNotifications = false; + foreach ($notifications as $folderid) { + // check if the notification on the folder is within our filter + if ($this->CountChange($folderid)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Notification received on folder '%s'", $folderid)); + $validNotifications = true; + $this->waitingTime = time()-$started; + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Notification received on folder '%s', but it is not relevant", $folderid)); + } + } + if ($validNotifications) + return true; + } + // use polling mechanism + else { + ZPush::GetTopCollector()->AnnounceInformation(sprintf("Polling %d/%ds on %s", ($now-$started), $lifetime, $checkClasses)); + if ($this->CountChanges($onlyPingable)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Found changes polling")); + return true; + } + else { + sleep($nextInterval); + } + } // end polling + } // end wait for changes + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): no changes found after %ds", time() - $started)); + + return false; + } + + /** + * Checks if the currently known collections for + * changes performing Exporter->GetChangeCount() + * + * @param boolean $onlyPingable (opt) only check for folders which have the PingableFlag + * + * @access public + * @return boolean indicating if changes were found or not + */ + public function CountChanges($onlyPingable = false) { + $changesAvailable = false; + foreach ($this->collections as $folderid => $spa) { + if ($onlyPingable && $spa->GetPingableFlag() !== true) + continue; + + if (isset($this->addparms[$spa->GetFolderId()]["status"]) && $this->addparms[$spa->GetFolderId()]["status"] != SYNC_STATUS_SUCCESS) + continue; + + if ($this->CountChange($folderid)) + $changesAvailable = true; + } + + return $changesAvailable; + } + + /** + * Checks a folder for changes performing Exporter->GetChangeCount() + * + * @param string $folderid counts changes for a folder + * + * @access private + * @return boolean indicating if changes were found or not + */ + private function CountChange($folderid) { + $spa = $this->GetCollection($folderid); + + // switch user store if this is a additional folder (additional true -> do not debug) + ZPush::GetBackend()->Setup(ZPush::GetAdditionalSyncFolderStore($folderid, true)); + $changecount = false; + + try { + $exporter = ZPush::GetBackend()->GetExporter($folderid); + if ($exporter !== false && isset($this->addparms[$folderid]["state"])) { + $importer = false; + + $exporter->Config($this->addparms[$folderid]["state"], BACKEND_DISCARD_DATA); + $exporter->ConfigContentParameters($spa->GetCPO()); + $ret = $exporter->InitializeExporter($importer); + + if ($ret !== false) + $changecount = $exporter->GetChangeCount(); + } + } + catch (StatusException $ste) { + throw new StatusException("SyncCollections->CountChange(): exporter can not be re-configured.", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN); + } + + // start over if exporter can not be configured atm + if ($changecount === false ) + ZLog::Write(LOGLEVEL_WARN, "SyncCollections->CountChange(): no changes received from Exporter."); + + $this->changes[$folderid] = $changecount; + + if(isset($this->addparms[$folderid]['savestate'])) { + try { + // Discard any data + while(is_array($exporter->Synchronize())); + $this->addparms[$folderid]['savestate'] = $exporter->GetState(); + } + catch (StatusException $ste) { + throw new StatusException("SyncCollections->CountChange(): could not get new state from exporter", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN); + } + } + + return ($changecount > 0); + } + + /** + * Returns an array with all folderid and the amount of changes found + * + * @access public + * @return array + */ + public function GetChangedFolderIds() { + return $this->changes; + } + + /** + * Indicates if there are folders which are pingable + * + * @access public + * @return boolean + */ + public function PingableFolders() { + $pingable = false; + + foreach ($this->collections as $folderid => $spa) { + if ($spa->GetPingableFlag() == true) + $pingable = true; + } + + return $pingable; + } + + /** + * Indicates if the process did wait in a sink, polling or before running a + * regular export to find changes + * + * @access public + * @return array + */ + public function WaitedForChanges() { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->WaitedForChanges: waited for %d seconds", $this->waitingTime)); + return ($this->waitingTime > 0); + } + + /** + * Simple Iterator Interface implementation to traverse through collections + */ + + /** + * Rewind the Iterator to the first element + * + * @access public + * @return + */ + public function rewind() { + return reset($this->collections); + } + + /** + * Returns the current element + * + * @access public + * @return mixed + */ + public function current() { + return current($this->collections); + } + + /** + * Return the key of the current element + * + * @access public + * @return scalar on success, or NULL on failure. + */ + public function key() { + return key($this->collections); + } + + /** + * Move forward to next element + * + * @access public + * @return + */ + public function next() { + return next($this->collections); + } + + /** + * Checks if current position is valid + * + * @access public + * @return boolean + */ + public function valid() { + return (key($this->collections) !== null); + } + + /** + * Gets the StateManager from the DeviceManager + * if it's not available + * + * @access private + * @return + */ + private function loadStateManager() { + if (!isset($this->stateManager)) + $this->stateManager = ZPush::GetDeviceManager()->GetStateManager(); + } +} + +?> \ No newline at end of file diff --git a/sources/lib/core/syncparameters.php b/sources/lib/core/syncparameters.php new file mode 100644 index 0000000..7029ad9 --- /dev/null +++ b/sources/lib/core/syncparameters.php @@ -0,0 +1,419 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncParameters extends StateObject { + const DEFAULTOPTIONS = "DEFAULT"; + const EMAILOPTIONS = "EMAIL"; + const CALENDAROPTIONS = "CALENDAR"; + const CONTACTOPTIONS = "CONTACTS"; + const NOTEOPTIONS = "NOTES"; + const TASKOPTIONS = "TASKS"; + const SMSOPTIONS = "SMS"; + + private $synckeyChanged = false; + private $currentCPO = self::DEFAULTOPTIONS; + + protected $unsetdata = array( + 'uuid' => false, + 'uuidcounter' => false, + 'uuidnewcounter' => false, + 'folderid' => false, + 'referencelifetime' => 10, + 'lastsynctime' => false, + 'referencepolicykey' => true, + 'pingableflag' => false, + 'contentclass' => false, + 'deletesasmoves' => false, + 'conversationmode' => false, + 'windowsize' => 5, + 'contentparameters' => array(), + 'foldersynctotal' => false, + 'foldersyncremaining' => false, + ); + + /** + * SyncParameters constructor + */ + public function SyncParameters() { + // initialize ContentParameters for the current option + $this->checkCPO(); + } + + + /** + * SyncKey methods + * + * The current and next synckey is saved as uuid and counter + * so partial and ping can access the latest states. + */ + + /** + * Returns the latest SyncKey of this folder + * + * @access public + * @return string/boolean false if no uuid/counter available + */ + public function GetSyncKey() { + if (isset($this->uuid) && isset($this->uuidCounter)) + return StateManager::BuildStateKey($this->uuid, $this->uuidCounter); + + return false; + } + + /** + * Sets the the current synckey. + * This is done by parsing it and saving uuid and counter. + * By setting the current key, the "next" key is obsolete + * + * @param string $synckey + * + * @access public + * @return boolean + */ + public function SetSyncKey($synckey) { + list($this->uuid, $this->uuidCounter) = StateManager::ParseStateKey($synckey); + + // remove newSyncKey + unset($this->uuidNewCounter); + + return true; + } + + /** + * Indicates if this folder has a synckey + * + * @access public + * @return booleans + */ + public function HasSyncKey() { + return (isset($this->uuid) && isset($this->uuidCounter)); + } + + /** + * Sets the the next synckey. + * This is done by parsing it and saving uuid and next counter. + * if the folder has no synckey until now (new sync), the next counter becomes current asl well. + * + * @param string $synckey + * + * @access public + * @throws FatalException if the uuids of current and next do not match + * @return boolean + */ + public function SetNewSyncKey($synckey) { + list($uuid, $uuidNewCounter) = StateManager::ParseStateKey($synckey); + if (!$this->HasSyncKey()) { + $this->uuid = $uuid; + $this->uuidCounter = $uuidNewCounter; + } + else if ($uuid !== $this->uuid) + throw new FatalException("SyncParameters->SetNewSyncKey(): new SyncKey must have the same UUID as current SyncKey"); + + $this->uuidNewCounter = $uuidNewCounter; + $this->synckeyChanged = true; + } + + /** + * Returns the next synckey + * + * @access public + * @return string/boolean returns false if uuid or counter are not available + */ + public function GetNewSyncKey() { + if (isset($this->uuid) && isset($this->uuidNewCounter)) + return StateManager::BuildStateKey($this->uuid, $this->uuidNewCounter); + + return false; + } + + /** + * Indicates if the folder has a next synckey + * + * @access public + * @return boolean + */ + public function HasNewSyncKey() { + return (isset($this->uuid) && isset($this->uuidNewCounter)); + } + + /** + * Return the latest synckey. + * When this is called the new key becomes the current key (if a new key is available). + * The current key is then returned. + * + * @access public + * @return string + */ + public function GetLatestSyncKey() { + // New becomes old + if ($this->HasUuidNewCounter()) { + $this->uuidCounter = $this->uuidNewCounter; + unset($this->uuidNewCounter); + } + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncParameters->GetLatestSyncKey(): '%s'", $this->GetSyncKey())); + return $this->GetSyncKey(); + } + + /** + * Removes the saved SyncKey of this folder + * + * @access public + * @return boolean + */ + public function RemoveSyncKey() { + if (isset($this->uuid)) + unset($this->uuid); + + if (isset($this->uuidCounter)) + unset($this->uuidCounter); + + if (isset($this->uuidNewCounter)) + unset($this->uuidNewCounter); + + ZLog::Write(LOGLEVEL_DEBUG, "SyncParameters->RemoveSyncKey(): saved sync key removed"); + return true; + } + + + /** + * CPO methods + * + * A sync request can have several options blocks. Each block is saved into an own CPO object + * + */ + + /** + * Returns the a specified CPO + * + * @param string $options (opt) If not specified, the default Options (CPO) will be used + * Valid option SyncParameters::SMSOPTIONS (string "SMS") + * + * @access public + * @return ContentParameters object + */ + public function GetCPO($options = self::DEFAULTOPTIONS) { + $options = strtoupper($options); + $this->isValidType($options); + $options = $this->normalizeType($options); + + $this->checkCPO($options); + + // copy contentclass and conversationmode to the CPO + $this->contentParameters[$options]->SetContentClass($this->contentclass); + $this->contentParameters[$options]->SetConversationMode($this->conversationmode); + + return $this->contentParameters[$options]; + } + + /** + * Use the submitted CPO type for next setters/getters + * + * @param string $options (opt) If not specified, the default Options (CPO) will be used + * Valid option SyncParameters::SMSOPTIONS (string "SMS") + * + * @access public + * @return + */ + public function UseCPO($options = self::DEFAULTOPTIONS) { + $options = strtoupper($options); + $this->isValidType($options); + + // remove potential old default CPO if available + if (isset($this->contentParameters[self::DEFAULTOPTIONS]) && $options != self::DEFAULTOPTIONS && $options !== self::SMSOPTIONS) { + $a = $this->contentParameters; + unset($a[self::DEFAULTOPTIONS]); + $this->contentParameters = $a; + ZLog::Write(LOGLEVEL_DEBUG, "SyncParameters->UseCPO(): removed existing DEFAULT CPO as it is obsolete"); + } + + ZLOG::Write(LOGLEVEL_DEBUG, sprintf("SyncParameters->UseCPO('%s')", $options)); + $this->currentCPO = $options; + $this->checkCPO($this->currentCPO); + } + + /** + * Checks if a CPO is correctly inicialized and inicializes it if necessary + * + * @param string $options (opt) If not specified, the default Options (CPO) will be used + * Valid option SyncParameters::SMSOPTIONS (string "SMS") + * + * @access private + * @return boolean + */ + private function checkCPO($options = self::DEFAULTOPTIONS) { + $this->isValidType($options); + + if (!isset($this->contentParameters[$options])) { + $a = $this->contentParameters; + $a[$options] = new ContentParameters(); + $this->contentParameters = $a; + } + + return true; + } + + /** + * Checks if the requested option type is available + * + * @param string $options CPO type + * + * @access private + * @return boolean + * @throws FatalNotImplementedException + */ + private function isValidType($options) { + if ($options !== self::DEFAULTOPTIONS && + $options !== self::EMAILOPTIONS && + $options !== self::CALENDAROPTIONS && + $options !== self::CONTACTOPTIONS && + $options !== self::NOTEOPTIONS && + $options !== self::TASKOPTIONS && + $options !== self::SMSOPTIONS) + throw new FatalNotImplementedException(sprintf("SyncParameters->isAllowedType('%s') ContentParameters is invalid. Such type is not available.", $options)); + + return true; + } + + /** + * Normalizes the requested option type and returns it as + * default option if no default is available + * + * @param string $options CPO type + * + * @access private + * @return string + * @throws FatalNotImplementedException + */ + private function normalizeType($options) { + // return the requested CPO as it is defined + if (isset($this->contentParameters[$options])) + return $options; + + $returnCPO = $options; + // return email, calendar, contact or note CPO as default CPO if there no explicit default CPO defined + if ($options == self::DEFAULTOPTIONS && !isset($this->contentParameters[self::DEFAULTOPTIONS])) { + + if (isset($this->contentParameters[self::EMAILOPTIONS])) + $returnCPO = self::EMAILOPTIONS; + elseif (isset($this->contentParameters[self::CALENDAROPTIONS])) + $returnCPO = self::CALENDAROPTIONS; + elseif (isset($this->contentParameters[self::CONTACTOPTIONS])) + $returnCPO = self::CONTACTOPTIONS; + elseif (isset($this->contentParameters[self::NOTEOPTIONS])) + $returnCPO = self::NOTEOPTIONS; + elseif (isset($this->contentParameters[self::TASKOPTIONS])) + $returnCPO = self::TASKOPTIONS; + + if ($returnCPO != $options) + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncParameters->normalizeType(): using %s for requested %s", $returnCPO, $options)); + return $returnCPO; + } + // something unexpected happened, just return default, empty in the worst case + else { + ZLog::Write(LOGLEVEL_WARN, "SyncParameters->normalizeType(): no DEFAULT CPO available, creating empty CPO"); + $this->checkCPO(self::DEFAULTOPTIONS); + return self::DEFAULTOPTIONS; + } + } + + + /** + * PHP magic to implement any getter, setter, has and delete operations + * on an instance variable. + * + * NOTICE: All magic getters and setters of this object which are not defined in the unsetdata array are passed to the current CPO. + * + * Methods like e.g. "SetVariableName($x)" and "GetVariableName()" are supported + * + * @access public + * @return mixed + */ + public function __call($name, $arguments) { + $lowname = strtolower($name); + $operator = substr($lowname, 0,3); + $var = substr($lowname,3); + + if (array_key_exists($var, $this->unsetdata)) { + return parent::__call($name, $arguments); + } + + return $this->contentParameters[$this->currentCPO]->__call($name, $arguments); + } + + + /** + * un/serialization methods + */ + + /** + * Called before the StateObject is serialized + * + * @access protected + * @return boolean + */ + protected function preSerialize() { + parent::preSerialize(); + + if ($this->changed === true && ($this->synckeyChanged || $this->lastsynctime === false)) + $this->lastsynctime = time(); + + return true; + } + + /** + * Called after the StateObject was unserialized + * + * @access protected + * @return boolean + */ + protected function postUnserialize() { + // init with default options + $this->UseCPO(); + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/core/topcollector.php b/sources/lib/core/topcollector.php new file mode 100644 index 0000000..ae1b247 --- /dev/null +++ b/sources/lib/core/topcollector.php @@ -0,0 +1,299 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class TopCollector extends InterProcessData { + const ENABLEDAT = 2; + const TOPDATA = 3; + + protected $preserved; + protected $latest; + + /** + * Constructor + * + * @access public + */ + public function TopCollector() { + // initialize super parameters + $this->allocate = 2097152; // 2 MB + $this->type = 20; + parent::__construct(); + + // initialize params + $this->InitializeParams(); + + $this->preserved = array(); + // static vars come from the parent class + $this->latest = array( "pid" => self::$pid, + "ip" => Request::GetRemoteAddr(), + "user" => self::$user, + "start" => self::$start, + "devtype" => Request::GetDeviceType(), + "devid" => self::$devid, + "devagent" => Request::GetUserAgent(), + "command" => Request::GetCommandCode(), + "ended" => 0, + "push" => false, + ); + + $this->AnnounceInformation("initializing"); + } + + /** + * Destructor + * indicates that the process is shutting down + * + * @access public + */ + public function __destruct() { + $this->AnnounceInformation("OK", false, true); + } + + /** + * Advices all other processes that they should start/stop + * collecting data. The data saved is a timestamp. It has to be + * reactivated every couple of seconds + * + * @param boolean $stop (opt) default false (do collect) + * + * @access public + * @return boolean indicating if it was set to collect before + */ + public function CollectData($stop = false) { + $wasEnabled = false; + + // exclusive block + if ($this->blockMutex()) { + $wasEnabled = ($this->hasData(self::ENABLEDAT)) ? $this->getData(self::ENABLEDAT) : false; + + $time = time(); + if ($stop === true) $time = 0; + + if (! $this->setData($time, self::ENABLEDAT)) + return false; + $this->releaseMutex(); + } + // end exclusive block + + return $wasEnabled; + } + + /** + * Announces a string to the TopCollector + * + * @param string $info + * @param boolean $preserve info should be displayed when process terminates + * @param boolean $terminating indicates if the process is terminating + * + * @access public + * @return boolean + */ + public function AnnounceInformation($addinfo, $preserve = false, $terminating = false) { + $this->latest["addinfo"] = $addinfo; + $this->latest["update"] = time(); + + if ($terminating) { + $this->latest["ended"] = time(); + foreach ($this->preserved as $p) + $this->latest["addinfo"] .= " : ".$p; + } + + if ($preserve) + $this->preserved[] = $addinfo; + + // exclusive block + if ($this->blockMutex()) { + + if ($this->isEnabled()) { + $topdata = ($this->hasData(self::TOPDATA)) ? $this->getData(self::TOPDATA): array(); + + $this->checkArrayStructure($topdata); + + // update + $topdata[self::$devid][self::$user][self::$pid] = $this->latest; + $ok = $this->setData($topdata, self::TOPDATA); + } + $this->releaseMutex(); + } + // end exclusive block + + if ($this->isEnabled() === true && !$ok) { + ZLog::Write(LOGLEVEL_WARN, "TopCollector::AnnounceInformation(): could not write to shared memory. Z-Push top will not display this data."); + return false; + } + + return true; + } + + /** + * Returns all available top data + * + * @access public + * @return array + */ + public function ReadLatest() { + $topdata = array(); + + // exclusive block + if ($this->blockMutex()) { + $topdata = ($this->hasData(self::TOPDATA)) ? $this->getData(self::TOPDATA) : array(); + $this->releaseMutex(); + } + // end exclusive block + + return $topdata; + } + + /** + * Cleans up data collected so far + * + * @param boolean $all (optional) if set all data independently from the age is removed + * + * @access public + * @return boolean status + */ + public function ClearLatest($all = false) { + // it's ok when doing this every 10 sec + if ($all == false && time() % 10 != 0 ) + return true; + + $stat = false; + + // exclusive block + if ($this->blockMutex()) { + if ($all == true) { + $topdata = array(); + } + else { + $topdata = ($this->hasData(self::TOPDATA)) ? $this->getData(self::TOPDATA) : array(); + + $toClear = array(); + foreach ($topdata as $devid=>$users) { + foreach ($users as $user=>$pids) { + foreach ($pids as $pid=>$line) { + // remove everything which terminated for 20 secs or is not updated for more than 120 secs + if (($line["ended"] != 0 && time() - $line["ended"] > 20) || + time() - $line["update"] > 120) { + $toClear[] = array($devid, $user, $pid); + } + } + } + } + foreach ($toClear as $tc) + unset($topdata[$tc[0]][$tc[1]][$tc[2]]); + } + + $stat = $this->setData($topdata, self::TOPDATA); + $this->releaseMutex(); + } + // end exclusive block + + return $stat; + } + + /** + * Sets a different UserAgent for this connection + * + * @param string $agent + * + * @access public + * @return boolean + */ + public function SetUserAgent($agent) { + $this->latest["devagent"] = $agent; + return true; + } + + /** + * Marks this process as push connection + * + * @param string $agent + * + * @access public + * @return boolean + */ + public function SetAsPushConnection() { + $this->latest["push"] = true; + return true; + } + + /** + * Indicates if top data should be saved or not + * Returns true for 10 seconds after the latest CollectData() + * SHOULD only be called with locked mutex! + * + * @access private + * @return boolean + */ + private function isEnabled() { + $isEnabled = ($this->hasData(self::ENABLEDAT)) ? $this->getData(self::ENABLEDAT) : false; + return ($isEnabled !== false && ($isEnabled +300) > time()); + } + + /** + * Builds an array structure for the top data + * + * @param array $topdata reference to the topdata array + * + * @access private + * @return + */ + private function checkArrayStructure(&$topdata) { + if (!isset($topdata) || !is_array($topdata)) + $topdata = array(); + + if (!isset($topdata[self::$devid])) + $topdata[self::$devid] = array(); + + if (!isset($topdata[self::$devid][self::$user])) + $topdata[self::$devid][self::$user] = array(); + + if (!isset($topdata[self::$devid][self::$user][self::$pid])) + $topdata[self::$devid][self::$user][self::$pid] = array(); + } +} + +?> \ No newline at end of file diff --git a/sources/lib/core/zlog.php b/sources/lib/core/zlog.php new file mode 100644 index 0000000..5cc6d65 --- /dev/null +++ b/sources/lib/core/zlog.php @@ -0,0 +1,280 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class ZLog { + static private $devid = ''; + static private $user = ''; + static private $authUser = false; + static private $pidstr; + static private $wbxmlDebug = ''; + static private $lastLogs = array(); + static private $userLog = false; + static private $unAuthCache = array(); + + /** + * Initializes the logging + * + * @access public + * @return boolean + */ + static public function Initialize() { + global $specialLogUsers; + + // define some constants for the logging + if (!defined('LOGUSERLEVEL')) + define('LOGUSERLEVEL', LOGLEVEL_OFF); + + if (!defined('LOGLEVEL')) + define('LOGLEVEL', LOGLEVEL_OFF); + + list($user,) = Utils::SplitDomainUser(strtolower(Request::GetGETUser())); + self::$userLog = in_array($user, $specialLogUsers); + if (!defined('WBXML_DEBUG') && $user) { + // define the WBXML_DEBUG mode on user basis depending on the configurations + if (LOGLEVEL >= LOGLEVEL_WBXML || (LOGUSERLEVEL >= LOGLEVEL_WBXML && self::$userLog)) + define('WBXML_DEBUG', true); + else + define('WBXML_DEBUG', false); + } + + if ($user) + self::$user = '['. $user .'] '; + else + self::$user = ''; + + // log the device id if the global loglevel is set to log devid or the user is in and has the right log level + if (Request::GetDeviceID() != "" && (LOGLEVEL >= LOGLEVEL_DEVICEID || (LOGUSERLEVEL >= LOGLEVEL_DEVICEID && self::$userLog))) + self::$devid = '['. Request::GetDeviceID() .'] '; + else + self::$devid = ''; + + return true; + } + + /** + * Writes a log line + * + * @param int $loglevel one of the defined LOGLEVELS + * @param string $message + * @param boolean $truncate indicate if the message should be truncated, default true + * + * @access public + * @return + */ + static public function Write($loglevel, $message, $truncate = true) { + // truncate messages longer than 10 KB + $messagesize = strlen($message); + if ($truncate && $messagesize > 10240) + $message = substr($message, 0, 10240) . sprintf(" ", $messagesize); + + self::$lastLogs[$loglevel] = $message; + $data = self::buildLogString($loglevel) . $message . "\n"; + + if ($loglevel <= LOGLEVEL) { + @file_put_contents(LOGFILE, $data, FILE_APPEND); + } + + // should we write this into the user log? + if ($loglevel <= LOGUSERLEVEL && self::$userLog) { + // padd level for better reading + $data = str_replace(self::getLogLevelString($loglevel), self::getLogLevelString($loglevel,true), $data); + + // is the user authenticated? + if (self::logToUserFile()) { + // something was logged before the user was authenticated, write this to the log + if (!empty(self::$unAuthCache)) { + @file_put_contents(LOGFILEDIR . self::logToUserFile() . ".log", implode('', self::$unAuthCache), FILE_APPEND); + self::$unAuthCache = array(); + } + // only use plain old a-z characters for the generic log file + @file_put_contents(LOGFILEDIR . self::logToUserFile() . ".log", $data, FILE_APPEND); + } + // the user is not authenticated yet, we save the log into memory for now + else { + self::$unAuthCache[] = $data; + } + } + + if (($loglevel & LOGLEVEL_FATAL) || ($loglevel & LOGLEVEL_ERROR)) { + @file_put_contents(LOGERRORFILE, $data, FILE_APPEND); + } + + if ($loglevel & LOGLEVEL_WBXMLSTACK) { + self::$wbxmlDebug .= $message. "\n"; + } + } + + /** + * Returns logged information about the WBXML stack + * + * @access public + * @return string + */ + static public function GetWBXMLDebugInfo() { + return trim(self::$wbxmlDebug); + } + + /** + * Returns the last message logged for a log level + * + * @param int $loglevel one of the defined LOGLEVELS + * + * @access public + * @return string/false returns false if there was no message logged in that level + */ + static public function GetLastMessage($loglevel) { + return (isset(self::$lastLogs[$loglevel]))?self::$lastLogs[$loglevel]:false; + } + + /**---------------------------------------------------------------------------------------------------------- + * private log stuff + */ + + /** + * Returns the filename logs for a WBXML debug log user should be saved to + * + * @access private + * @return string + */ + static private function logToUserFile() { + global $specialLogUsers; + + if (self::$authUser === false) { + if (RequestProcessor::isUserAuthenticated()) { + $authuser = Request::GetAuthUser(); + if ($authuser && in_array($authuser, $specialLogUsers)) + self::$authUser = preg_replace('/[^a-z0-9]/', '_', strtolower($authuser)); + } + } + return self::$authUser; + } + + /** + * Returns the string to be logged + * + * @access private + * @return string + */ + static private function buildLogString($loglevel) { + if (!isset(self::$pidstr)) + self::$pidstr = '[' . str_pad(@getmypid(),5," ",STR_PAD_LEFT) . '] '; + + if (!isset(self::$user)) + self::$user = ''; + + if (!isset(self::$devid)) + self::$devid = ''; + + return Utils::GetFormattedTime() ." ". self::$pidstr . self::getLogLevelString($loglevel, (LOGLEVEL > LOGLEVEL_INFO)) ." ". self::$user . self::$devid; + } + + /** + * Returns the string representation of the LOGLEVEL. + * String can be padded + * + * @param int $loglevel one of the LOGLEVELs + * @param boolean $pad + * + * @access private + * @return string + */ + static private function getLogLevelString($loglevel, $pad = false) { + if ($pad) $s = " "; + else $s = ""; + switch($loglevel) { + case LOGLEVEL_OFF: return ""; break; + case LOGLEVEL_FATAL: return "[FATAL]"; break; + case LOGLEVEL_ERROR: return "[ERROR]"; break; + case LOGLEVEL_WARN: return "[".$s."WARN]"; break; + case LOGLEVEL_INFO: return "[".$s."INFO]"; break; + case LOGLEVEL_DEBUG: return "[DEBUG]"; break; + case LOGLEVEL_WBXML: return "[WBXML]"; break; + case LOGLEVEL_DEVICEID: return "[DEVICEID]"; break; + case LOGLEVEL_WBXMLSTACK: return "[WBXMLSTACK]"; break; + } + } +} + +/**---------------------------------------------------------------------------------------------------------- + * Legacy debug stuff + */ + +// deprecated +// backwards compatible +function debugLog($message) { + ZLog::Write(LOGLEVEL_DEBUG, $message); +} + +// TODO review error handler +function zarafa_error_handler($errno, $errstr, $errfile, $errline, $errcontext) { + $bt = debug_backtrace(); + switch ($errno) { + case 8192: // E_DEPRECATED since PHP 5.3.0 + // do not handle this message + break; + + case E_NOTICE: + case E_WARNING: + // TODO check if there is a better way to avoid these messages + if (stripos($errfile,'interprocessdata') !== false && stripos($errstr,'shm_get_var()') !== false) + break; + ZLog::Write(LOGLEVEL_WARN, "$errfile:$errline $errstr ($errno)"); + break; + + default: + ZLog::Write(LOGLEVEL_ERROR, "trace error: $errfile:$errline $errstr ($errno) - backtrace: ". (count($bt)-1) . " steps"); + for($i = 1, $bt_length = count($bt); $i < $bt_length; $i++) { + $file = $line = "unknown"; + if (isset($bt[$i]['file'])) $file = $bt[$i]['file']; + if (isset($bt[$i]['line'])) $line = $bt[$i]['line']; + ZLog::Write(LOGLEVEL_ERROR, "trace: $i:". $file . ":" . $line. " - " . ((isset($bt[$i]['class']))? $bt[$i]['class'] . $bt[$i]['type']:""). $bt[$i]['function']. "()"); + } + //throw new Exception("An error occured."); + break; + } +} + +error_reporting(E_ALL); +set_error_handler("zarafa_error_handler"); + +?> \ No newline at end of file diff --git a/sources/lib/core/zpush.php b/sources/lib/core/zpush.php new file mode 100644 index 0000000..7efac2c --- /dev/null +++ b/sources/lib/core/zpush.php @@ -0,0 +1,822 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class ZPush { + const UNAUTHENTICATED = 1; + const UNPROVISIONED = 2; + const NOACTIVESYNCCOMMAND = 3; + const WEBSERVICECOMMAND = 4; + const HIERARCHYCOMMAND = 5; + const PLAININPUT = 6; + const REQUESTHANDLER = 7; + const CLASS_NAME = 1; + const CLASS_REQUIRESPROTOCOLVERSION = 2; + const CLASS_DEFAULTTYPE = 3; + const CLASS_OTHERTYPES = 4; + + // AS versions + const ASV_1 = "1.0"; + const ASV_2 = "2.0"; + const ASV_21 = "2.1"; + const ASV_25 = "2.5"; + const ASV_12 = "12.0"; + const ASV_121 = "12.1"; + const ASV_14 = "14.0"; + + /** + * Command codes for base64 encoded requests (AS >= 12.1) + */ + const COMMAND_SYNC = 0; + const COMMAND_SENDMAIL = 1; + const COMMAND_SMARTFORWARD = 2; + const COMMAND_SMARTREPLY = 3; + const COMMAND_GETATTACHMENT = 4; + const COMMAND_FOLDERSYNC = 9; + const COMMAND_FOLDERCREATE = 10; + const COMMAND_FOLDERDELETE = 11; + const COMMAND_FOLDERUPDATE = 12; + const COMMAND_MOVEITEMS = 13; + const COMMAND_GETITEMESTIMATE = 14; + const COMMAND_MEETINGRESPONSE = 15; + const COMMAND_SEARCH = 16; + const COMMAND_SETTINGS = 17; + const COMMAND_PING = 18; + const COMMAND_ITEMOPERATIONS = 19; + const COMMAND_PROVISION = 20; + const COMMAND_RESOLVERECIPIENTS = 21; + const COMMAND_VALIDATECERT = 22; + + // Deprecated commands + const COMMAND_GETHIERARCHY = -1; + const COMMAND_CREATECOLLECTION = -2; + const COMMAND_DELETECOLLECTION = -3; + const COMMAND_MOVECOLLECTION = -4; + const COMMAND_NOTIFY = -5; + + // Webservice commands + const COMMAND_WEBSERVICE_DEVICE = -100; + const COMMAND_WEBSERVICE_USERS = -101; + + // Latest supported State version + const STATE_VERSION = IStateMachine::STATEVERSION_02; + + static private $autoloadBackendPreference = array( + "BackendZarafa", + "BackendCombined", + "BackendIMAP", + "BackendVCardDir", + "BackendMaildir" + ); + + static private $supportedASVersions = array( + self::ASV_1, + self::ASV_2, + self::ASV_21, + self::ASV_25, + self::ASV_12, + self::ASV_121, + self::ASV_14 + ); + + static private $supportedCommands = array( + // COMMAND // AS VERSION // REQUESTHANDLER // OTHER SETTINGS + self::COMMAND_SYNC => array(self::ASV_1, self::REQUESTHANDLER => "Sync"), + self::COMMAND_SENDMAIL => array(self::ASV_1, self::REQUESTHANDLER => "SendMail"), + self::COMMAND_SMARTFORWARD => array(self::ASV_1, self::REQUESTHANDLER => "SendMail"), + self::COMMAND_SMARTREPLY => array(self::ASV_1, self::REQUESTHANDLER => "SendMail"), + self::COMMAND_GETATTACHMENT => array(self::ASV_1, self::REQUESTHANDLER => "GetAttachment"), + self::COMMAND_GETHIERARCHY => array(self::ASV_1, self::REQUESTHANDLER => "GetHierarchy", self::HIERARCHYCOMMAND), // deprecated but implemented + self::COMMAND_CREATECOLLECTION => array(self::ASV_1), // deprecated & not implemented + self::COMMAND_DELETECOLLECTION => array(self::ASV_1), // deprecated & not implemented + self::COMMAND_MOVECOLLECTION => array(self::ASV_1), // deprecated & not implemented + self::COMMAND_FOLDERSYNC => array(self::ASV_2, self::REQUESTHANDLER => "FolderSync", self::HIERARCHYCOMMAND), + self::COMMAND_FOLDERCREATE => array(self::ASV_2, self::REQUESTHANDLER => "FolderChange", self::HIERARCHYCOMMAND), + self::COMMAND_FOLDERDELETE => array(self::ASV_2, self::REQUESTHANDLER => "FolderChange", self::HIERARCHYCOMMAND), + self::COMMAND_FOLDERUPDATE => array(self::ASV_2, self::REQUESTHANDLER => "FolderChange", self::HIERARCHYCOMMAND), + self::COMMAND_MOVEITEMS => array(self::ASV_1, self::REQUESTHANDLER => "MoveItems"), + self::COMMAND_GETITEMESTIMATE => array(self::ASV_1, self::REQUESTHANDLER => "GetItemEstimate"), + self::COMMAND_MEETINGRESPONSE => array(self::ASV_1, self::REQUESTHANDLER => "MeetingResponse"), + self::COMMAND_RESOLVERECIPIENTS => array(self::ASV_1, self::REQUESTHANDLER => "ResolveRecipients"), + self::COMMAND_VALIDATECERT => array(self::ASV_1, self::REQUESTHANDLER => "ValidateCert"), + self::COMMAND_PROVISION => array(self::ASV_25, self::REQUESTHANDLER => "Provisioning", self::UNAUTHENTICATED, self::UNPROVISIONED), + self::COMMAND_SEARCH => array(self::ASV_1, self::REQUESTHANDLER => "Search"), + self::COMMAND_PING => array(self::ASV_2, self::REQUESTHANDLER => "Ping", self::UNPROVISIONED), + self::COMMAND_NOTIFY => array(self::ASV_1, self::REQUESTHANDLER => "Notify"), // deprecated & not implemented + self::COMMAND_ITEMOPERATIONS => array(self::ASV_12, self::REQUESTHANDLER => "ItemOperations"), + 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), + ); + + + + static private $classes = array( + "Email" => array( + self::CLASS_NAME => "SyncMail", + self::CLASS_REQUIRESPROTOCOLVERSION => false, + self::CLASS_DEFAULTTYPE => SYNC_FOLDER_TYPE_INBOX, + self::CLASS_OTHERTYPES => array(SYNC_FOLDER_TYPE_OTHER, SYNC_FOLDER_TYPE_DRAFTS, SYNC_FOLDER_TYPE_WASTEBASKET, + SYNC_FOLDER_TYPE_SENTMAIL, SYNC_FOLDER_TYPE_OUTBOX, SYNC_FOLDER_TYPE_USER_MAIL, + SYNC_FOLDER_TYPE_JOURNAL, SYNC_FOLDER_TYPE_USER_JOURNAL), + ), + "Contacts" => array( + self::CLASS_NAME => "SyncContact", + self::CLASS_REQUIRESPROTOCOLVERSION => true, + self::CLASS_DEFAULTTYPE => SYNC_FOLDER_TYPE_CONTACT, + self::CLASS_OTHERTYPES => array(SYNC_FOLDER_TYPE_USER_CONTACT), + ), + "Calendar" => array( + self::CLASS_NAME => "SyncAppointment", + self::CLASS_REQUIRESPROTOCOLVERSION => false, + self::CLASS_DEFAULTTYPE => SYNC_FOLDER_TYPE_APPOINTMENT, + self::CLASS_OTHERTYPES => array(SYNC_FOLDER_TYPE_USER_APPOINTMENT), + ), + "Tasks" => array( + self::CLASS_NAME => "SyncTask", + self::CLASS_REQUIRESPROTOCOLVERSION => false, + self::CLASS_DEFAULTTYPE => SYNC_FOLDER_TYPE_TASK, + self::CLASS_OTHERTYPES => array(SYNC_FOLDER_TYPE_USER_TASK), + ), + "Notes" => array( + self::CLASS_NAME => "SyncNote", + self::CLASS_REQUIRESPROTOCOLVERSION => false, + self::CLASS_DEFAULTTYPE => SYNC_FOLDER_TYPE_NOTE, + self::CLASS_OTHERTYPES => array(SYNC_FOLDER_TYPE_USER_NOTE), + ), + ); + + + static private $stateMachine; + static private $searchProvider; + static private $deviceManager; + static private $topCollector; + static private $backend; + static private $addSyncFolders; + + + /** + * Verifies configuration + * + * @access public + * @return boolean + * @throws FatalMisconfigurationException + */ + static public function CheckConfig() { + // check the php version + if (version_compare(phpversion(),'5.1.0') < 0) + throw new FatalException("The configured PHP version is too old. Please make sure at least PHP 5.1 is used."); + + // some basic checks + if (!defined('BASE_PATH')) + throw new FatalMisconfigurationException("The BASE_PATH is not configured. Check if the config.php file is in place."); + + if (substr(BASE_PATH, -1,1) != "/") + throw new FatalMisconfigurationException("The BASE_PATH should terminate with a '/'"); + + if (!file_exists(BASE_PATH)) + throw new FatalMisconfigurationException("The configured BASE_PATH does not exist or can not be accessed."); + + if (defined('BASE_PATH_CLI') && file_exists(BASE_PATH_CLI)) + define('REAL_BASE_PATH', BASE_PATH_CLI); + else + define('REAL_BASE_PATH', BASE_PATH); + + if (!defined('LOGFILEDIR')) + throw new FatalMisconfigurationException("The LOGFILEDIR is not configured. Check if the config.php file is in place."); + + if (substr(LOGFILEDIR, -1,1) != "/") + throw new FatalMisconfigurationException("The LOGFILEDIR should terminate with a '/'"); + + if (!file_exists(LOGFILEDIR)) + throw new FatalMisconfigurationException("The configured LOGFILEDIR does not exist or can not be accessed."); + + if ((!file_exists(LOGFILE) && !touch(LOGFILE)) || !is_writable(LOGFILE)) + throw new FatalMisconfigurationException("The configured LOGFILE can not be modified."); + + if ((!file_exists(LOGERRORFILE) && !touch(LOGERRORFILE)) || !is_writable(LOGERRORFILE)) + throw new FatalMisconfigurationException("The configured LOGERRORFILE can not be modified."); + + // check ownership on the (eventually) just created files + Utils::FixFileOwner(LOGFILE); + Utils::FixFileOwner(LOGERRORFILE); + + // set time zone + // code contributed by Robert Scheck (rsc) - more information: https://developer.berlios.de/mantis/view.php?id=479 + if(function_exists("date_default_timezone_set")) { + if(defined('TIMEZONE') ? constant('TIMEZONE') : false) { + if (! @date_default_timezone_set(TIMEZONE)) + throw new FatalMisconfigurationException(sprintf("The configured TIMEZONE '%s' is not valid. Please check supported timezones at http://www.php.net/manual/en/timezones.php", constant('TIMEZONE'))); + } + else if(!ini_get('date.timezone')) { + date_default_timezone_set('Europe/Amsterdam'); + } + } + + return true; + } + + /** + * Verifies Timezone, StateMachine and Backend configuration + * + * @access public + * @return boolean + * @trows FatalMisconfigurationException + */ + static public function CheckAdvancedConfig() { + global $specialLogUsers, $additionalFolders; + + if (!is_array($specialLogUsers)) + throw new FatalMisconfigurationException("The WBXML log users is not an array."); + + if (!defined('SINK_FORCERECHECK')) { + define('SINK_FORCERECHECK', 300); + } + 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)) + throw new FatalMisconfigurationException("The SYNC_CONTACTS_MAXPICTURESIZE value must be a number higher than 0."); + + // 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."); + else { + self::$addSyncFolders = array(); + + // process configured data + foreach ($additionalFolders as $af) { + + if (!is_array($af) || !isset($af['store']) || !isset($af['folderid']) || !isset($af['name']) || !isset($af['type'])) { + ZLog::Write(LOGLEVEL_ERROR, "ZPush::CheckConfig() : the additional folder synchronization is not configured correctly. Missing parameters. Entry will be ignored."); + continue; + } + + if ($af['store'] == "" || $af['folderid'] == "" || $af['name'] == "" || $af['type'] == "") { + ZLog::Write(LOGLEVEL_WARN, "ZPush::CheckConfig() : the additional folder synchronization is not configured correctly. Empty parameters. Entry will be ignored."); + continue; + } + + if (!in_array($af['type'], array(SYNC_FOLDER_TYPE_USER_CONTACT, SYNC_FOLDER_TYPE_USER_APPOINTMENT, SYNC_FOLDER_TYPE_USER_TASK, SYNC_FOLDER_TYPE_USER_MAIL))) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPush::CheckConfig() : the type of the additional synchronization folder '%s is not permitted.", $af['name'])); + continue; + } + + $folder = new SyncFolder(); + $folder->serverid = $af['folderid']; + $folder->parentid = 0; // only top folders are supported + $folder->displayname = $af['name']; + $folder->type = $af['type']; + // save store as custom property which is not streamed directly to the device + $folder->NoBackendFolder = true; + $folder->Store = $af['store']; + self::$addSyncFolders[$folder->serverid] = $folder; + } + + } + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Used timezone '%s'", date_default_timezone_get())); + + // get the statemachine, which will also try to load the backend.. This could throw errors + self::GetStateMachine(); + } + + /** + * Returns the StateMachine object + * which has to be an IStateMachine implementation + * + * @access public + * @throws FatalNotImplementedException + * @throws HTTPReturnCodeException + * @return object implementation of IStateMachine + */ + static public function GetStateMachine() { + if (!isset(ZPush::$stateMachine)) { + // the backend could also return an own IStateMachine implementation + $backendStateMachine = self::GetBackend()->GetStateMachine(); + + // if false is returned, use the default StateMachine + if ($backendStateMachine !== false) { + ZLog::Write(LOGLEVEL_DEBUG, "Backend implementation of IStateMachine: ".get_class($backendStateMachine)); + if (in_array('IStateMachine', class_implements($backendStateMachine))) + ZPush::$stateMachine = $backendStateMachine; + else + throw new FatalNotImplementedException("State machine returned by the backend does not implement the IStateMachine interface!"); + } + else { + // Initialize the default StateMachine + include_once('lib/default/filestatemachine.php'); + ZPush::$stateMachine = new FileStateMachine(); + } + + if (ZPush::$stateMachine->GetStateVersion() !== ZPush::GetLatestStateVersion()) { + if (class_exists("TopCollector")) self::GetTopCollector()->AnnounceInformation("Run migration script!", true); + throw new HTTPReturnCodeException(sprintf("The state version available to the %s is not the latest version - please run the state upgrade script. See release notes for more information.", get_class(ZPush::$stateMachine), 503)); + } + } + return ZPush::$stateMachine; + } + + /** + * Returns the latest version of supported states + * + * @access public + * @return int + */ + static public function GetLatestStateVersion() { + return self::STATE_VERSION; + } + + /** + * Returns the DeviceManager object + * + * @param boolean $initialize (opt) default true: initializes the DeviceManager if not already done + * + * @access public + * @return object DeviceManager + */ + static public function GetDeviceManager($initialize = true) { + if (!isset(ZPush::$deviceManager) && $initialize) + ZPush::$deviceManager = new DeviceManager(); + + return ZPush::$deviceManager; + } + + /** + * Returns the Top data collector object + * + * @access public + * @return object TopCollector + */ + static public function GetTopCollector() { + if (!isset(ZPush::$topCollector)) + ZPush::$topCollector = new TopCollector(); + + return ZPush::$topCollector; + } + + /** + * Loads a backend file + * + * @param string $backendname + + * @access public + * @throws FatalNotImplementedException + * @return boolean + */ + static public function IncludeBackend($backendname) { + if ($backendname == false) return false; + + $backendname = strtolower($backendname); + if (substr($backendname, 0, 7) !== 'backend') + throw new FatalNotImplementedException(sprintf("Backend '%s' is not allowed",$backendname)); + + $rbn = substr($backendname, 7); + + $subdirbackend = REAL_BASE_PATH . "backend/" . $rbn . "/" . $rbn . ".php"; + $stdbackend = REAL_BASE_PATH . "backend/" . $rbn . ".php"; + + if (is_file($subdirbackend)) + $toLoad = $subdirbackend; + else if (is_file($stdbackend)) + $toLoad = $stdbackend; + else + return false; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Including backend file: '%s'", $toLoad)); + include_once($toLoad); + return true; + } + + /** + * Returns the SearchProvider object + * which has to be an ISearchProvider implementation + * + * @access public + * @return object implementation of ISearchProvider + * @throws FatalMisconfigurationException, FatalNotImplementedException + */ + static public function GetSearchProvider() { + if (!isset(ZPush::$searchProvider)) { + // is a global searchprovider configured ? It will outrank the backend + if (defined('SEARCH_PROVIDER') && @constant('SEARCH_PROVIDER') != "") { + $searchClass = @constant('SEARCH_PROVIDER'); + + if (! class_exists($searchClass)) + self::IncludeBackend($searchClass); + + if (class_exists($searchClass)) + $aSearchProvider = new $searchClass(); + else + throw new FatalMisconfigurationException(sprintf("Search provider '%s' can not be loaded. Check configuration!", $searchClass)); + } + // get the searchprovider from the backend + else + $aSearchProvider = self::GetBackend()->GetSearchProvider(); + + if (in_array('ISearchProvider', class_implements($aSearchProvider))) + ZPush::$searchProvider = $aSearchProvider; + else + throw new FatalNotImplementedException("Instantiated SearchProvider does not implement the ISearchProvider interface!"); + } + return ZPush::$searchProvider; + } + + /** + * Returns the Backend for this request + * the backend has to be an IBackend implementation + * + * @access public + * @return object IBackend implementation + */ + static public function GetBackend() { + // if the backend is not yet loaded, load backend drivers and instantiate it + if (!isset(ZPush::$backend)) { + // Initialize our backend + $ourBackend = @constant('BACKEND_PROVIDER'); + + // if no backend provider is defined, try to include automatically + if ($ourBackend == false || $ourBackend == "") { + $loaded = false; + foreach (self::$autoloadBackendPreference as $autoloadBackend) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPush::GetBackend(): trying autoload backend '%s'", $autoloadBackend)); + $loaded = self::IncludeBackend($autoloadBackend); + if ($loaded) { + $ourBackend = $autoloadBackend; + break; + } + } + if (!$ourBackend || !$loaded) + throw new FatalMisconfigurationException("No Backend provider can not be loaded. Check your installation and configuration!"); + } + else + self::IncludeBackend($ourBackend); + + if (class_exists($ourBackend)) + ZPush::$backend = new $ourBackend(); + else + throw new FatalMisconfigurationException(sprintf("Backend provider '%s' can not be loaded. Check configuration!", $ourBackend)); + } + return ZPush::$backend; + } + + /** + * Returns additional folder objects which should be synchronized to the device + * + * @access public + * @return array + */ + static public function GetAdditionalSyncFolders() { + // TODO if there are any user based folders which should be synchronized, they have to be returned here as well!! + return self::$addSyncFolders; + } + + /** + * Returns additional folder objects which should be synchronized to the device + * + * @param string $folderid + * @param boolean $noDebug (opt) by default, debug message is shown + * + * @access public + * @return string + */ + static public function GetAdditionalSyncFolderStore($folderid, $noDebug = false) { + $val = (isset(self::$addSyncFolders[$folderid]->Store))? self::$addSyncFolders[$folderid]->Store : false; + if (!$noDebug) + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPush::GetAdditionalSyncFolderStore('%s'): '%s'", $folderid, Utils::PrintAsString($val))); + return $val; + } + + /** + * Returns a SyncObject class name for a folder class + * + * @param string $folderclass + * + * @access public + * @return string + * @throws FatalNotImplementedException + */ + static public function getSyncObjectFromFolderClass($folderclass) { + if (!isset(self::$classes[$folderclass])) + throw new FatalNotImplementedException("Class '$folderclass' is not supported"); + + $class = self::$classes[$folderclass][self::CLASS_NAME]; + if (self::$classes[$folderclass][self::CLASS_REQUIRESPROTOCOLVERSION]) + return new $class(Request::GetProtocolVersion()); + else + return new $class(); + } + + /** + * Returns the default foldertype for a folder class + * + * @param string $folderclass folderclass sent by the mobile + * + * @access public + * @return string + */ + static public function getDefaultFolderTypeFromFolderClass($folderclass) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPush::getDefaultFolderTypeFromFolderClass('%s'): '%d'", $folderclass, self::$classes[$folderclass][self::CLASS_DEFAULTTYPE])); + return self::$classes[$folderclass][self::CLASS_DEFAULTTYPE]; + } + + /** + * Returns the folder class for a foldertype + * + * @param string $foldertype + * + * @access public + * @return string/false false if no class for this type is available + */ + static public function GetFolderClassFromFolderType($foldertype) { + $class = false; + foreach (self::$classes as $aClass => $cprops) { + if ($cprops[self::CLASS_DEFAULTTYPE] == $foldertype || in_array($foldertype, $cprops[self::CLASS_OTHERTYPES])) { + $class = $aClass; + break; + } + } + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPush::GetFolderClassFromFolderType('%s'): %s", $foldertype, Utils::PrintAsString($class))); + return $class; + } + + /** + * Prints the Z-Push legal header to STDOUT + * Using this breaks ActiveSync synchronization if wbxml is expected + * + * @param string $message (opt) message to be displayed + * @param string $additionalMessage (opt) additional message to be displayed + + * @access public + * @return + * + */ + static public function PrintZPushLegal($message = "", $additionalMessage = "") { + ZLog::Write(LOGLEVEL_DEBUG,"ZPush::PrintZPushLegal()"); + $zpush_version = @constant('ZPUSH_VERSION'); + + if ($message) + $message = "

". $message . "

"; + if ($additionalMessage) + $additionalMessage .= "
"; + + header("Content-type: text/html"); + print << +
+ Z-Push ActiveSync +
+ + +

Z-Push - Open Source ActiveSync

+ Version $zpush_version
+ $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
+
+ All modifications to this sourcecode must be published and returned to the community.
+ Please see AGPLv3 License for details.
+
+ + +END; + } + + /** + * Indicates the latest AS version supported by Z-Push + * + * @access public + * @return string + */ + static public function GetLatestSupportedASVersion() { + return end(self::$supportedASVersions); + } + + /** + * Indicates which is the highest AS version supported by the backend + * + * @access public + * @return string + * @throws FatalNotImplementedException if the backend returns an invalid version + */ + static public function GetSupportedASVersion() { + $version = self::GetBackend()->GetSupportedASVersion(); + if (!in_array($version, self::$supportedASVersions)) + throw new FatalNotImplementedException(sprintf("AS version '%s' reported by the backend is not supported", $version)); + + return $version; + } + + /** + * Returns AS server header + * + * @access public + * @return string + */ + static public function GetServerHeader() { + if (self::GetSupportedASVersion() == self::ASV_25) + return "MS-Server-ActiveSync: 6.5.7638.1"; + else + return "MS-Server-ActiveSync: ". self::GetSupportedASVersion(); + } + + /** + * Returns AS protocol versions which are supported + * + * @param boolean $valueOnly (opt) default: false (also returns the header name) + * + * @access public + * @return string + */ + static public function GetSupportedProtocolVersions($valueOnly = false) { + $versions = implode(',', array_slice(self::$supportedASVersions, 0, (array_search(self::GetSupportedASVersion(), self::$supportedASVersions)+1))); + ZLog::Write(LOGLEVEL_DEBUG, "ZPush::GetSupportedProtocolVersions(): " . $versions); + + if ($valueOnly === true) + return $versions; + + return "MS-ASProtocolVersions: " . $versions; + } + + /** + * Returns AS commands which are supported + * + * @access public + * @return string + */ + static public function GetSupportedCommands() { + $asCommands = array(); + // filter all non-activesync commands + foreach (self::$supportedCommands as $c=>$v) + if (!self::checkCommandOptions($c, self::NOACTIVESYNCCOMMAND) && + self::checkCommandOptions($c, self::GetSupportedASVersion())) + $asCommands[] = Utils::GetCommandFromCode($c); + + $commands = implode(',', $asCommands); + ZLog::Write(LOGLEVEL_DEBUG, "ZPush::GetSupportedCommands(): " . $commands); + return "MS-ASProtocolCommands: " . $commands; + } + + /** + * Loads and instantiates a request processor for a command + * + * @param int $commandCode + * + * @access public + * @return RequestProcessor sub-class + */ + static public function GetRequestHandlerForCommand($commandCode) { + if (!array_key_exists($commandCode, self::$supportedCommands) || + !array_key_exists(self::REQUESTHANDLER, self::$supportedCommands[$commandCode]) ) + throw new FatalNotImplementedException(sprintf("Command '%s' has no request handler or class", Utils::GetCommandFromCode($commandCode))); + + $class = self::$supportedCommands[$commandCode][self::REQUESTHANDLER]; + if ($class == "Webservice") + $handlerclass = REAL_BASE_PATH . "lib/webservice/webservice.php"; + else + $handlerclass = REAL_BASE_PATH . "lib/request/" . strtolower($class) . ".php"; + + if (is_file($handlerclass)) + include($handlerclass); + + if (class_exists($class)) + return new $class(); + else + throw new FatalNotImplementedException(sprintf("Request handler '%s' can not be loaded", $class)); + } + + /** + * Indicates if a commands requires authentication or not + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + static public function CommandNeedsAuthentication($commandCode) { + $stat = ! self::checkCommandOptions($commandCode, self::UNAUTHENTICATED); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPush::CommandNeedsAuthentication(%d): %s", $commandCode, Utils::PrintAsString($stat))); + return $stat; + } + + /** + * Indicates if the Provisioning check has to be forced on these commands + * + * @param string $commandCode + + * @access public + * @return boolean + */ + static public function CommandNeedsProvisioning($commandCode) { + $stat = ! self::checkCommandOptions($commandCode, self::UNPROVISIONED); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPush::CommandNeedsProvisioning(%s): %s", $commandCode, Utils::PrintAsString($stat))); + return $stat; + } + + /** + * Indicates if these commands expect plain text input instead of wbxml + * + * @param string $commandCode + * + * @access public + * @return boolean + */ + static public function CommandNeedsPlainInput($commandCode) { + $stat = self::checkCommandOptions($commandCode, self::PLAININPUT); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPush::CommandNeedsPlainInput(%d): %s", $commandCode, Utils::PrintAsString($stat))); + return $stat; + } + + /** + * Indicates if the comand to be executed operates on the hierarchy + * + * @param int $commandCode + + * @access public + * @return boolean + */ + static public function HierarchyCommand($commandCode) { + $stat = self::checkCommandOptions($commandCode, self::HIERARCHYCOMMAND); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPush::HierarchyCommand(%d): %s", $commandCode, Utils::PrintAsString($stat))); + return $stat; + } + + /** + * Checks access types of a command + * + * @param string $commandCode a commandCode + * @param string $option e.g. self::UNAUTHENTICATED + + * @access private + * @throws FatalNotImplementedException + * @return object StateMachine + */ + static private function checkCommandOptions($commandCode, $option) { + if ($commandCode === false) return false; + + if (!array_key_exists($commandCode, self::$supportedCommands)) + throw new FatalNotImplementedException(sprintf("Command '%s' is not supported", Utils::GetCommandFromCode($commandCode))); + + $capa = self::$supportedCommands[$commandCode]; + $defcapa = in_array($option, $capa, true); + + // if not looking for a default capability, check if the command is supported since a previous AS version + if (!$defcapa) { + $verkey = array_search($option, self::$supportedASVersions, true); + if ($verkey !== false && ($verkey >= array_search($capa[0], self::$supportedASVersions))) { + $defcapa = true; + } + } + + return $defcapa; + } + +} +?> \ No newline at end of file diff --git a/sources/lib/core/zpushdefs.php b/sources/lib/core/zpushdefs.php new file mode 100644 index 0000000..ac42066 --- /dev/null +++ b/sources/lib/core/zpushdefs.php @@ -0,0 +1,1066 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +define("SYNC_SYNCHRONIZE","Synchronize"); +define("SYNC_REPLIES","Replies"); +define("SYNC_ADD","Add"); +define("SYNC_MODIFY","Modify"); +define("SYNC_REMOVE","Remove"); +define("SYNC_FETCH","Fetch"); +define("SYNC_SYNCKEY","SyncKey"); +define("SYNC_CLIENTENTRYID","ClientEntryId"); +define("SYNC_SERVERENTRYID","ServerEntryId"); +define("SYNC_STATUS","Status"); +define("SYNC_FOLDER","Folder"); +define("SYNC_FOLDERTYPE","FolderType"); +define("SYNC_VERSION","Version"); +define("SYNC_FOLDERID","FolderId"); +define("SYNC_GETCHANGES","GetChanges"); +define("SYNC_MOREAVAILABLE","MoreAvailable"); +define("SYNC_MAXITEMS","MaxItems"); +define("SYNC_WINDOWSIZE","WindowSize"); //MaxItems before z-push 2 +define("SYNC_PERFORM","Perform"); +define("SYNC_OPTIONS","Options"); +define("SYNC_FILTERTYPE","FilterType"); +define("SYNC_TRUNCATION","Truncation"); +define("SYNC_RTFTRUNCATION","RtfTruncation"); +define("SYNC_CONFLICT","Conflict"); +define("SYNC_FOLDERS","Folders"); +define("SYNC_DATA","Data"); +define("SYNC_DELETESASMOVES","DeletesAsMoves"); +define("SYNC_NOTIFYGUID","NotifyGUID"); +define("SYNC_SUPPORTED","Supported"); +define("SYNC_SOFTDELETE","SoftDelete"); +define("SYNC_MIMESUPPORT","MIMESupport"); +define("SYNC_MIMETRUNCATION","MIMETruncation"); +define("SYNC_NEWMESSAGE","NewMessage"); +define("SYNC_WAIT","Wait"); //12.1 and 14.0 +define("SYNC_LIMIT","Limit"); //12.1 and 14.0 +define("SYNC_PARTIAL","Partial"); //12.1 and 14.0 +define("SYNC_CONVERSATIONMODE","ConversationMode"); //14.0 +define("SYNC_HEARTBEATINTERVAL","HeartbeatInterval"); //14.0 + +// POOMCONTACTS +define("SYNC_POOMCONTACTS_ANNIVERSARY","POOMCONTACTS:Anniversary"); +define("SYNC_POOMCONTACTS_ASSISTANTNAME","POOMCONTACTS:AssistantName"); +define("SYNC_POOMCONTACTS_ASSISTNAMEPHONENUMBER","POOMCONTACTS:AssistnamePhoneNumber"); +define("SYNC_POOMCONTACTS_BIRTHDAY","POOMCONTACTS:Birthday"); +define("SYNC_POOMCONTACTS_BODY","POOMCONTACTS:Body"); +define("SYNC_POOMCONTACTS_BODYSIZE","POOMCONTACTS:BodySize"); +define("SYNC_POOMCONTACTS_BODYTRUNCATED","POOMCONTACTS:BodyTruncated"); +define("SYNC_POOMCONTACTS_BUSINESS2PHONENUMBER","POOMCONTACTS:Business2PhoneNumber"); +define("SYNC_POOMCONTACTS_BUSINESSCITY","POOMCONTACTS:BusinessCity"); +define("SYNC_POOMCONTACTS_BUSINESSCOUNTRY","POOMCONTACTS:BusinessCountry"); +define("SYNC_POOMCONTACTS_BUSINESSPOSTALCODE","POOMCONTACTS:BusinessPostalCode"); +define("SYNC_POOMCONTACTS_BUSINESSSTATE","POOMCONTACTS:BusinessState"); +define("SYNC_POOMCONTACTS_BUSINESSSTREET","POOMCONTACTS:BusinessStreet"); +define("SYNC_POOMCONTACTS_BUSINESSFAXNUMBER","POOMCONTACTS:BusinessFaxNumber"); +define("SYNC_POOMCONTACTS_BUSINESSPHONENUMBER","POOMCONTACTS:BusinessPhoneNumber"); +define("SYNC_POOMCONTACTS_CARPHONENUMBER","POOMCONTACTS:CarPhoneNumber"); +define("SYNC_POOMCONTACTS_CATEGORIES","POOMCONTACTS:Categories"); +define("SYNC_POOMCONTACTS_CATEGORY","POOMCONTACTS:Category"); +define("SYNC_POOMCONTACTS_CHILDREN","POOMCONTACTS:Children"); +define("SYNC_POOMCONTACTS_CHILD","POOMCONTACTS:Child"); +define("SYNC_POOMCONTACTS_COMPANYNAME","POOMCONTACTS:CompanyName"); +define("SYNC_POOMCONTACTS_DEPARTMENT","POOMCONTACTS:Department"); +define("SYNC_POOMCONTACTS_EMAIL1ADDRESS","POOMCONTACTS:Email1Address"); +define("SYNC_POOMCONTACTS_EMAIL2ADDRESS","POOMCONTACTS:Email2Address"); +define("SYNC_POOMCONTACTS_EMAIL3ADDRESS","POOMCONTACTS:Email3Address"); +define("SYNC_POOMCONTACTS_FILEAS","POOMCONTACTS:FileAs"); +define("SYNC_POOMCONTACTS_FIRSTNAME","POOMCONTACTS:FirstName"); +define("SYNC_POOMCONTACTS_HOME2PHONENUMBER","POOMCONTACTS:Home2PhoneNumber"); +define("SYNC_POOMCONTACTS_HOMECITY","POOMCONTACTS:HomeCity"); +define("SYNC_POOMCONTACTS_HOMECOUNTRY","POOMCONTACTS:HomeCountry"); +define("SYNC_POOMCONTACTS_HOMEPOSTALCODE","POOMCONTACTS:HomePostalCode"); +define("SYNC_POOMCONTACTS_HOMESTATE","POOMCONTACTS:HomeState"); +define("SYNC_POOMCONTACTS_HOMESTREET","POOMCONTACTS:HomeStreet"); +define("SYNC_POOMCONTACTS_HOMEFAXNUMBER","POOMCONTACTS:HomeFaxNumber"); +define("SYNC_POOMCONTACTS_HOMEPHONENUMBER","POOMCONTACTS:HomePhoneNumber"); +define("SYNC_POOMCONTACTS_JOBTITLE","POOMCONTACTS:JobTitle"); +define("SYNC_POOMCONTACTS_LASTNAME","POOMCONTACTS:LastName"); +define("SYNC_POOMCONTACTS_MIDDLENAME","POOMCONTACTS:MiddleName"); +define("SYNC_POOMCONTACTS_MOBILEPHONENUMBER","POOMCONTACTS:MobilePhoneNumber"); +define("SYNC_POOMCONTACTS_OFFICELOCATION","POOMCONTACTS:OfficeLocation"); +define("SYNC_POOMCONTACTS_OTHERCITY","POOMCONTACTS:OtherCity"); +define("SYNC_POOMCONTACTS_OTHERCOUNTRY","POOMCONTACTS:OtherCountry"); +define("SYNC_POOMCONTACTS_OTHERPOSTALCODE","POOMCONTACTS:OtherPostalCode"); +define("SYNC_POOMCONTACTS_OTHERSTATE","POOMCONTACTS:OtherState"); +define("SYNC_POOMCONTACTS_OTHERSTREET","POOMCONTACTS:OtherStreet"); +define("SYNC_POOMCONTACTS_PAGERNUMBER","POOMCONTACTS:PagerNumber"); +define("SYNC_POOMCONTACTS_RADIOPHONENUMBER","POOMCONTACTS:RadioPhoneNumber"); +define("SYNC_POOMCONTACTS_SPOUSE","POOMCONTACTS:Spouse"); +define("SYNC_POOMCONTACTS_SUFFIX","POOMCONTACTS:Suffix"); +define("SYNC_POOMCONTACTS_TITLE","POOMCONTACTS:Title"); +define("SYNC_POOMCONTACTS_WEBPAGE","POOMCONTACTS:WebPage"); +define("SYNC_POOMCONTACTS_YOMICOMPANYNAME","POOMCONTACTS:YomiCompanyName"); +define("SYNC_POOMCONTACTS_YOMIFIRSTNAME","POOMCONTACTS:YomiFirstName"); +define("SYNC_POOMCONTACTS_YOMILASTNAME","POOMCONTACTS:YomiLastName"); +define("SYNC_POOMCONTACTS_RTF","POOMCONTACTS:Rtf"); +define("SYNC_POOMCONTACTS_PICTURE","POOMCONTACTS:Picture"); +define("SYNC_POOMCONTACTS_ALIAS","POOMCONTACTS:Alias"); //14.0 +define("SYNC_POOMCONTACTS_WEIGHEDRANK","POOMCONTACTS:WeightedRank"); //14.0 + +// POOMMAIL +define("SYNC_POOMMAIL_ATTACHMENT","POOMMAIL:Attachment"); +define("SYNC_POOMMAIL_ATTACHMENTS","POOMMAIL:Attachments"); +define("SYNC_POOMMAIL_ATTNAME","POOMMAIL:AttName"); +define("SYNC_POOMMAIL_ATTSIZE","POOMMAIL:AttSize"); +define("SYNC_POOMMAIL_ATTOID","POOMMAIL:AttOid"); +define("SYNC_POOMMAIL_ATTMETHOD","POOMMAIL:AttMethod"); +define("SYNC_POOMMAIL_ATTREMOVED","POOMMAIL:AttRemoved"); +define("SYNC_POOMMAIL_BODY","POOMMAIL:Body"); +define("SYNC_POOMMAIL_BODYSIZE","POOMMAIL:BodySize"); +define("SYNC_POOMMAIL_BODYTRUNCATED","POOMMAIL:BodyTruncated"); +define("SYNC_POOMMAIL_DATERECEIVED","POOMMAIL:DateReceived"); +define("SYNC_POOMMAIL_DISPLAYNAME","POOMMAIL:DisplayName"); +define("SYNC_POOMMAIL_DISPLAYTO","POOMMAIL:DisplayTo"); +define("SYNC_POOMMAIL_IMPORTANCE","POOMMAIL:Importance"); +define("SYNC_POOMMAIL_MESSAGECLASS","POOMMAIL:MessageClass"); +define("SYNC_POOMMAIL_SUBJECT","POOMMAIL:Subject"); +define("SYNC_POOMMAIL_READ","POOMMAIL:Read"); +define("SYNC_POOMMAIL_TO","POOMMAIL:To"); +define("SYNC_POOMMAIL_CC","POOMMAIL:Cc"); +define("SYNC_POOMMAIL_FROM","POOMMAIL:From"); +define("SYNC_POOMMAIL_REPLY_TO","POOMMAIL:Reply-To"); +define("SYNC_POOMMAIL_ALLDAYEVENT","POOMMAIL:AllDayEvent"); +define("SYNC_POOMMAIL_CATEGORIES","POOMMAIL:Categories"); //not supported in 12.1 +define("SYNC_POOMMAIL_CATEGORY","POOMMAIL:Category"); //not supported in 12.1 +define("SYNC_POOMMAIL_DTSTAMP","POOMMAIL:DtStamp"); +define("SYNC_POOMMAIL_ENDTIME","POOMMAIL:EndTime"); +define("SYNC_POOMMAIL_INSTANCETYPE","POOMMAIL:InstanceType"); +define("SYNC_POOMMAIL_BUSYSTATUS","POOMMAIL:BusyStatus"); +define("SYNC_POOMMAIL_LOCATION","POOMMAIL:Location"); +define("SYNC_POOMMAIL_MEETINGREQUEST","POOMMAIL:MeetingRequest"); +define("SYNC_POOMMAIL_ORGANIZER","POOMMAIL:Organizer"); +define("SYNC_POOMMAIL_RECURRENCEID","POOMMAIL:RecurrenceId"); +define("SYNC_POOMMAIL_REMINDER","POOMMAIL:Reminder"); +define("SYNC_POOMMAIL_RESPONSEREQUESTED","POOMMAIL:ResponseRequested"); +define("SYNC_POOMMAIL_RECURRENCES","POOMMAIL:Recurrences"); +define("SYNC_POOMMAIL_RECURRENCE","POOMMAIL:Recurrence"); +define("SYNC_POOMMAIL_TYPE","POOMMAIL:Type"); +define("SYNC_POOMMAIL_UNTIL","POOMMAIL:Until"); +define("SYNC_POOMMAIL_OCCURRENCES","POOMMAIL:Occurrences"); +define("SYNC_POOMMAIL_INTERVAL","POOMMAIL:Interval"); +define("SYNC_POOMMAIL_DAYOFWEEK","POOMMAIL:DayOfWeek"); +define("SYNC_POOMMAIL_DAYOFMONTH","POOMMAIL:DayOfMonth"); +define("SYNC_POOMMAIL_WEEKOFMONTH","POOMMAIL:WeekOfMonth"); +define("SYNC_POOMMAIL_MONTHOFYEAR","POOMMAIL:MonthOfYear"); +define("SYNC_POOMMAIL_STARTTIME","POOMMAIL:StartTime"); +define("SYNC_POOMMAIL_SENSITIVITY","POOMMAIL:Sensitivity"); +define("SYNC_POOMMAIL_TIMEZONE","POOMMAIL:TimeZone"); +define("SYNC_POOMMAIL_GLOBALOBJID","POOMMAIL:GlobalObjId"); +define("SYNC_POOMMAIL_THREADTOPIC","POOMMAIL:ThreadTopic"); +define("SYNC_POOMMAIL_MIMEDATA","POOMMAIL:MIMEData"); +define("SYNC_POOMMAIL_MIMETRUNCATED","POOMMAIL:MIMETruncated"); +define("SYNC_POOMMAIL_MIMESIZE","POOMMAIL:MIMESize"); +define("SYNC_POOMMAIL_INTERNETCPID","POOMMAIL:InternetCPID"); +define("SYNC_POOMMAIL_FLAG", "POOMMAIL:Flag"); //12.0, 12.1 and 14.0 +define("SYNC_POOMMAIL_FLAGSTATUS", "POOMMAIL:FlagStatus"); //12.0, 12.1 and 14.0 +define("SYNC_POOMMAIL_CONTENTCLASS", "POOMMAIL:ContentClass"); //12.0, 12.1 and 14.0 +define("SYNC_POOMMAIL_FLAGTYPE", "POOMMAIL:FlagType"); //12.0, 12.1 and 14.0 +define("SYNC_POOMMAIL_COMPLETETIME", "POOMMAIL:CompleteTime"); //14.0 +define("SYNC_POOMMAIL_DISALLOWNEWTIMEPROPOSAL", "POOMMAIL:DisallowNewTimeProposal"); //14.0 + +// AIRNOTIFY +define("SYNC_AIRNOTIFY_NOTIFY","AirNotify:Notify"); +define("SYNC_AIRNOTIFY_NOTIFICATION","AirNotify:Notification"); +define("SYNC_AIRNOTIFY_VERSION","AirNotify:Version"); +define("SYNC_AIRNOTIFY_LIFETIME","AirNotify:Lifetime"); +define("SYNC_AIRNOTIFY_DEVICEINFO","AirNotify:DeviceInfo"); +define("SYNC_AIRNOTIFY_ENABLE","AirNotify:Enable"); +define("SYNC_AIRNOTIFY_FOLDER","AirNotify:Folder"); +define("SYNC_AIRNOTIFY_SERVERENTRYID","AirNotify:ServerEntryId"); +define("SYNC_AIRNOTIFY_DEVICEADDRESS","AirNotify:DeviceAddress"); +define("SYNC_AIRNOTIFY_VALIDCARRIERPROFILES","AirNotify:ValidCarrierProfiles"); +define("SYNC_AIRNOTIFY_CARRIERPROFILE","AirNotify:CarrierProfile"); +define("SYNC_AIRNOTIFY_STATUS","AirNotify:Status"); +define("SYNC_AIRNOTIFY_REPLIES","AirNotify:Replies"); +define("SYNC_AIRNOTIFY_VERSION='1.1'","AirNotify:Version='1.1'"); +define("SYNC_AIRNOTIFY_DEVICES","AirNotify:Devices"); +define("SYNC_AIRNOTIFY_DEVICE","AirNotify:Device"); +define("SYNC_AIRNOTIFY_ID","AirNotify:Id"); +define("SYNC_AIRNOTIFY_EXPIRY","AirNotify:Expiry"); +define("SYNC_AIRNOTIFY_NOTIFYGUID","AirNotify:NotifyGUID"); + +// POOMCAL +define("SYNC_POOMCAL_TIMEZONE","POOMCAL:Timezone"); +define("SYNC_POOMCAL_ALLDAYEVENT","POOMCAL:AllDayEvent"); +define("SYNC_POOMCAL_ATTENDEES","POOMCAL:Attendees"); +define("SYNC_POOMCAL_ATTENDEE","POOMCAL:Attendee"); +define("SYNC_POOMCAL_EMAIL","POOMCAL:Email"); +define("SYNC_POOMCAL_NAME","POOMCAL:Name"); +define("SYNC_POOMCAL_BODY","POOMCAL:Body"); +define("SYNC_POOMCAL_BODYTRUNCATED","POOMCAL:BodyTruncated"); +define("SYNC_POOMCAL_BUSYSTATUS","POOMCAL:BusyStatus"); +define("SYNC_POOMCAL_CATEGORIES","POOMCAL:Categories"); +define("SYNC_POOMCAL_CATEGORY","POOMCAL:Category"); +define("SYNC_POOMCAL_RTF","POOMCAL:Rtf"); +define("SYNC_POOMCAL_DTSTAMP","POOMCAL:DtStamp"); +define("SYNC_POOMCAL_ENDTIME","POOMCAL:EndTime"); +define("SYNC_POOMCAL_EXCEPTION","POOMCAL:Exception"); +define("SYNC_POOMCAL_EXCEPTIONS","POOMCAL:Exceptions"); +define("SYNC_POOMCAL_DELETED","POOMCAL:Deleted"); +define("SYNC_POOMCAL_EXCEPTIONSTARTTIME","POOMCAL:ExceptionStartTime"); +define("SYNC_POOMCAL_LOCATION","POOMCAL:Location"); +define("SYNC_POOMCAL_MEETINGSTATUS","POOMCAL:MeetingStatus"); +define("SYNC_POOMCAL_ORGANIZEREMAIL","POOMCAL:OrganizerEmail"); +define("SYNC_POOMCAL_ORGANIZERNAME","POOMCAL:OrganizerName"); +define("SYNC_POOMCAL_RECURRENCE","POOMCAL:Recurrence"); +define("SYNC_POOMCAL_TYPE","POOMCAL:Type"); +define("SYNC_POOMCAL_UNTIL","POOMCAL:Until"); +define("SYNC_POOMCAL_OCCURRENCES","POOMCAL:Occurrences"); +define("SYNC_POOMCAL_INTERVAL","POOMCAL:Interval"); +define("SYNC_POOMCAL_DAYOFWEEK","POOMCAL:DayOfWeek"); +define("SYNC_POOMCAL_DAYOFMONTH","POOMCAL:DayOfMonth"); +define("SYNC_POOMCAL_WEEKOFMONTH","POOMCAL:WeekOfMonth"); +define("SYNC_POOMCAL_MONTHOFYEAR","POOMCAL:MonthOfYear"); +define("SYNC_POOMCAL_REMINDER","POOMCAL:Reminder"); +define("SYNC_POOMCAL_SENSITIVITY","POOMCAL:Sensitivity"); +define("SYNC_POOMCAL_SUBJECT","POOMCAL:Subject"); +define("SYNC_POOMCAL_STARTTIME","POOMCAL:StartTime"); +define("SYNC_POOMCAL_UID","POOMCAL:UID"); +define("SYNC_POOMCAL_ATTENDEESTATUS","POOMCAL:Attendee_Status"); //12.0, 12.1 and 14.0 +define("SYNC_POOMCAL_ATTENDEETYPE","POOMCAL:Attendee_Type"); //12.0, 12.1 and 14.0 +define("SYNC_POOMCAL_ATTACHMENT","POOMCAL:Attachment"); //12.0, 12.1 and 14.0 +define("SYNC_POOMCAL_ATTACHMENTS","POOMCAL:Attachments"); //12.0, 12.1 and 14.0 +define("SYNC_POOMCAL_ATTNAME","POOMCAL:AttName"); //12.0, 12.1 and 14.0 +define("SYNC_POOMCAL_ATTSIZE","POOMCAL:AttSize"); //12.0, 12.1 and 14.0 +define("SYNC_POOMCAL_ATTOID","POOMCAL:AttOid"); //12.0, 12.1 and 14.0 +define("SYNC_POOMCAL_ATTMETHOD","POOMCAL:AttMethod"); //12.0, 12.1 and 14.0 +define("SYNC_POOMCAL_ATTREMOVED","POOMCAL:AttRemoved"); //12.0, 12.1 and 14.0 +define("SYNC_POOMCAL_DISPLAYNAME","POOMCAL:DisplayName"); //12.0, 12.1 and 14.0 +define("SYNC_POOMCAL_DISALLOWNEWTIMEPROPOSAL","POOMCAL:DisallowNewTimeProposal"); //14.0 +define("SYNC_POOMCAL_RESPONSEREQUESTED","POOMCAL:ResponseRequested"); //14.0 +define("SYNC_POOMCAL_APPOINTMENTREPLYTIME","POOMCAL:AppointmentReplyTime"); //14.0 +define("SYNC_POOMCAL_RESPONSETYPE","POOMCAL:ResponseType"); //14.0 +define("SYNC_POOMCAL_CALENDARTYPE","POOMCAL:CalendarType"); //14.0 +define("SYNC_POOMCAL_ISLEAPMONTH","POOMCAL:IsLeapMonth"); //14.0 +define("SYNC_POOMCAL_FIRSTDAYOFWEEK","POOMCAL:FirstDayOfWeek"); //post 14.0 +define("SYNC_POOMCAL_ONLINEMEETINGINTERNALLINK","POOMCAL:OnlineMeetingInternalLink"); //post 14.0 +define("SYNC_POOMCAL_ONLINEMEETINGEXTERNALLINK","POOMCAL:OnlineMeetingExternalLink"); //post 14.0 + +// Move +define("SYNC_MOVE_MOVES","Move:Moves"); +define("SYNC_MOVE_MOVE","Move:Move"); +define("SYNC_MOVE_SRCMSGID","Move:SrcMsgId"); +define("SYNC_MOVE_SRCFLDID","Move:SrcFldId"); +define("SYNC_MOVE_DSTFLDID","Move:DstFldId"); +define("SYNC_MOVE_RESPONSE","Move:Response"); +define("SYNC_MOVE_STATUS","Move:Status"); +define("SYNC_MOVE_DSTMSGID","Move:DstMsgId"); + +// GetItemEstimate +define("SYNC_GETITEMESTIMATE_GETITEMESTIMATE","GetItemEstimate:GetItemEstimate"); +define("SYNC_GETITEMESTIMATE_VERSION","GetItemEstimate:Version"); +define("SYNC_GETITEMESTIMATE_FOLDERS","GetItemEstimate:Folders"); +define("SYNC_GETITEMESTIMATE_FOLDER","GetItemEstimate:Folder"); +define("SYNC_GETITEMESTIMATE_FOLDERTYPE","GetItemEstimate:FolderType"); +define("SYNC_GETITEMESTIMATE_FOLDERID","GetItemEstimate:FolderId"); +define("SYNC_GETITEMESTIMATE_DATETIME","GetItemEstimate:DateTime"); +define("SYNC_GETITEMESTIMATE_ESTIMATE","GetItemEstimate:Estimate"); +define("SYNC_GETITEMESTIMATE_RESPONSE","GetItemEstimate:Response"); +define("SYNC_GETITEMESTIMATE_STATUS","GetItemEstimate:Status"); + +// FolderHierarchy +define("SYNC_FOLDERHIERARCHY_FOLDERS","FolderHierarchy:Folders"); +define("SYNC_FOLDERHIERARCHY_FOLDER","FolderHierarchy:Folder"); +define("SYNC_FOLDERHIERARCHY_DISPLAYNAME","FolderHierarchy:DisplayName"); +define("SYNC_FOLDERHIERARCHY_SERVERENTRYID","FolderHierarchy:ServerEntryId"); +define("SYNC_FOLDERHIERARCHY_PARENTID","FolderHierarchy:ParentId"); +define("SYNC_FOLDERHIERARCHY_TYPE","FolderHierarchy:Type"); +define("SYNC_FOLDERHIERARCHY_RESPONSE","FolderHierarchy:Response"); +define("SYNC_FOLDERHIERARCHY_STATUS","FolderHierarchy:Status"); +define("SYNC_FOLDERHIERARCHY_CONTENTCLASS","FolderHierarchy:ContentClass"); +define("SYNC_FOLDERHIERARCHY_CHANGES","FolderHierarchy:Changes"); +define("SYNC_FOLDERHIERARCHY_ADD","FolderHierarchy:Add"); +define("SYNC_FOLDERHIERARCHY_REMOVE","FolderHierarchy:Remove"); +define("SYNC_FOLDERHIERARCHY_UPDATE","FolderHierarchy:Update"); +define("SYNC_FOLDERHIERARCHY_SYNCKEY","FolderHierarchy:SyncKey"); +define("SYNC_FOLDERHIERARCHY_FOLDERCREATE","FolderHierarchy:FolderCreate"); +define("SYNC_FOLDERHIERARCHY_FOLDERDELETE","FolderHierarchy:FolderDelete"); +define("SYNC_FOLDERHIERARCHY_FOLDERUPDATE","FolderHierarchy:FolderUpdate"); +define("SYNC_FOLDERHIERARCHY_FOLDERSYNC","FolderHierarchy:FolderSync"); +define("SYNC_FOLDERHIERARCHY_COUNT","FolderHierarchy:Count"); +define("SYNC_FOLDERHIERARCHY_VERSION","FolderHierarchy:Version"); +// only for internal use - never to be streamed to the mobile +define("SYNC_FOLDERHIERARCHY_IGNORE_STORE","FolderHierarchy:IgnoreStore"); + +// MeetingResponse +define("SYNC_MEETINGRESPONSE_CALENDARID","MeetingResponse:CalendarId"); +define("SYNC_MEETINGRESPONSE_FOLDERID","MeetingResponse:FolderId"); +define("SYNC_MEETINGRESPONSE_MEETINGRESPONSE","MeetingResponse:MeetingResponse"); +define("SYNC_MEETINGRESPONSE_REQUESTID","MeetingResponse:RequestId"); +define("SYNC_MEETINGRESPONSE_REQUEST","MeetingResponse:Request"); +define("SYNC_MEETINGRESPONSE_RESULT","MeetingResponse:Result"); +define("SYNC_MEETINGRESPONSE_STATUS","MeetingResponse:Status"); +define("SYNC_MEETINGRESPONSE_USERRESPONSE","MeetingResponse:UserResponse"); +define("SYNC_MEETINGRESPONSE_VERSION","MeetingResponse:Version"); +define("SYNC_MEETINGRESPONSE_INSTANCEID","MeetingResponse:InstanceId"); + +// POOMTASKS +define("SYNC_POOMTASKS_BODY","POOMTASKS:Body"); +define("SYNC_POOMTASKS_BODYSIZE","POOMTASKS:BodySize"); +define("SYNC_POOMTASKS_BODYTRUNCATED","POOMTASKS:BodyTruncated"); +define("SYNC_POOMTASKS_CATEGORIES","POOMTASKS:Categories"); +define("SYNC_POOMTASKS_CATEGORY","POOMTASKS:Category"); +define("SYNC_POOMTASKS_COMPLETE","POOMTASKS:Complete"); +define("SYNC_POOMTASKS_DATECOMPLETED","POOMTASKS:DateCompleted"); +define("SYNC_POOMTASKS_DUEDATE","POOMTASKS:DueDate"); +define("SYNC_POOMTASKS_UTCDUEDATE","POOMTASKS:UtcDueDate"); +define("SYNC_POOMTASKS_IMPORTANCE","POOMTASKS:Importance"); +define("SYNC_POOMTASKS_RECURRENCE","POOMTASKS:Recurrence"); +define("SYNC_POOMTASKS_TYPE","POOMTASKS:Type"); +define("SYNC_POOMTASKS_START","POOMTASKS:Start"); +define("SYNC_POOMTASKS_UNTIL","POOMTASKS:Until"); +define("SYNC_POOMTASKS_OCCURRENCES","POOMTASKS:Occurrences"); +define("SYNC_POOMTASKS_INTERVAL","POOMTASKS:Interval"); +define("SYNC_POOMTASKS_DAYOFWEEK","POOMTASKS:DayOfWeek"); +define("SYNC_POOMTASKS_DAYOFMONTH","POOMTASKS:DayOfMonth"); +define("SYNC_POOMTASKS_WEEKOFMONTH","POOMTASKS:WeekOfMonth"); +define("SYNC_POOMTASKS_MONTHOFYEAR","POOMTASKS:MonthOfYear"); +define("SYNC_POOMTASKS_REGENERATE","POOMTASKS:Regenerate"); +define("SYNC_POOMTASKS_DEADOCCUR","POOMTASKS:DeadOccur"); +define("SYNC_POOMTASKS_REMINDERSET","POOMTASKS:ReminderSet"); +define("SYNC_POOMTASKS_REMINDERTIME","POOMTASKS:ReminderTime"); +define("SYNC_POOMTASKS_SENSITIVITY","POOMTASKS:Sensitivity"); +define("SYNC_POOMTASKS_STARTDATE","POOMTASKS:StartDate"); +define("SYNC_POOMTASKS_UTCSTARTDATE","POOMTASKS:UtcStartDate"); +define("SYNC_POOMTASKS_SUBJECT","POOMTASKS:Subject"); +define("SYNC_POOMTASKS_RTF","POOMTASKS:Rtf"); +define("SYNC_POOMTASKS_ORDINALDATE","POOMTASKS:OrdinalDate"); //12.0, 12.1 and 14.0 +define("SYNC_POOMTASKS_SUBORDINALDATE","POOMTASKS:SubOrdinalDate"); //12.0, 12.1 and 14.0 +define("SYNC_POOMTASKS_CALENDARTYPE","POOMTASKS:CalendarType"); //14.0 +define("SYNC_POOMTASKS_ISLEAPMONTH","POOMTASKS:IsLeapMonth"); //14.0 +define("SYNC_POOMTASKS_FIRSTDAYOFWEEK","POOMTASKS:FirstDayOfWeek"); // post 14.0 + +// ResolveRecipients +define("SYNC_RESOLVERECIPIENTS_RESOLVERECIPIENTS","ResolveRecipients:ResolveRecipients"); +define("SYNC_RESOLVERECIPIENTS_RESPONSE","ResolveRecipients:Response"); +define("SYNC_RESOLVERECIPIENTS_STATUS","ResolveRecipients:Status"); +define("SYNC_RESOLVERECIPIENTS_TYPE","ResolveRecipients:Type"); +define("SYNC_RESOLVERECIPIENTS_RECIPIENT","ResolveRecipients:Recipient"); +define("SYNC_RESOLVERECIPIENTS_DISPLAYNAME","ResolveRecipients:DisplayName"); +define("SYNC_RESOLVERECIPIENTS_EMAILADDRESS","ResolveRecipients:EmailAddress"); +define("SYNC_RESOLVERECIPIENTS_CERTIFICATES","ResolveRecipients:Certificates"); +define("SYNC_RESOLVERECIPIENTS_CERTIFICATE","ResolveRecipients:Certificate"); +define("SYNC_RESOLVERECIPIENTS_MINICERTIFICATE","ResolveRecipients:MiniCertificate"); +define("SYNC_RESOLVERECIPIENTS_OPTIONS","ResolveRecipients:Options"); +define("SYNC_RESOLVERECIPIENTS_TO","ResolveRecipients:To"); +define("SYNC_RESOLVERECIPIENTS_CERTIFICATERETRIEVAL","ResolveRecipients:CertificateRetrieval"); +define("SYNC_RESOLVERECIPIENTS_RECIPIENTCOUNT","ResolveRecipients:RecipientCount"); +define("SYNC_RESOLVERECIPIENTS_MAXCERTIFICATES","ResolveRecipients:MaxCertificates"); +define("SYNC_RESOLVERECIPIENTS_MAXAMBIGUOUSRECIPIENTS","ResolveRecipients:MaxAmbiguousRecipients"); +define("SYNC_RESOLVERECIPIENTS_CERTIFICATECOUNT","ResolveRecipients:CertificateCount"); +define("SYNC_RESOLVERECIPIENTS_AVAILABILITY","ResolveRecipients:Availability"); //14.0 +define("SYNC_RESOLVERECIPIENTS_STARTTIME","ResolveRecipients:StartTime"); //14.0 +define("SYNC_RESOLVERECIPIENTS_ENDTIME","ResolveRecipients:EndTime"); //14.0 +define("SYNC_RESOLVERECIPIENTS_MERGEDFREEBUSY","ResolveRecipients:MergedFreeBusy"); //14.0 +define("SYNC_RESOLVERECIPIENTS_PICTURE","ResolveRecipients:Picture"); //post 14.0 +define("SYNC_RESOLVERECIPIENTS_MAXSIZE","ResolveRecipients:MaxSize"); //post 14.0 +define("SYNC_RESOLVERECIPIENTS_DATA","ResolveRecipients:Data"); //post 14.0 +define("SYNC_RESOLVERECIPIENTS_MAXPICTURES","ResolveRecipients:MaxPictures"); //post 14.0 + +// ValidateCert +define("SYNC_VALIDATECERT_VALIDATECERT","ValidateCert:ValidateCert"); +define("SYNC_VALIDATECERT_CERTIFICATES","ValidateCert:Certificates"); +define("SYNC_VALIDATECERT_CERTIFICATE","ValidateCert:Certificate"); +define("SYNC_VALIDATECERT_CERTIFICATECHAIN","ValidateCert:CertificateChain"); +define("SYNC_VALIDATECERT_CHECKCRL","ValidateCert:CheckCRL"); +define("SYNC_VALIDATECERT_STATUS","ValidateCert:Status"); + +// POOMCONTACTS2 +define("SYNC_POOMCONTACTS2_CUSTOMERID","POOMCONTACTS2:CustomerId"); +define("SYNC_POOMCONTACTS2_GOVERNMENTID","POOMCONTACTS2:GovernmentId"); +define("SYNC_POOMCONTACTS2_IMADDRESS","POOMCONTACTS2:IMAddress"); +define("SYNC_POOMCONTACTS2_IMADDRESS2","POOMCONTACTS2:IMAddress2"); +define("SYNC_POOMCONTACTS2_IMADDRESS3","POOMCONTACTS2:IMAddress3"); +define("SYNC_POOMCONTACTS2_MANAGERNAME","POOMCONTACTS2:ManagerName"); +define("SYNC_POOMCONTACTS2_COMPANYMAINPHONE","POOMCONTACTS2:CompanyMainPhone"); +define("SYNC_POOMCONTACTS2_ACCOUNTNAME","POOMCONTACTS2:AccountName"); +define("SYNC_POOMCONTACTS2_NICKNAME","POOMCONTACTS2:NickName"); +define("SYNC_POOMCONTACTS2_MMS","POOMCONTACTS2:MMS"); + +// Ping +define("SYNC_PING_PING","Ping:Ping"); +define("SYNC_PING_STATUS","Ping:Status"); +define("SYNC_PING_LIFETIME", "Ping:LifeTime"); +define("SYNC_PING_FOLDERS", "Ping:Folders"); +define("SYNC_PING_FOLDER", "Ping:Folder"); +define("SYNC_PING_SERVERENTRYID", "Ping:ServerEntryId"); +define("SYNC_PING_FOLDERTYPE", "Ping:FolderType"); +define("SYNC_PING_MAXFOLDERS", "Ping:MaxFolders"); //missing in < z-push 2 +define("SYNC_PING_VERSION", "Ping:Version"); //missing in < z-push 2 + +//Provision +define("SYNC_PROVISION_PROVISION", "Provision:Provision"); +define("SYNC_PROVISION_POLICIES", "Provision:Policies"); +define("SYNC_PROVISION_POLICY", "Provision:Policy"); +define("SYNC_PROVISION_POLICYTYPE", "Provision:PolicyType"); +define("SYNC_PROVISION_POLICYKEY", "Provision:PolicyKey"); +define("SYNC_PROVISION_DATA", "Provision:Data"); +define("SYNC_PROVISION_STATUS", "Provision:Status"); +define("SYNC_PROVISION_REMOTEWIPE", "Provision:RemoteWipe"); +define("SYNC_PROVISION_EASPROVISIONDOC", "Provision:EASProvisionDoc"); +define("SYNC_PROVISION_DEVPWENABLED", "Provision:DevicePasswordEnabled"); +define("SYNC_PROVISION_ALPHANUMPWREQ", "Provision:AlphanumericDevicePasswordRequired"); +define("SYNC_PROVISION_DEVENCENABLED", "Provision:DeviceEncryptionEnabled"); +define("SYNC_PROVISION_REQSTORAGECARDENC", "Provision:RequireStorageCardEncryption"); +define("SYNC_PROVISION_PWRECOVERYENABLED", "Provision:PasswordRecoveryEnabled"); +define("SYNC_PROVISION_DOCBROWSEENABLED", "Provision:DocumentBrowseEnabled"); +define("SYNC_PROVISION_ATTENABLED", "Provision:AttachmentsEnabled"); +define("SYNC_PROVISION_MINDEVPWLENGTH", "Provision:MinDevicePasswordLength"); +define("SYNC_PROVISION_MAXINACTTIMEDEVLOCK", "Provision:MaxInactivityTimeDeviceLock"); +define("SYNC_PROVISION_MAXDEVPWFAILEDATTEMPTS", "Provision:MaxDevicePasswordFailedAttempts"); +define("SYNC_PROVISION_MAXATTSIZE", "Provision:MaxAttachmentSize"); +define("SYNC_PROVISION_ALLOWSIMPLEDEVPW", "Provision:AllowSimpleDevicePassword"); +define("SYNC_PROVISION_DEVPWEXPIRATION", "Provision:DevicePasswordExpiration"); +define("SYNC_PROVISION_DEVPWHISTORY", "Provision:DevicePasswordHistory"); +define("SYNC_PROVISION_ALLOWSTORAGECARD", "Provision:AllowStorageCard"); +define("SYNC_PROVISION_ALLOWCAM", "Provision:AllowCamera"); +define("SYNC_PROVISION_REQDEVENC", "Provision:RequireDeviceEncryption"); +define("SYNC_PROVISION_ALLOWUNSIGNEDAPPS", "Provision:AllowUnsignedApplications"); +define("SYNC_PROVISION_ALLOWUNSIGNEDINSTALLATIONPACKAGES", "Provision:AllowUnsignedInstallationPackages"); +define("SYNC_PROVISION_MINDEVPWCOMPLEXCHARS", "Provision:MinDevicePasswordComplexCharacters"); +define("SYNC_PROVISION_ALLOWWIFI", "Provision:AllowWiFi"); +define("SYNC_PROVISION_ALLOWTEXTMESSAGING", "Provision:AllowTextMessaging"); +define("SYNC_PROVISION_ALLOWPOPIMAPEMAIL", "Provision:AllowPOPIMAPEmail"); +define("SYNC_PROVISION_ALLOWBLUETOOTH", "Provision:AllowBluetooth"); +define("SYNC_PROVISION_ALLOWIRDA", "Provision:AllowIrDA"); +define("SYNC_PROVISION_REQMANUALSYNCWHENROAM", "Provision:RequireManualSyncWhenRoaming"); +define("SYNC_PROVISION_ALLOWDESKTOPSYNC", "Provision:AllowDesktopSync"); +define("SYNC_PROVISION_MAXCALAGEFILTER", "Provision:MaxCalendarAgeFilter"); +define("SYNC_PROVISION_ALLOWHTMLEMAIL", "Provision:AllowHTMLEmail"); +define("SYNC_PROVISION_MAXEMAILAGEFILTER", "Provision:MaxEmailAgeFilter"); +define("SYNC_PROVISION_MAXEMAILBODYTRUNCSIZE", "Provision:MaxEmailBodyTruncationSize"); +define("SYNC_PROVISION_MAXEMAILHTMLBODYTRUNCSIZE", "Provision:MaxEmailHTMLBodyTruncationSize"); +define("SYNC_PROVISION_REQSIGNEDSMIMEMESSAGES", "Provision:RequireSignedSMIMEMessages"); +define("SYNC_PROVISION_REQENCSMIMEMESSAGES", "Provision:RequireEncryptedSMIMEMessages"); +define("SYNC_PROVISION_REQSIGNEDSMIMEALGORITHM", "Provision:RequireSignedSMIMEAlgorithm"); +define("SYNC_PROVISION_REQENCSMIMEALGORITHM", "Provision:RequireEncryptionSMIMEAlgorithm"); +define("SYNC_PROVISION_ALLOWSMIMEENCALGORITHNEG", "Provision:AllowSMIMEEncryptionAlgorithmNegotiation"); +define("SYNC_PROVISION_ALLOWSMIMESOFTCERTS", "Provision:AllowSMIMESoftCerts"); +define("SYNC_PROVISION_ALLOWBROWSER", "Provision:AllowBrowser"); +define("SYNC_PROVISION_ALLOWCONSUMEREMAIL", "Provision:AllowConsumerEmail"); +define("SYNC_PROVISION_ALLOWREMOTEDESKTOP", "Provision:AllowRemoteDesktop"); +define("SYNC_PROVISION_ALLOWINTERNETSHARING", "Provision:AllowInternetSharing"); +define("SYNC_PROVISION_UNAPPROVEDINROMAPPLIST", "Provision:UnapprovedInROMApplicationList"); +define("SYNC_PROVISION_APPNAME", "Provision:ApplicationName"); +define("SYNC_PROVISION_APPROVEDAPPLIST", "Provision:ApprovedApplicationList"); +define("SYNC_PROVISION_HASH", "Provision:Hash"); + + +//Search +define("SYNC_SEARCH_SEARCH", "Search:Search"); +define("SYNC_SEARCH_STORE", "Search:Store"); +define("SYNC_SEARCH_NAME", "Search:Name"); +define("SYNC_SEARCH_QUERY", "Search:Query"); +define("SYNC_SEARCH_OPTIONS", "Search:Options"); +define("SYNC_SEARCH_RANGE", "Search:Range"); +define("SYNC_SEARCH_STATUS", "Search:Status"); +define("SYNC_SEARCH_RESPONSE", "Search:Response"); +define("SYNC_SEARCH_RESULT", "Search:Result"); +define("SYNC_SEARCH_PROPERTIES", "Search:Properties"); +define("SYNC_SEARCH_TOTAL", "Search:Total"); +define("SYNC_SEARCH_EQUALTO", "Search:EqualTo"); +define("SYNC_SEARCH_VALUE", "Search:Value"); +define("SYNC_SEARCH_AND", "Search:And"); +define("SYNC_SEARCH_OR", "Search:Or"); +define("SYNC_SEARCH_FREETEXT", "Search:FreeText"); +define("SYNC_SEARCH_DEEPTRAVERSAL", "Search:DeepTraversal"); +define("SYNC_SEARCH_LONGID", "Search:LongId"); +define("SYNC_SEARCH_REBUILDRESULTS", "Search:RebuildResults"); +define("SYNC_SEARCH_LESSTHAN", "Search:LessThan"); +define("SYNC_SEARCH_GREATERTHAN", "Search:GreaterThan"); +define("SYNC_SEARCH_SCHEMA", "Search:Schema"); +define("SYNC_SEARCH_SUPPORTED", "Search:Supported"); +define("SYNC_SEARCH_USERNAME", "Search:UserName"); //12.1 and 14.0 +define("SYNC_SEARCH_PASSWORD", "Search:Password"); //12.1 and 14.0 +define("SYNC_SEARCH_CONVERSATIONID", "Search:ConversationId"); //14.0 +define("SYNC_SEARCH_PICTURE","Search:Picture"); //post 14.0 +define("SYNC_SEARCH_MAXSIZE","Search:MaxSize"); //post 14.0 +define("SYNC_SEARCH_MAXPICTURES","Search:MaxPictures"); //post 14.0 + +//GAL +define("SYNC_GAL_DISPLAYNAME", "GAL:DisplayName"); +define("SYNC_GAL_PHONE", "GAL:Phone"); +define("SYNC_GAL_OFFICE", "GAL:Office"); +define("SYNC_GAL_TITLE", "GAL:Title"); +define("SYNC_GAL_COMPANY", "GAL:Company"); +define("SYNC_GAL_ALIAS", "GAL:Alias"); +define("SYNC_GAL_FIRSTNAME", "GAL:FirstName"); +define("SYNC_GAL_LASTNAME", "GAL:LastName"); +define("SYNC_GAL_HOMEPHONE", "GAL:HomePhone"); +define("SYNC_GAL_MOBILEPHONE", "GAL:MobilePhone"); +define("SYNC_GAL_EMAILADDRESS", "GAL:EmailAddress"); +define("SYNC_GAL_PICTURE","GAL:Picture"); //post 14.0 +define("SYNC_GAL_MAXSIZE","GAL:Status"); //post 14.0 +define("SYNC_GAL_DATA","GAL:Data"); //post 14.0 + +//AirSyncBase //12.0, 12.1 and 14.0 +define("SYNC_AIRSYNCBASE_BODYPREFERENCE", "AirSyncBase:BodyPreference"); +define("SYNC_AIRSYNCBASE_TYPE", "AirSyncBase:Type"); +define("SYNC_AIRSYNCBASE_TRUNCATIONSIZE", "AirSyncBase:TruncationSize"); +define("SYNC_AIRSYNCBASE_ALLORNONE", "AirSyncBase:AllOrNone"); +define("SYNC_AIRSYNCBASE_BODY", "AirSyncBase:Body"); +define("SYNC_AIRSYNCBASE_DATA", "AirSyncBase:Data"); +define("SYNC_AIRSYNCBASE_ESTIMATEDDATASIZE", "AirSyncBase:EstimatedDataSize"); +define("SYNC_AIRSYNCBASE_TRUNCATED", "AirSyncBase:Truncated"); +define("SYNC_AIRSYNCBASE_ATTACHMENTS", "AirSyncBase:Attachments"); +define("SYNC_AIRSYNCBASE_ATTACHMENT", "AirSyncBase:Attachment"); +define("SYNC_AIRSYNCBASE_DISPLAYNAME", "AirSyncBase:DisplayName"); +define("SYNC_AIRSYNCBASE_FILEREFERENCE", "AirSyncBase:FileReference"); +define("SYNC_AIRSYNCBASE_METHOD", "AirSyncBase:Method"); +define("SYNC_AIRSYNCBASE_CONTENTID", "AirSyncBase:ContentId"); +define("SYNC_AIRSYNCBASE_CONTENTLOCATION", "AirSyncBase:ContentLocation"); //not used +define("SYNC_AIRSYNCBASE_ISINLINE", "AirSyncBase:IsInline"); +define("SYNC_AIRSYNCBASE_NATIVEBODYTYPE", "AirSyncBase:NativeBodyType"); +define("SYNC_AIRSYNCBASE_CONTENTTYPE", "AirSyncBase:ContentType"); +define("SYNC_AIRSYNCBASE_PREVIEW", "AirSyncBase:Preview"); //14.0 +define("SYNC_AIRSYNCBASE_BODYPARTPREFERENCE", "AirSyncBase:BodyPartPreference"); //post 14.0 +define("SYNC_AIRSYNCBASE_BODYPART", "AirSyncBase:BodyPart"); //post 14.0 +define("SYNC_AIRSYNCBASE_STATUS", "AirSyncBase:Status"); //post 14.0 + +//Settings //12.0, 12.1 and 14.0 +define("SYNC_SETTINGS_SETTINGS", "Settings:Settings"); +define("SYNC_SETTINGS_STATUS", "Settings:Status"); +define("SYNC_SETTINGS_GET", "Settings:Get"); +define("SYNC_SETTINGS_SET", "Settings:Set"); +define("SYNC_SETTINGS_OOF", "Settings:Oof"); +define("SYNC_SETTINGS_OOFSTATE", "Settings:OofState"); +define("SYNC_SETTINGS_STARTTIME", "Settings:StartTime"); +define("SYNC_SETTINGS_ENDTIME", "Settings:EndTime"); +define("SYNC_SETTINGS_OOFMESSAGE", "Settings:OofMessage"); +define("SYNC_SETTINGS_APPLIESTOINTERVAL", "Settings:AppliesToInternal"); +define("SYNC_SETTINGS_APPLIESTOEXTERNALKNOWN", "Settings:AppliesToExternalKnown"); +define("SYNC_SETTINGS_APPLIESTOEXTERNALUNKNOWN", "Settings:AppliesToExternalUnknown"); +define("SYNC_SETTINGS_ENABLED", "Settings:Enabled"); +define("SYNC_SETTINGS_REPLYMESSAGE", "Settings:ReplyMessage"); +define("SYNC_SETTINGS_BODYTYPE", "Settings:BodyType"); +define("SYNC_SETTINGS_DEVICEPW", "Settings:DevicePassword"); +define("SYNC_SETTINGS_PW", "Settings:Password"); +define("SYNC_SETTINGS_DEVICEINFORMATION", "Settings:DeviceInformaton"); +define("SYNC_SETTINGS_MODEL", "Settings:Model"); +define("SYNC_SETTINGS_IMEI", "Settings:IMEI"); +define("SYNC_SETTINGS_FRIENDLYNAME", "Settings:FriendlyName"); +define("SYNC_SETTINGS_OS", "Settings:OS"); +define("SYNC_SETTINGS_OSLANGUAGE", "Settings:OSLanguage"); +define("SYNC_SETTINGS_PHONENUMBER", "Settings:PhoneNumber"); +define("SYNC_SETTINGS_USERINFORMATION", "Settings:UserInformation"); +define("SYNC_SETTINGS_EMAILADDRESSES", "Settings:EmailAddresses"); +define("SYNC_SETTINGS_SMPTADDRESS", "Settings:SmtpAddress"); +define("SYNC_SETTINGS_USERAGENT", "Settings:UserAgent"); //12.1 and 14.0 +define("SYNC_SETTINGS_ENABLEOUTBOUNDSMS", "Settings:EnableOutboundSMS"); //14.0 +define("SYNC_SETTINGS_MOBILEOPERATOR", "Settings:MobileOperator"); //14.0 +define("SYNC_SETTINGS_PRIMARYSMTPADDRESS", "Settings:PrimarySmtpAddress"); +define("SYNC_SETTINGS_ACCOUNTS", "Settings:Accounts"); +define("SYNC_SETTINGS_ACCOUNT", "Settings:Account"); +define("SYNC_SETTINGS_ACCOUNTID", "Settings:AccountId"); +define("SYNC_SETTINGS_ACCOUNTNAME", "Settings:AccountName"); +define("SYNC_SETTINGS_USERDISPLAYNAME", "Settings:UserDisplayName"); //12.1 and 14.0 +define("SYNC_SETTINGS_SENDDISABLED", "Settings:SendDisabled"); //14.0 +define("SYNC_SETTINGS_IHSMANAGEMENTINFORMATION", "Settings:ihsManagementInformation"); //14.0 +// only for internal use - never to be streamed to the mobile +define("SYNC_SETTINGS_PROP_STATUS", "Settings:PropertyStatus"); + +//DocumentLibrary //12.0, 12.1 and 14.0 +define("SYNC_DOCUMENTLIBRARY_LINKID", "DocumentLibrary:LinkId"); +define("SYNC_DOCUMENTLIBRARY_DISPLAYNAME", "DocumentLibrary:DisplayName"); +define("SYNC_DOCUMENTLIBRARY_ISFOLDER", "DocumentLibrary:IsFolder"); +define("SYNC_DOCUMENTLIBRARY_CREATIONDATE", "DocumentLibrary:CreationDate"); +define("SYNC_DOCUMENTLIBRARY_LASTMODIFIEDDATE", "DocumentLibrary:LastModifiedDate"); +define("SYNC_DOCUMENTLIBRARY_ISHIDDEN", "DocumentLibrary:IsHidden"); +define("SYNC_DOCUMENTLIBRARY_CONTENTLENGTH", "DocumentLibrary:ContentLength"); +define("SYNC_DOCUMENTLIBRARY_CONTENTTYPE", "DocumentLibrary:ContentType"); + +//ItemOperations //12.0, 12.1 and 14.0 +define("SYNC_ITEMOPERATIONS_ITEMOPERATIONS", "ItemOperations:ItemOperations"); +define("SYNC_ITEMOPERATIONS_FETCH", "ItemOperations:Fetch"); +define("SYNC_ITEMOPERATIONS_STORE", "ItemOperations:Store"); +define("SYNC_ITEMOPERATIONS_OPTIONS", "ItemOperations:Options"); +define("SYNC_ITEMOPERATIONS_RANGE", "ItemOperations:Range"); +define("SYNC_ITEMOPERATIONS_TOTAL", "ItemOperations:Total"); +define("SYNC_ITEMOPERATIONS_PROPERTIES", "ItemOperations:Properties"); +define("SYNC_ITEMOPERATIONS_DATA", "ItemOperations:Data"); +define("SYNC_ITEMOPERATIONS_STATUS", "ItemOperations:Status"); +define("SYNC_ITEMOPERATIONS_RESPONSE", "ItemOperations:Response"); +define("SYNC_ITEMOPERATIONS_VERSIONS", "ItemOperations:Version"); +define("SYNC_ITEMOPERATIONS_SCHEMA", "ItemOperations:Schema"); +define("SYNC_ITEMOPERATIONS_PART", "ItemOperations:Part"); +define("SYNC_ITEMOPERATIONS_EMPTYFOLDERCONTENTS", "ItemOperations:EmptyFolderContents"); +define("SYNC_ITEMOPERATIONS_DELETESUBFOLDERS", "ItemOperations:DeleteSubFolders"); +define("SYNC_ITEMOPERATIONS_USERNAME", "ItemOperations:UserName"); //12.1 and 14.0 +define("SYNC_ITEMOPERATIONS_PASSWORD", "ItemOperations:Password"); //12.1 and 14.0 +define("SYNC_ITEMOPERATIONS_MOVE", "ItemOperations:Move"); //14.0 +define("SYNC_ITEMOPERATIONS_DSTFLDID", "ItemOperations:DstFldId"); //14.0 +define("SYNC_ITEMOPERATIONS_CONVERSATIONID", "ItemOperations:ConversationId"); //14.0 +define("SYNC_ITEMOPERATIONS_MOVEALWAYS", "ItemOperations:MoveAlways"); //14.0 + +//ComposeMail //14.0 +define("SYNC_COMPOSEMAIL_SENDMAIL", "ComposeMail:SendMail"); +define("SYNC_COMPOSEMAIL_SMARTFORWARD", "ComposeMail:SmartForward"); +define("SYNC_COMPOSEMAIL_SMARTREPLY", "ComposeMail:SmartReply"); +define("SYNC_COMPOSEMAIL_SAVEINSENTITEMS", "ComposeMail:SaveInSentItems"); +define("SYNC_COMPOSEMAIL_REPLACEMIME", "ComposeMail:ReplaceMime"); +define("SYNC_COMPOSEMAIL_TYPE", "ComposeMail:Type"); +define("SYNC_COMPOSEMAIL_SOURCE", "ComposeMail:Source"); +define("SYNC_COMPOSEMAIL_FOLDERID", "ComposeMail:FolderId"); +define("SYNC_COMPOSEMAIL_ITEMID", "ComposeMail:ItemId"); +define("SYNC_COMPOSEMAIL_LONGID", "ComposeMail:LongId"); +define("SYNC_COMPOSEMAIL_INSTANCEID", "ComposeMail:InstanceId"); +define("SYNC_COMPOSEMAIL_MIME", "ComposeMail:MIME"); +define("SYNC_COMPOSEMAIL_CLIENTID", "ComposeMail:ClientId"); +define("SYNC_COMPOSEMAIL_STATUS", "ComposeMail:Status"); +define("SYNC_COMPOSEMAIL_ACCOUNTID", "ComposeMail:AccountId"); +// only for internal use - never to be streamed to the mobile +define("SYNC_COMPOSEMAIL_REPLYFLAG","ComposeMail:ReplyFlag"); +define("SYNC_COMPOSEMAIL_FORWARDFLAG","ComposeMail:ForwardFlag"); + +//POOMMAIL2 //14.0 +define("SYNC_POOMMAIL2_UMCALLERID", "POOMMAIL2:UmCallerId"); +define("SYNC_POOMMAIL2_UMUSERNOTES", "POOMMAIL2:UmUserNotes"); +define("SYNC_POOMMAIL2_UMATTDURATION", "POOMMAIL2:UmAttDuration"); +define("SYNC_POOMMAIL2_UMATTORDER", "POOMMAIL2:UmAttOrder"); +define("SYNC_POOMMAIL2_CONVERSATIONID", "POOMMAIL2:ConversationId"); +define("SYNC_POOMMAIL2_CONVERSATIONINDEX", "POOMMAIL2:ConversationIndex"); +define("SYNC_POOMMAIL2_LASTVERBEXECUTED", "POOMMAIL2:LastVerbExecuted"); +define("SYNC_POOMMAIL2_LASTVERBEXECUTIONTIME", "POOMMAIL2:LastVerbExecutionTime"); +define("SYNC_POOMMAIL2_RECEIVEDASBCC", "POOMMAIL2:ReceivedAsBcc"); +define("SYNC_POOMMAIL2_SENDER", "POOMMAIL2:Sender"); +define("SYNC_POOMMAIL2_CALENDARTYPE", "POOMMAIL2:CalendarType"); +define("SYNC_POOMMAIL2_ISLEAPMONTH", "POOMMAIL2:IsLeapMonth"); +define("SYNC_POOMMAIL2_ACCOUNTID", "POOMMAIL2:AccountId"); +define("SYNC_POOMMAIL2_FIRSTDAYOFWEEK", "POOMMAIL2:FirstDayOfWeek"); +define("SYNC_POOMMAIL2_MEETINGMESSAGETYPE", "POOMMAIL2:MeetingMessageType"); + +//Notes //14.0 +define("SYNC_NOTES_SUBJECT", "Notes:Subject"); +define("SYNC_NOTES_MESSAGECLASS", "Notes:MessageClass"); +define("SYNC_NOTES_LASTMODIFIEDDATE", "Notes:LastModifiedDate"); +define("SYNC_NOTES_CATEGORIES", "Notes:Categories"); +define("SYNC_NOTES_CATEGORY", "Notes:Category"); + +//RightsManagement //post 14.0 +define("SYNC_RIGHTSMANAGEMENT_SUPPORT", "RightsManagement:RightsManagementSupport"); +define("SYNC_RIGHTSMANAGEMENT_TEMPLATES", "RightsManagement:RightsManagementTemplates"); +define("SYNC_RIGHTSMANAGEMENT_TEMPLATE", "RightsManagement:RightsManagementTemplate"); +define("SYNC_RIGHTSMANAGEMENT_LICENSE", "RightsManagement:RightsManagementLicense"); +define("SYNC_RIGHTSMANAGEMENT_EDITALLOWED", "RightsManagement:EditAllowed"); +define("SYNC_RIGHTSMANAGEMENT_REPLYALLOWED", "RightsManagement:ReplyAllowed"); +define("SYNC_RIGHTSMANAGEMENT_REPLYALLALLOWED", "RightsManagement:ReplyAllAllowed"); +define("SYNC_RIGHTSMANAGEMENT_FORWARDALLOWED", "RightsManagement:ForwardAllowed"); +define("SYNC_RIGHTSMANAGEMENT_MODIFYRECIPIENTSALLOWED", "RightsManagement:ModifyRecipientsAllowed"); +define("SYNC_RIGHTSMANAGEMENT_EXTRACTALLOWED", "RightsManagement:ExtractAllowed"); +define("SYNC_RIGHTSMANAGEMENT_PRINTALLOWED", "RightsManagement:PrintAllowed"); +define("SYNC_RIGHTSMANAGEMENT_EXPORTALLOWED", "RightsManagement:ExportAllowed"); +define("SYNC_RIGHTSMANAGEMENT_PROGRAMMATICACCESSALLOWED", "RightsManagement:ProgrammaticAccessAllowed"); +define("SYNC_RIGHTSMANAGEMENT_RMOWNER", "RightsManagement:RMOwner"); +define("SYNC_RIGHTSMANAGEMENT_CONTENTEXPIRYDATE", "RightsManagement:ContentExpiryDate"); +define("SYNC_RIGHTSMANAGEMENT_TEMPLATEID", "RightsManagement:TemplateID"); +define("SYNC_RIGHTSMANAGEMENT_TEMPLATENAME", "RightsManagement:TemplateName"); +define("SYNC_RIGHTSMANAGEMENT_TEMPLATEDESCRIPTION", "RightsManagement:TemplateDescription"); +define("SYNC_RIGHTSMANAGEMENT_CONTENTOWNER", "RightsManagement:ContentOwner"); +define("SYNC_RIGHTSMANAGEMENT_REMOVERIGHTSMGNTDIST", "RightsManagement:RemoveRightsManagementDistribution"); + +// Other constants +define("SYNC_FOLDER_TYPE_OTHER", 1); +define("SYNC_FOLDER_TYPE_INBOX", 2); +define("SYNC_FOLDER_TYPE_DRAFTS", 3); +define("SYNC_FOLDER_TYPE_WASTEBASKET", 4); +define("SYNC_FOLDER_TYPE_SENTMAIL", 5); +define("SYNC_FOLDER_TYPE_OUTBOX", 6); +define("SYNC_FOLDER_TYPE_TASK", 7); +define("SYNC_FOLDER_TYPE_APPOINTMENT", 8); +define("SYNC_FOLDER_TYPE_CONTACT", 9); +define("SYNC_FOLDER_TYPE_NOTE", 10); +define("SYNC_FOLDER_TYPE_JOURNAL", 11); +define("SYNC_FOLDER_TYPE_USER_MAIL", 12); +define("SYNC_FOLDER_TYPE_USER_APPOINTMENT", 13); +define("SYNC_FOLDER_TYPE_USER_CONTACT", 14); +define("SYNC_FOLDER_TYPE_USER_TASK", 15); +define("SYNC_FOLDER_TYPE_USER_JOURNAL", 16); +define("SYNC_FOLDER_TYPE_USER_NOTE", 17); +define("SYNC_FOLDER_TYPE_UNKNOWN", 18); +define("SYNC_FOLDER_TYPE_RECIPIENT_CACHE", 19); +define("SYNC_FOLDER_TYPE_DUMMY", 999999); + +define("SYNC_CONFLICT_OVERWRITE_SERVER", 0); +define("SYNC_CONFLICT_OVERWRITE_PIM", 1); + +define("SYNC_FILTERTYPE_ALL", 0); +define("SYNC_FILTERTYPE_1DAY", 1); +define("SYNC_FILTERTYPE_3DAYS", 2); +define("SYNC_FILTERTYPE_1WEEK", 3); +define("SYNC_FILTERTYPE_2WEEKS", 4); +define("SYNC_FILTERTYPE_1MONTH", 5); +define("SYNC_FILTERTYPE_3MONTHS", 6); +define("SYNC_FILTERTYPE_6MONTHS", 7); +define("SYNC_FILTERTYPE_INCOMPLETETASKS", 8); + +define("SYNC_TRUNCATION_HEADERS", 0); +define("SYNC_TRUNCATION_512B", 1); +define("SYNC_TRUNCATION_1K", 2); +define("SYNC_TRUNCATION_2K", 3); +define("SYNC_TRUNCATION_5K", 4); +define("SYNC_TRUNCATION_10K", 5); +define("SYNC_TRUNCATION_20K", 6); +define("SYNC_TRUNCATION_50K", 7); +define("SYNC_TRUNCATION_100K", 8); +define("SYNC_TRUNCATION_ALL", 9); + +define("SYNC_PROVISION_STATUS_SUCCESS", 1); +define("SYNC_PROVISION_STATUS_PROTERROR", 2); +define("SYNC_PROVISION_STATUS_SERVERERROR", 3); +define("SYNC_PROVISION_STATUS_DEVEXTMANAGED", 4); + +define("SYNC_PROVISION_POLICYSTATUS_SUCCESS", 1); +define("SYNC_PROVISION_POLICYSTATUS_NOPOLICY", 2); +define("SYNC_PROVISION_POLICYSTATUS_UNKNOWNVALUE", 3); +define("SYNC_PROVISION_POLICYSTATUS_CORRUPTED", 4); +define("SYNC_PROVISION_POLICYSTATUS_POLKEYMISM", 5); + +define("SYNC_PROVISION_RWSTATUS_NA", 0); +define("SYNC_PROVISION_RWSTATUS_OK", 1); +define("SYNC_PROVISION_RWSTATUS_PENDING", 2); +define("SYNC_PROVISION_RWSTATUS_REQUESTED", 4); +define("SYNC_PROVISION_RWSTATUS_WIPED", 8); + +define("SYNC_STATUS_SUCCESS", 1); +define("SYNC_STATUS_INVALIDSYNCKEY", 3); +define("SYNC_STATUS_PROTOCOLLERROR", 4); +define("SYNC_STATUS_SERVERERROR", 5); +define("SYNC_STATUS_CLIENTSERVERCONVERSATIONERROR", 6); +define("SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT", 7); +define("SYNC_STATUS_OBJECTNOTFOUND", 8); +define("SYNC_STATUS_SYNCCANNOTBECOMPLETED", 9); +define("SYNC_STATUS_FOLDERHIERARCHYCHANGED", 12); +define("SYNC_STATUS_SYNCREQUESTINCOMPLETE", 13); +define("SYNC_STATUS_INVALIDWAITORHBVALUE", 14); +define("SYNC_STATUS_SYNCREQUESTINVALID", 15); +define("SYNC_STATUS_RETRY", 16); + +define("SYNC_FSSTATUS_SUCCESS", 1); +define("SYNC_FSSTATUS_FOLDEREXISTS", 2); +define("SYNC_FSSTATUS_SYSTEMFOLDER", 3); +define("SYNC_FSSTATUS_FOLDERDOESNOTEXIST", 4); +define("SYNC_FSSTATUS_PARENTNOTFOUND", 5); +define("SYNC_FSSTATUS_SERVERERROR", 6); +define("SYNC_FSSTATUS_REQUESTTIMEOUT", 8); +define("SYNC_FSSTATUS_SYNCKEYERROR", 9); +define("SYNC_FSSTATUS_MAILFORMEDREQ", 10); +define("SYNC_FSSTATUS_UNKNOWNERROR", 11); +define("SYNC_FSSTATUS_CODEUNKNOWN", 12); + +define("SYNC_GETITEMESTSTATUS_SUCCESS", 1); +define("SYNC_GETITEMESTSTATUS_COLLECTIONINVALID", 2); +define("SYNC_GETITEMESTSTATUS_SYNCSTATENOTPRIMED", 3); +define("SYNC_GETITEMESTSTATUS_SYNCKKEYINVALID", 4); + +define("SYNC_ITEMOPERATIONSSTATUS_SUCCESS", 1); +define("SYNC_ITEMOPERATIONSSTATUS_PROTERROR", 2); +define("SYNC_ITEMOPERATIONSSTATUS_SERVERERROR", 3); +define("SYNC_ITEMOPERATIONSSTATUS_DL_BADURI", 4); +define("SYNC_ITEMOPERATIONSSTATUS_DL_ACCESSDENIED", 5); +define("SYNC_ITEMOPERATIONSSTATUS_DL_NOTFOUND", 6); +define("SYNC_ITEMOPERATIONSSTATUS_DL_CONNFAILED", 7); +define("SYNC_ITEMOPERATIONSSTATUS_DL_BYTERANGEINVALID", 8); +define("SYNC_ITEMOPERATIONSSTATUS_DL_STOREUNKNOWN", 9); +define("SYNC_ITEMOPERATIONSSTATUS_DL_EMPTYFILE", 10); +define("SYNC_ITEMOPERATIONSSTATUS_DL_TOOLARGE", 11); +define("SYNC_ITEMOPERATIONSSTATUS_DL_IOFAILURE", 12); +define("SYNC_ITEMOPERATIONSSTATUS_CONVERSIONFAILED", 14); +define("SYNC_ITEMOPERATIONSSTATUS_INVALIDATT", 15); +define("SYNC_ITEMOPERATIONSSTATUS_BLOCKED", 16); +define("SYNC_ITEMOPERATIONSSTATUS_EMPTYFOLDER", 17); +define("SYNC_ITEMOPERATIONSSTATUS_CREDSREQUIRED", 18); +define("SYNC_ITEMOPERATIONSSTATUS_PROTOCOLERROR", 155); +define("SYNC_ITEMOPERATIONSSTATUS_UNSUPPORTEDACTION", 156); + +define("SYNC_MEETRESPSTATUS_SUCCESS", 1); +define("SYNC_MEETRESPSTATUS_INVALIDMEETREQ", 2); +define("SYNC_MEETRESPSTATUS_MAILBOXERROR", 3); +define("SYNC_MEETRESPSTATUS_SERVERERROR", 4); + +define("SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID", 1); +define("SYNC_MOVEITEMSSTATUS_INVALIDDESTID", 2); +define("SYNC_MOVEITEMSSTATUS_SUCCESS", 3); +define("SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST", 4); +define("SYNC_MOVEITEMSSTATUS_CANNOTMOVE", 5); +define("SYNC_MOVEITEMSSTATUS_SOURCEORDESTLOCKED", 7); + +define("SYNC_PINGSTATUS_HBEXPIRED", 1); +define("SYNC_PINGSTATUS_CHANGES", 2); +define("SYNC_PINGSTATUS_FAILINGPARAMS", 3); +define("SYNC_PINGSTATUS_SYNTAXERROR", 4); +define("SYNC_PINGSTATUS_HBOUTOFRANGE", 5); +define("SYNC_PINGSTATUS_TOOMUCHFOLDERS", 6); +define("SYNC_PINGSTATUS_FOLDERHIERSYNCREQUIRED", 7); +define("SYNC_PINGSTATUS_SERVERERROR", 8); + +define("SYNC_RESOLVERECIPSSTATUS_SUCCESS", 1); +define("SYNC_RESOLVERECIPSSTATUS_PROTOCOLERROR", 5); +define("SYNC_RESOLVERECIPSSTATUS_SERVERERROR", 6); +define("SYNC_RESOLVERECIPSSTATUS_RESPONSE_SUCCESS", 1); +define("SYNC_RESOLVERECIPSSTATUS_RESPONSE_AMBRECIP", 2); +define("SYNC_RESOLVERECIPSSTATUS_RESPONSE_AMBRECIPPARTIAL", 3); +define("SYNC_RESOLVERECIPSSTATUS_RESPONSE_UNRESOLVEDRECIP", 4); +define("SYNC_RESOLVERECIPSSTATUS_CERTIFICATES_SUCCESS", 1); +define("SYNC_RESOLVERECIPSSTATUS_CERTIFICATES_NOVALIDCERT", 7); +define("SYNC_RESOLVERECIPSSTATUS_CERTIFICATES_CERTLIMIT", 8); +define("SYNC_RESOLVERECIPSSTATUS_AVAILABILITY_SUCCESS", 1); +define("SYNC_RESOLVERECIPSSTATUS_AVAILABILITY_MORETHAN100", 160); +define("SYNC_RESOLVERECIPSSTATUS_AVAILABILITY_MORETHAN20", 161); +define("SYNC_RESOLVERECIPSSTATUS_AVAILABILITY_REISSUE", 162); +define("SYNC_RESOLVERECIPSSTATUS_AVAILABILITY_FAILED", 163); +define("SYNC_RESOLVERECIPSSTATUS_PICTURE_SUCCESS", 1); +define("SYNC_RESOLVERECIPSSTATUS_PICTURE_NOFOTO", 173); +define("SYNC_RESOLVERECIPSSTATUS_PICTURE_MAXSIZEEXCEEDED", 174); +define("SYNC_RESOLVERECIPSSTATUS_PICTURE_MAXPICTURESEXCEEDED", 175); + +define("SYNC_SEARCHSTATUS_SUCCESS", 1); +define("SYNC_SEARCHSTATUS_SERVERERROR", 3); +define("SYNC_SEARCHSTATUS_STORE_SUCCESS", 1); +define("SYNC_SEARCHSTATUS_STORE_REQINVALID", 2); +define("SYNC_SEARCHSTATUS_STORE_SERVERERROR", 3); +define("SYNC_SEARCHSTATUS_STORE_BADLINK", 4); +define("SYNC_SEARCHSTATUS_STORE_ACCESSDENIED", 5); +define("SYNC_SEARCHSTATUS_STORE_NOTFOUND", 6); +define("SYNC_SEARCHSTATUS_STORE_CONNECTIONFAILED", 7); +define("SYNC_SEARCHSTATUS_STORE_TOOCOMPLEX", 8); +define("SYNC_SEARCHSTATUS_STORE_TIMEDOUT", 10); +define("SYNC_SEARCHSTATUS_STORE_FOLDERSYNCREQ", 11); +define("SYNC_SEARCHSTATUS_STORE_ENDOFRETRANGE", 12); +define("SYNC_SEARCHSTATUS_STORE_ACCESSBLOCKED", 13); +define("SYNC_SEARCHSTATUS_STORE_CREDENTIALSREQ", 14); +define("SYNC_SEARCHSTATUS_PICTURE_SUCCESS", 1); +define("SYNC_SEARCHSTATUS_PICTURE_NOFOTO", 173); +define("SYNC_SEARCHSTATUS_PICTURE_MAXSIZEEXCEEDED", 174); +define("SYNC_SEARCHSTATUS_PICTURE_MAXPICTURESEXCEEDED", 175); + +define("SYNC_SETTINGSSTATUS_SUCCESS", 1); +define("SYNC_SETTINGSSTATUS_PROTOCOLLERROR", 2); +define("SYNC_SETTINGSSTATUS_DEVINFO_SUCCESS", 1); +define("SYNC_SETTINGSSTATUS_DEVINFO_PROTOCOLLERROR", 2); +define("SYNC_SETTINGSSTATUS_DEVIPASS_SUCCESS", 1); +define("SYNC_SETTINGSSTATUS_DEVIPASS_PROTOCOLLERROR", 2); +define("SYNC_SETTINGSSTATUS_DEVIPASS_INVALIDARGS", 3); +define("SYNC_SETTINGSSTATUS_DEVIPASS_DENIED", 7); +define("SYNC_SETTINGSSTATUS_USERINFO_SUCCESS", 1); +define("SYNC_SETTINGSSTATUS_USERINFO_PROTOCOLLERROR", 2); + +define("SYNC_SETTINGSOOF_DISABLED", 0); +define("SYNC_SETTINGSOOF_GLOBAL", 1); +define("SYNC_SETTINGSOOF_TIMEBASED", 2); + +define("SYNC_MIMETRUNCATION_ALL", 0); +define("SYNC_MIMETRUNCATION_4096", 1); +define("SYNC_MIMETRUNCATION_5120", 2); +define("SYNC_MIMETRUNCATION_7168", 3); +define("SYNC_MIMETRUNCATION_10240", 4); +define("SYNC_MIMETRUNCATION_20480", 5); +define("SYNC_MIMETRUNCATION_51200", 6); +define("SYNC_MIMETRUNCATION_102400", 7); +define("SYNC_MIMETRUNCATION_COMPLETE", 8); + +define("SYNC_MIMESUPPORT_NEVER", 0); +define("SYNC_MIMESUPPORT_SMIME", 1); +define("SYNC_MIMESUPPORT_ALWAYS", 2); + +define("SYNC_VALIDATECERTSTATUS_SUCCESS", 1); +define("SYNC_VALIDATECERTSTATUS_PROTOCOLLERROR", 2); +define("SYNC_VALIDATECERTSTATUS_CANTVALIDATESIG", 3); +define("SYNC_VALIDATECERTSTATUS_DIGIDUNTRUSTED", 4); +define("SYNC_VALIDATECERTSTATUS_CERTCHAINNOTCORRECT", 5); +define("SYNC_VALIDATECERTSTATUS_DIGIDNOTVALIDFORSIGN", 6); +define("SYNC_VALIDATECERTSTATUS_DIGIDNOTVALID", 7); +define("SYNC_VALIDATECERTSTATUS_INVALIDCHAINCERTSTIME", 8); +define("SYNC_VALIDATECERTSTATUS_DIGIDUSEDINCORRECTLY", 9); +define("SYNC_VALIDATECERTSTATUS_INCORRECTDIGIDINFO", 10); +define("SYNC_VALIDATECERTSTATUS_INCORRECTUSEOFDIGIDINCHAIN", 11); +define("SYNC_VALIDATECERTSTATUS_DIGIDDOESNOTMATCHEMAIL", 12); +define("SYNC_VALIDATECERTSTATUS_DIGIDREVOKED", 13); +define("SYNC_VALIDATECERTSTATUS_DIGIDSERVERUNAVAILABLE", 14); +define("SYNC_VALIDATECERTSTATUS_DIGIDINCHAINREVOKED", 15); +define("SYNC_VALIDATECERTSTATUS_DIGIDREVSTATUSUNVALIDATED", 16); +define("SYNC_VALIDATECERTSTATUS_SERVERERROR", 17); + +define("SYNC_COMMONSTATUS_SUCCESS", 1); +define("SYNC_COMMONSTATUS_INVALIDCONTENT", 101); +define("SYNC_COMMONSTATUS_INVALIDWBXML", 102); +define("SYNC_COMMONSTATUS_INVALIDXML", 103); +define("SYNC_COMMONSTATUS_INVALIDDATETIME", 104); +define("SYNC_COMMONSTATUS_INVALIDCOMBINATIONOFIDS", 105); +define("SYNC_COMMONSTATUS_INVALIDIDS", 106); +define("SYNC_COMMONSTATUS_INVALIDMIME", 107); +define("SYNC_COMMONSTATUS_DEVIDMISSINGORINVALID", 108); +define("SYNC_COMMONSTATUS_DEVTYPEMISSINGORINVALID", 109); +define("SYNC_COMMONSTATUS_SERVERERROR", 110); +define("SYNC_COMMONSTATUS_SERVERERRORRETRYLATER", 111); +define("SYNC_COMMONSTATUS_ADACCESSDENIED", 112); +define("SYNC_COMMONSTATUS_MAILBOXQUOTAEXCEEDED", 113); +define("SYNC_COMMONSTATUS_MAILBOXSERVEROFFLINE", 114); +define("SYNC_COMMONSTATUS_SENDQUOTAEXCEEDED", 115); +define("SYNC_COMMONSTATUS_MESSRECIPUNRESOLVED", 116); +define("SYNC_COMMONSTATUS_MESSREPLYNOTALLOWED", 117); +define("SYNC_COMMONSTATUS_MESSPREVSENT", 118); +define("SYNC_COMMONSTATUS_MESSHASNORECIP", 119); +define("SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED", 120); +define("SYNC_COMMONSTATUS_MESSREPLYFAILED", 121); +define("SYNC_COMMONSTATUS_ATTTOOLARGE", 122); +define("SYNC_COMMONSTATUS_USERHASNOMAILBOX", 123); +define("SYNC_COMMONSTATUS_USERCANTBEANONYMOUS", 124); +define("SYNC_COMMONSTATUS_USERPRINCIPALNOTFOUND", 125); +define("SYNC_COMMONSTATUS_USERDISABLEDFORSYNC", 126); +define("SYNC_COMMONSTATUS_USERONNEWMAILBOXCANTSYNC", 127); +define("SYNC_COMMONSTATUS_USERONLEGACYMAILBOXCANTSYNC", 128); +define("SYNC_COMMONSTATUS_DEVICEBLOCKEDFORUSER", 129); +define("SYNC_COMMONSTATUS_ACCESSDENIED", 130); +define("SYNC_COMMONSTATUS_ACCOUNTDISABLED", 131); +define("SYNC_COMMONSTATUS_SYNCSTATENOTFOUND", 132); +define("SYNC_COMMONSTATUS_SYNCSTATELOCKED", 133); +define("SYNC_COMMONSTATUS_SYNCSTATECORRUPT", 134); +define("SYNC_COMMONSTATUS_SYNCSTATEEXISTS", 135); +define("SYNC_COMMONSTATUS_SYNCSTATEVERSIONINVALID", 136); +define("SYNC_COMMONSTATUS_COMMANDONOTSUPPORTED", 137); +define("SYNC_COMMONSTATUS_VERSIONNOTSUPPORTED", 138); +define("SYNC_COMMONSTATUS_DEVNOTFULLYPROVISIONABLE", 139); +define("SYNC_COMMONSTATUS_REMWIPEREQUESTED", 140); +define("SYNC_COMMONSTATUS_LEGACYDEVONSTRICTPOLICY", 141); +define("SYNC_COMMONSTATUS_DEVICENOTPROVISIONED", 142); +define("SYNC_COMMONSTATUS_POLICYREFRESH", 143); +define("SYNC_COMMONSTATUS_INVALIDPOLICYKEY", 144); +define("SYNC_COMMONSTATUS_EXTMANDEVICESNOTALLOWED", 145); +define("SYNC_COMMONSTATUS_NORECURRINCAL", 146); +define("SYNC_COMMONSTATUS_UNEXPECTEDITEMCLASS", 147); +define("SYNC_COMMONSTATUS_REMSERVERHASNOSSL", 148); +define("SYNC_COMMONSTATUS_INVALIDSTOREDREQ", 149); +define("SYNC_COMMONSTATUS_ITEMNOTFOUND", 150); +define("SYNC_COMMONSTATUS_TOOMANYFOLDERS", 151); +define("SYNC_COMMONSTATUS_NOFOLDERSFOUND", 152); +define("SYNC_COMMONSTATUS_ITEMLOSTAFTERMOVE", 153); +define("SYNC_COMMONSTATUS_FAILUREINMOVE", 154); +define("SYNC_COMMONSTATUS_NONPERSISTANTMOVEDISALLOWED", 155); +define("SYNC_COMMONSTATUS_MOVEINVALIDDESTFOLDER", 156); +define("SYNC_COMMONSTATUS_INVALIDACCOUNTID", 166); +define("SYNC_COMMONSTATUS_ACCOUNTSENDDISABLED", 167); +define("SYNC_COMMONSTATUS_IRMFEATUREDISABLED", 168); +define("SYNC_COMMONSTATUS_IRMTRANSIENTERROR", 169); +define("SYNC_COMMONSTATUS_IRMPERMANENTERROR", 170); +define("SYNC_COMMONSTATUS_IRMINVALIDTEMPLATEID", 171); +define("SYNC_COMMONSTATUS_IRMOPERATIONNOTPERMITTED", 172); +define("SYNC_COMMONSTATUS_NOPICTURE", 173); +define("SYNC_COMMONSTATUS_PICTURETOOLARGE", 174); +define("SYNC_COMMONSTATUS_PICTURELIMITREACHED", 175); +define("SYNC_COMMONSTATUS_BODYPARTCONVERSATIONTOOLARGE", 176); +define("SYNC_COMMONSTATUS_MAXDEVICESREACHED", 177); + +define("HTTP_CODE_200", 200); +define("HTTP_CODE_401", 401); +define("HTTP_CODE_449", 449); +define("HTTP_CODE_500", 500); + + +//logging defs +define("LOGLEVEL_OFF", 0); +define("LOGLEVEL_FATAL", 1); +define("LOGLEVEL_ERROR", 2); +define("LOGLEVEL_WARN", 4); +define("LOGLEVEL_INFO", 8); +define("LOGLEVEL_DEBUG", 16); +define("LOGLEVEL_WBXML", 32); +define("LOGLEVEL_DEVICEID", 64); +define("LOGLEVEL_WBXMLSTACK", 128); + +define("LOGLEVEL_ALL", LOGLEVEL_FATAL | LOGLEVEL_ERROR | LOGLEVEL_WARN | LOGLEVEL_INFO | LOGLEVEL_DEBUG | LOGLEVEL_WBXML); + +define("BACKEND_DISCARD_DATA", 1); + +define("SYNC_BODYPREFERENCE_UNDEFINED", 0); +define("SYNC_BODYPREFERENCE_PLAIN", 1); +define("SYNC_BODYPREFERENCE_HTML", 2); +define("SYNC_BODYPREFERENCE_RTF", 3); +define("SYNC_BODYPREFERENCE_MIME", 4); + +define("SYNC_FLAGSTATUS_CLEAR", 0); +define("SYNC_FLAGSTATUS_COMPLETE", 1); +define("SYNC_FLAGSTATUS_ACTIVE", 2); + +define("DEFAULT_EMAIL_CONTENTCLASS", "urn:content-classes:message"); + +define("SYNC_MAIL_LASTVERB_UNKNOWN", 0); +define("SYNC_MAIL_LASTVERB_REPLYSENDER", 1); +define("SYNC_MAIL_LASTVERB_REPLYALL", 2); +define("SYNC_MAIL_LASTVERB_FORWARD", 3); + +define("INTERNET_CPID_WINDOWS1252", 1252); +define("INTERNET_CPID_UTF8", 65001); + +define("MAPI_E_NOT_ENOUGH_MEMORY_32BIT", -2147024882); +define("MAPI_E_NOT_ENOUGH_MEMORY_64BIT", 2147942414); + +define("SYNC_SETTINGSOOF_BODYTYPE_HTML", "HTML"); +define("SYNC_SETTINGSOOF_BODYTYPE_TEXT", "TEXT"); + +define("SYNC_FILEAS_FIRSTLAST", 1); +define("SYNC_FILEAS_LASTFIRST", 2); +define("SYNC_FILEAS_COMPANYONLY", 3); +define("SYNC_FILEAS_COMPANYLAST", 4); +define("SYNC_FILEAS_COMPANYFIRST", 5); +define("SYNC_FILEAS_LASTCOMPANY", 6); +define("SYNC_FILEAS_FIRSTCOMPANY", 7); + +define ("SYNC_RESOLVERECIPIENTS_TYPE_GAL", 1); +define ("SYNC_RESOLVERECIPIENTS_TYPE_CONTACT", 2); + +define("SYNC_RESOLVERECIPIENTS_CERTRETRIEVE_NO", 1); +define("SYNC_RESOLVERECIPIENTS_CERTRETRIEVE_FULL", 2); +define("SYNC_RESOLVERECIPIENTS_CERTRETRIEVE_MINI", 3); + +define("NOTEIVERB_REPLYTOSENDER", 102); +define("NOTEIVERB_REPLYTOALL", 103); +define("NOTEIVERB_FORWARD", 104); + +define("AS_REPLYTOSENDER", 1); +define("AS_REPLYTOALL", 2); +define("AS_FORWARD", 3); + +?> \ No newline at end of file diff --git a/sources/lib/default/backend.php b/sources/lib/default/backend.php new file mode 100644 index 0000000..d7c7c09 --- /dev/null +++ b/sources/lib/default/backend.php @@ -0,0 +1,294 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +abstract class Backend implements IBackend { + protected $permanentStorage; + protected $stateStorage; + + /** + * Constructor + * + * @access public + */ + public function Backend() { + } + + /** + * Returns a IStateMachine implementation used to save states + * The default StateMachine should be used here, so, false is fine + * + * @access public + * @return boolean/object + */ + public function GetStateMachine() { + return false; + } + + /** + * Returns a ISearchProvider implementation used for searches + * the SearchProvider is just a stub + * + * @access public + * @return object Implementation of ISearchProvider + */ + public function GetSearchProvider() { + return new SearchProvider(); + } + + /** + * Indicates which AS version is supported by the backend. + * By default AS version 2.5 (ASV_25) is returned (Z-Push 1 standard). + * Subclasses can overwrite this method to set another AS version + * + * @access public + * @return string AS version constant + */ + public function GetSupportedASVersion() { + return ZPush::ASV_25; + } + + /********************************************************************* + * Methods to be implemented + * + * public function Logon($username, $domain, $password); + * public function Setup($store, $checkACLonly = false, $folderid = false); + * public function Logoff(); + * public function GetHierarchy(); + * public function GetImporter($folderid = false); + * public function GetExporter($folderid = false); + * public function SendMail($sm); + * public function Fetch($folderid, $id, $contentparameters); + * public function GetWasteBasket(); + * public function GetAttachmentData($attname); + * public function MeetingResponse($requestid, $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) { + return false; + } + + /** + * 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() { + return false; + } + + /** + * 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) { + return false; + } + + /** + * 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) { + return array(); + } + + /** + * Applies settings to and gets informations from the device + * + * @param SyncObject $settings (SyncOOF or SyncUserInformation possible) + * + * @access public + * @return SyncObject $settings + */ + public function Settings($settings) { + if ($settings instanceof SyncOOF || $settings instanceof SyncUserInformation) + $settings->Status = SYNC_SETTINGSSTATUS_SUCCESS; + return $settings; + } + + /** + * Resolves recipients + * + * @param SyncObject $resolveRecipients + * + * @access public + * @return SyncObject $resolveRecipients + */ + public function ResolveRecipients($resolveRecipients) { + $r = new SyncResolveRecipients(); + $r->status = SYNC_RESOLVERECIPSSTATUS_PROTOCOLERROR; + $r->recipient = array(); + return $r; + } + + + /**---------------------------------------------------------------------------------------------------------- + * Protected methods for BackendStorage + * + * Backends can use a permanent and a state related storage to save additional data + * used during the synchronization. + * + * While permament storage is bound to the device and user, state related data works linked + * to the regular states (and its counters). + * + * Both consist of a StateObject, while the backend can decide what to save in it. + * + * Before using $this->permanentStorage and $this->stateStorage the initilize methods have to be + * called from the backend. + * + * Backend->LogOff() must call $this->SaveStorages() so the data is written to disk! + * + * These methods are an abstraction layer for StateManager->Get/SetBackendStorage() + * which can also be used independently. + */ + + /** + * Loads the permanent storage data of the user and device + * + * @access protected + * @return + */ + protected function InitializePermanentStorage() { + if (!isset($this->permanentStorage)) { + try { + $this->permanentStorage = ZPush::GetDeviceManager()->GetStateManager()->GetBackendStorage(StateManager::BACKENDSTORAGE_PERMANENT); + } + catch (StateNotYetAvailableException $snyae) { + $this->permanentStorage = new StateObject(); + } + catch(StateNotFoundException $snfe) { + $this->permanentStorage = new StateObject(); + } + } + } + + /** + * Loads the state related storage data of the user and device + * All data not necessary for the next state should be removed + * + * @access protected + * @return + */ + protected function InitializeStateStorage() { + if (!isset($this->stateStorage)) { + try { + $this->stateStorage = ZPush::GetDeviceManager()->GetStateManager()->GetBackendStorage(StateManager::BACKENDSTORAGE_STATE); + } + catch (StateNotYetAvailableException $snyae) { + $this->stateStorage = new StateObject(); + } + catch(StateNotFoundException $snfe) { + $this->stateStorage = new StateObject(); + } + } + } + + /** + * Saves the permanent and state related storage data of the user and device + * if they were loaded previousily + * If the backend storage is used this should be called + * + * @access protected + * @return + */ + protected function SaveStorages() { + if (isset($this->permanentStorage)) { + try { + ZPush::GetDeviceManager()->GetStateManager()->SetBackendStorage($this->permanentStorage, StateManager::BACKENDSTORAGE_PERMANENT); + } + catch (StateNotYetAvailableException $snyae) { } + catch(StateNotFoundException $snfe) { } + } + if (isset($this->stateStorage)) { + try { + $this->storage_state = ZPush::GetDeviceManager()->GetStateManager()->SetBackendStorage($this->stateStorage, StateManager::BACKENDSTORAGE_STATE); + } + catch (StateNotYetAvailableException $snyae) { } + catch(StateNotFoundException $snfe) { } + } + } + +} +?> \ No newline at end of file diff --git a/sources/lib/default/diffbackend/diffbackend.php b/sources/lib/default/diffbackend/diffbackend.php new file mode 100644 index 0000000..0abb9b5 --- /dev/null +++ b/sources/lib/default/diffbackend/diffbackend.php @@ -0,0 +1,378 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// default backend +include_once('lib/default/backend.php'); + +// DiffBackend components +include_once('diffstate.php'); +include_once('importchangesdiff.php'); +include_once('exportchangesdiff.php'); + + +abstract class BackendDiff extends Backend { + protected $store; + + /** + * Setup the backend to work on a specific store or checks ACLs there. + * If only the $store is submitted, all Import/Export/Fetch/Etc operations should be + * performed on this store (switch operations store). + * If the ACL check is enabled, this operation should just indicate the ACL status on + * the submitted store, without changing the store for operations. + * For the ACL status, the currently logged on user MUST have access rights on + * - the entire store - admin access if no folderid is sent, or + * - on a specific folderid in the store (secretary/full access rights) + * + * The ACLcheck MUST fail if a folder of the authenticated user is checked! + * + * @param string $store target store, could contain a "domain\user" value + * @param boolean $checkACLonly if set to true, Setup() should just check ACLs + * @param string $folderid if set, only ACLs on this folderid are relevant + * + * @access public + * @return boolean + */ + public function Setup($store, $checkACLonly = false, $folderid = false) { + $this->store = $store; + + // we don't know if and how diff backends implement the "admin" check, but this will disable it for the webservice + // backends which want to implement this, need to overwrite this method explicitely. For more info see https://jira.zarafa.com/browse/ZP-462 + if ($store == "SYSTEM" && $checkACLonly == true) + return false; + + return true; + } + + /** + * Returns an array of SyncFolder types with the entire folder hierarchy + * on the server (the array itself is flat, but refers to parents via the 'parent' property + * + * provides AS 1.0 compatibility + * + * @access public + * @return array SYNC_FOLDER + */ + function GetHierarchy() { + $folders = array(); + + $fl = $this->GetFolderList(); + if (is_array($fl)) + foreach($fl as $f) + $folders[] = $this->GetFolder($f['id']); + + return $folders; + } + + /** + * Returns the importer to process changes from the mobile + * If no $folderid is given, hierarchy importer is expected + * + * @param string $folderid (opt) + * + * @access public + * @return object(ImportChanges) + * @throws StatusException + */ + public function GetImporter($folderid = false) { + return new ImportChangesDiff($this, $folderid); + } + + /** + * Returns the exporter to send changes to the mobile + * If no $folderid is given, hierarchy exporter is expected + * + * @param string $folderid (opt) + * + * @access public + * @return object(ExportChanges) + * @throws StatusException + */ + public function GetExporter($folderid = false) { + return new ExportChangesDiff($this, $folderid); + } + + /** + * Returns all available data of a single message + * + * @param string $folderid + * @param string $id + * @param ContentParameters $contentparameters flag + * + * @access public + * @return object(SyncObject) + * @throws StatusException + */ + public function Fetch($folderid, $id, $contentparameters) { + // override truncation + $contentparameters->SetTruncation(SYNC_TRUNCATION_ALL); + $msg = $this->GetMessage($folderid, $id, $contentparameters); + if ($msg === false) + throw new StatusException("BackendDiff->Fetch('%s','%s'): Error, unable retrieve message from backend", SYNC_STATUS_OBJECTNOTFOUND); + return $msg; + } + + /** + * Processes a response to a meeting request. + * CalendarID is a reference and has to be set if a new calendar item is created + * + * @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) { + throw new StatusException(sprintf("BackendDiff->MeetingResponse('%s','%s','%s'): Error, this functionality is not supported by the diff backend", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_MAILBOXERROR); + } + + /**---------------------------------------------------------------------------------------------------------- + * Abstract DiffBackend methods + * + * Need to be implemented in the actual diff backend + */ + + /** + * Returns a list (array) of folders, each entry being an associative array + * with the same entries as StatFolder(). This method should return stable information; ie + * if nothing has changed, the items in the array must be exactly the same. The order of + * the items within the array is not important though. + * + * @access protected + * @return array/boolean false if the list could not be retrieved + */ + public abstract function GetFolderList(); + + /** + * Returns an actual SyncFolder object with all the properties set. Folders + * are pretty simple, having only a type, a name, a parent and a server ID. + * + * @param string $id id of the folder + * + * @access public + * @return object SyncFolder with information + */ + public abstract function GetFolder($id); + + /** + * Returns folder stats. An associative array with properties is expected. + * + * @param string $id id of the folder + * + * @access public + * @return array + * Associative array( + * string "id" The server ID that will be used to identify the folder. It must be unique, and not too long + * How long exactly is not known, but try keeping it under 20 chars or so. It must be a string. + * string "parent" The server ID of the parent of the folder. Same restrictions as 'id' apply. + * long "mod" This is the modification signature. It is any arbitrary string which is constant as long as + * the folder has not changed. In practice this means that 'mod' can be equal to the folder name + * as this is the only thing that ever changes in folders. (the type is normally constant) + * ) + */ + public abstract function StatFolder($id); + + /** + * Creates or modifies a folder + * + * @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 abstract function ChangeFolder($folderid, $oldid, $displayname, $type); + + /** + * Deletes a folder + * + * @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 abstract function DeleteFolder($id, $parentid); + + /** + * Returns a list (array) of messages, each entry being an associative array + * with the same entries as StatMessage(). This method should return stable information; ie + * if nothing has changed, the items in the array must be exactly the same. The order of + * the items within the array is not important though. + * + * The $cutoffdate is a date in the past, representing the date since which items should be shown. + * This cutoffdate is determined by the user's setting of getting 'Last 3 days' of e-mail, etc. If + * the cutoffdate is ignored, the user will not be able to select their own cutoffdate, but all + * will work OK apart from that. + * + * @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 abstract function GetMessageList($folderid, $cutoffdate); + + /** + * Returns the actual SyncXXX object type. The '$folderid' of parent folder can be used. + * Mixing item types returned is illegal and will be blocked by the engine; ie returning an Email object in a + * Tasks folder will not do anything. The SyncXXX objects should be filled with as much information as possible, + * but at least the subject, body, to, from, etc. + * + * @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 abstract function GetMessage($folderid, $id, $contentparameters); + + /** + * 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 or boolean if fails + * Associative array( + * string "id" Server unique identifier for the message. Again, try to keep this short (under 20 chars) + * int "flags" simply '0' for unread, '1' for read + * long "mod" This is the modification signature. It is any arbitrary string which is constant as long as + * the message has not changed. As soon as this signature changes, the item is assumed to be completely + * changed, and will be sent to the PDA as a whole. Normally you can use something like the modification + * time for this field, which will change as soon as the contents have changed. + * ) + */ + public abstract function StatMessage($folderid, $id); + + /** + * Called when a message has been changed on the mobile. The new message must be saved to disk. + * The return value must be whatever would be returned from StatMessage() after the message has been saved. + * This way, the 'flags' and the 'mod' properties of the StatMessage() item may change via ChangeMessage(). + * This method will never be called on E-mail items as it's not 'possible' to change e-mail items. It's only + * possible to set them as 'read' or 'unread'. + * + * @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 abstract function ChangeMessage($folderid, $id, $message, $contentParameters); + + /** + * Changes the 'read' flag of a message on disk. The $flags + * parameter can only be '1' (read) or '0' (unread). After a call to + * SetReadFlag(), 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 'read' 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 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 abstract function SetReadFlag($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 + * GetMessageList() should no longer list the message. If it does, the message will be re-sent to the mobile + * as it will be seen as a 'new' item. This means that if this method is not implemented, it's possible to + * delete messages on the PDA, but as soon as a sync is done, the item will be resynched to the mobile + * + * @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 abstract function DeleteMessage($folderid, $id, $contentParameters); + + /** + * Called when the user moves an item on the PDA from one folder to another. Whatever is needed + * to move the message on disk has to be done here. After this call, StatMessage() and GetMessageList() + * should show the items to have a new parent. This means that it will disappear from GetMessageList() + * of the sourcefolder and the destination folder will show the new message + * + * @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 abstract function MoveMessage($folderid, $id, $newfolderid, $contentParameters); + +} +?> \ No newline at end of file diff --git a/sources/lib/default/diffbackend/diffstate.php b/sources/lib/default/diffbackend/diffstate.php new file mode 100644 index 0000000..8f317fa --- /dev/null +++ b/sources/lib/default/diffbackend/diffstate.php @@ -0,0 +1,296 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class DiffState implements IChanges { + protected $syncstate; + protected $backend; + protected $flags; + protected $contentparameters; + protected $cutoffdate; + + /** + * Initializes the state + * + * @param string $state + * @param int $flags + * + * @access public + * @return boolean status flag + * @throws StatusException + */ + public function Config($state, $flags = 0) { + if ($state == "") + $state = array(); + + if (!is_array($state)) + throw new StatusException("Invalid state", SYNC_FSSTATUS_CODEUNKNOWN); + + $this->syncstate = $state; + $this->flags = $flags; + return true; + } + + /** + * Configures additional parameters used for content synchronization + * + * @param ContentParameters $contentparameters + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ConfigContentParameters($contentparameters) { + $this->contentparameters = $contentparameters; + $this->cutoffdate = Utils::GetCutOffDate($contentparameters->GetFilterType()); + } + + /** + * Returns state + * + * @access public + * @return string + * @throws StatusException + */ + public function GetState() { + if (!isset($this->syncstate) || !is_array($this->syncstate)) + throw new StatusException("DiffState->GetState(): Error, state not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN); + + return $this->syncstate; + } + + + /**---------------------------------------------------------------------------------------------------------- + * DiffState specific stuff + */ + + /** + * Comparing function used for sorting of the differential engine + * + * @param array $a + * @param array $b + * + * @access public + * @return boolean + */ + static public function RowCmp($a, $b) { + // TODO implement different comparing functions + return $a["id"] < $b["id"] ? 1 : -1; + } + + /** + * Differential mechanism + * Compares the current syncstate to the sent $new + * + * @param array $new + * + * @access protected + * @return array + */ + protected function getDiffTo($new) { + $changes = array(); + + // Sort both arrays in the same way by ID + usort($this->syncstate, array("DiffState", "RowCmp")); + usort($new, array("DiffState", "RowCmp")); + + $inew = 0; + $iold = 0; + + // Get changes by comparing our list of messages with + // our previous state + while(1) { + $change = array(); + + if($iold >= count($this->syncstate) || $inew >= count($new)) + break; + + if($this->syncstate[$iold]["id"] == $new[$inew]["id"]) { + // Both messages are still available, compare flags and mod + if(isset($this->syncstate[$iold]["flags"]) && isset($new[$inew]["flags"]) && $this->syncstate[$iold]["flags"] != $new[$inew]["flags"]) { + // Flags changed + $change["type"] = "flags"; + $change["id"] = $new[$inew]["id"]; + $change["flags"] = $new[$inew]["flags"]; + $changes[] = $change; + } + + if($this->syncstate[$iold]["mod"] != $new[$inew]["mod"]) { + $change["type"] = "change"; + $change["id"] = $new[$inew]["id"]; + $changes[] = $change; + } + + $inew++; + $iold++; + } else { + if($this->syncstate[$iold]["id"] > $new[$inew]["id"]) { + // Message in state seems to have disappeared (delete) + $change["type"] = "delete"; + $change["id"] = $this->syncstate[$iold]["id"]; + $changes[] = $change; + $iold++; + } else { + // Message in new seems to be new (add) + $change["type"] = "change"; + $change["flags"] = SYNC_NEWMESSAGE; + $change["id"] = $new[$inew]["id"]; + $changes[] = $change; + $inew++; + } + } + } + + while($iold < count($this->syncstate)) { + // All data left in 'syncstate' have been deleted + $change["type"] = "delete"; + $change["id"] = $this->syncstate[$iold]["id"]; + $changes[] = $change; + $iold++; + } + + while($inew < count($new)) { + // All data left in new have been added + $change["type"] = "change"; + $change["flags"] = SYNC_NEWMESSAGE; + $change["id"] = $new[$inew]["id"]; + $changes[] = $change; + $inew++; + } + + return $changes; + } + + /** + * Update the state to reflect changes + * + * @param string $type of change + * @param array $change + * + * + * @access protected + * @return + */ + protected function updateState($type, $change) { + // Change can be a change or an add + if($type == "change") { + for($i=0; $i < count($this->syncstate); $i++) { + if($this->syncstate[$i]["id"] == $change["id"]) { + $this->syncstate[$i] = $change; + return; + } + } + // Not found, add as new + $this->syncstate[] = $change; + } else { + for($i=0; $i < count($this->syncstate); $i++) { + // Search for the entry for this item + if($this->syncstate[$i]["id"] == $change["id"]) { + if($type == "flags") { + // Update flags + $this->syncstate[$i]["flags"] = $change["flags"]; + } else if($type == "delete") { + // Delete item + array_splice($this->syncstate, $i, 1); + } + return; + } + } + } + } + + /** + * Returns TRUE if the given ID conflicts with the given operation. This is only true in the following situations: + * - Changed here and changed there + * - Changed here and deleted there + * - Deleted here and changed there + * Any other combination of operations can be done (e.g. change flags & move or move & delete) + * + * @param string $type of change + * @param string $folderid + * @param string $id + * + * @access protected + * @return + */ + protected function isConflict($type, $folderid, $id) { + $stat = $this->backend->StatMessage($folderid, $id); + + if(!$stat) { + // Message is gone + if($type == "change") + return true; // deleted here, but changed there + else + return false; // all other remote changes still result in a delete (no conflict) + } + + foreach($this->syncstate as $state) { + if($state["id"] == $id) { + $oldstat = $state; + break; + } + } + + if(!isset($oldstat)) { + // New message, can never conflict + return false; + } + + if($stat["mod"] != $oldstat["mod"]) { + // Changed here + if($type == "delete" || $type == "change") + return true; // changed here, but deleted there -> conflict, or changed here and changed there -> conflict + else + return false; // changed here, and other remote changes (move or flags) + } + } + +} + +?> \ No newline at end of file diff --git a/sources/lib/default/diffbackend/exportchangesdiff.php b/sources/lib/default/diffbackend/exportchangesdiff.php new file mode 100644 index 0000000..982d7bd --- /dev/null +++ b/sources/lib/default/diffbackend/exportchangesdiff.php @@ -0,0 +1,218 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class ExportChangesDiff extends DiffState implements IExportChanges{ + private $importer; + private $folderid; + private $changes; + private $step; + + /** + * Constructor + * + * @param object $backend + * @param string $folderid + * + * @access public + * @throws StatusException + */ + public function ExportChangesDiff($backend, $folderid) { + $this->backend = $backend; + $this->folderid = $folderid; + } + + /** + * Sets the importer the exporter will sent it's changes to + * and initializes the Exporter + * + * @param object &$importer Implementation of IImportChanges + * + * @access public + * @return boolean + * @throws StatusException + */ + public function InitializeExporter(&$importer) { + $this->changes = array(); + $this->step = 0; + $this->importer = $importer; + + if($this->folderid) { + // Get the changes since the last sync + if(!isset($this->syncstate) || !$this->syncstate) + $this->syncstate = array(); + + ZLog::Write(LOGLEVEL_DEBUG,sprintf("ExportChangesDiff->InitializeExporter(): Initializing message diff engine. '%d' messages in state", count($this->syncstate))); + + //do nothing if it is a dummy folder + if ($this->folderid != SYNC_FOLDER_TYPE_DUMMY) { + // Get our lists - syncstate (old) and msglist (new) + $msglist = $this->backend->GetMessageList($this->folderid, $this->cutoffdate); + // if the folder was deleted, no information is available anymore. A hierarchysync should be executed + if($msglist === false) + throw new StatusException("ExportChangesDiff->InitializeExporter(): Error, no message list available from the backend", SYNC_STATUS_FOLDERHIERARCHYCHANGED, null, LOGLEVEL_INFO); + + $this->changes = $this->getDiffTo($msglist); + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, "Initializing folder diff engine"); + + ZLog::Write(LOGLEVEL_DEBUG, "ExportChangesDiff->InitializeExporter(): Initializing folder diff engine"); + + $folderlist = $this->backend->GetFolderList(); + if($folderlist === false) + throw new StatusException("ExportChangesDiff->InitializeExporter(): error, no folders available from the backend", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN); + + if(!isset($this->syncstate) || !$this->syncstate) + $this->syncstate = array(); + + $this->changes = $this->getDiffTo($folderlist); + } + + ZLog::Write(LOGLEVEL_INFO, sprintf("ExportChangesDiff->InitializeExporter(): Found '%d' changes", count($this->changes) )); + } + + /** + * Returns the amount of changes to be exported + * + * @access public + * @return int + */ + public function GetChangeCount() { + return count($this->changes); + } + + /** + * Synchronizes a change + * + * @access public + * @return array + */ + public function Synchronize() { + $progress = array(); + + // Get one of our stored changes and send it to the importer, store the new state if + // it succeeds + if($this->folderid == false) { + if($this->step < count($this->changes)) { + $change = $this->changes[$this->step]; + + switch($change["type"]) { + case "change": + $folder = $this->backend->GetFolder($change["id"]); + $stat = $this->backend->StatFolder($change["id"]); + + if(!$folder) + return; + + if($this->flags & BACKEND_DISCARD_DATA || $this->importer->ImportFolderChange($folder)) + $this->updateState("change", $stat); + break; + case "delete": + if($this->flags & BACKEND_DISCARD_DATA || $this->importer->ImportFolderDeletion($change["id"])) + $this->updateState("delete", $change); + break; + } + + $this->step++; + + $progress = array(); + $progress["steps"] = count($this->changes); + $progress["progress"] = $this->step; + + return $progress; + } else { + return false; + } + } + else { + if($this->step < count($this->changes)) { + $change = $this->changes[$this->step]; + + switch($change["type"]) { + case "change": + // Note: because 'parseMessage' and 'statMessage' are two seperate + // calls, we have a chance that the message has changed between both + // calls. This may cause our algorithm to 'double see' changes. + + $stat = $this->backend->StatMessage($this->folderid, $change["id"]); + $message = $this->backend->GetMessage($this->folderid, $change["id"], $this->contentparameters); + + // copy the flag to the message + $message->flags = (isset($change["flags"])) ? $change["flags"] : 0; + + if($stat && $message) { + if($this->flags & BACKEND_DISCARD_DATA || $this->importer->ImportMessageChange($change["id"], $message) == true) + $this->updateState("change", $stat); + } + break; + case "delete": + if($this->flags & BACKEND_DISCARD_DATA || $this->importer->ImportMessageDeletion($change["id"]) == true) + $this->updateState("delete", $change); + break; + case "flags": + if($this->flags & BACKEND_DISCARD_DATA || $this->importer->ImportMessageReadFlag($change["id"], $change["flags"]) == true) + $this->updateState("flags", $change); + break; + case "move": + if($this->flags & BACKEND_DISCARD_DATA || $this->importer->ImportMessageMove($change["id"], $change["parent"]) == true) + $this->updateState("move", $change); + break; + } + + $this->step++; + + $progress = array(); + $progress["steps"] = count($this->changes); + $progress["progress"] = $this->step; + + return $progress; + } else { + return false; + } + } + } +} + +?> \ No newline at end of file diff --git a/sources/lib/default/diffbackend/importchangesdiff.php b/sources/lib/default/diffbackend/importchangesdiff.php new file mode 100644 index 0000000..90a8eb3 --- /dev/null +++ b/sources/lib/default/diffbackend/importchangesdiff.php @@ -0,0 +1,275 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class ImportChangesDiff extends DiffState implements IImportChanges { + private $folderid; + + /** + * Constructor + * + * @param object $backend + * @param string $folderid + * + * @access public + * @throws StatusException + */ + public function ImportChangesDiff($backend, $folderid = false) { + $this->backend = $backend; + $this->folderid = $folderid; + } + + /** + * Would load objects which are expected to be exported with this state + * The DiffBackend implements conflict detection on the fly + * + * @param ContentParameters $contentparameters class of objects + * @param string $state + * + * @access public + * @return boolean + * @throws StatusException + */ + public function LoadConflicts($contentparameters, $state) { + // changes are detected on the fly + return true; + } + + /** + * Imports a single message + * + * @param string $id + * @param SyncObject $message + * + * @access public + * @return boolean/string - failure / id of message + * @throws StatusException + */ + public function ImportMessageChange($id, $message) { + //do nothing if it is in a dummy folder + if ($this->folderid == SYNC_FOLDER_TYPE_DUMMY) + throw new StatusException(sprintf("ImportChangesDiff->ImportMessageChange('%s','%s'): can not be done on a dummy folder", $id, get_class($message)), SYNC_STATUS_SYNCCANNOTBECOMPLETED); + + if($id) { + // See if there's a conflict + $conflict = $this->isConflict("change", $this->folderid, $id); + + // Update client state if this is an update + $change = array(); + $change["id"] = $id; + $change["mod"] = 0; // dummy, will be updated later if the change succeeds + $change["parent"] = $this->folderid; + $change["flags"] = (isset($message->read)) ? $message->read : 0; + $this->updateState("change", $change); + + if($conflict && $this->flags == SYNC_CONFLICT_OVERWRITE_PIM) + // in these cases the status SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT should be returned, so the mobile client can inform the end user + throw new StatusException(sprintf("ImportChangesDiff->ImportMessageChange('%s','%s'): Conflict detected. Data from PIM will be dropped! Server overwrites PIM. User is informed.", $id, get_class($message)), SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT, null, LOGLEVEL_INFO); + } + + $stat = $this->backend->ChangeMessage($this->folderid, $id, $message, $this->contentparameters); + + if(!is_array($stat)) + throw new StatusException(sprintf("ImportChangesDiff->ImportMessageChange('%s','%s'): unknown error in backend", $id, get_class($message)), SYNC_STATUS_SYNCCANNOTBECOMPLETED); + + // Record the state of the message + $this->updateState("change", $stat); + + return $stat["id"]; + } + + /** + * Imports a deletion. This may conflict if the local object has been modified + * + * @param string $id + * @param SyncObject $message + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ImportMessageDeletion($id) { + //do nothing if it is in a dummy folder + if ($this->folderid == SYNC_FOLDER_TYPE_DUMMY) + throw new StatusException(sprintf("ImportChangesDiff->ImportMessageDeletion('%s'): can not be done on a dummy folder", $id), SYNC_STATUS_SYNCCANNOTBECOMPLETED); + + // See if there's a conflict + $conflict = $this->isConflict("delete", $this->folderid, $id); + + // Update client state + $change = array(); + $change["id"] = $id; + $this->updateState("delete", $change); + + // If there is a conflict, and the server 'wins', then return without performing the change + // this will cause the exporter to 'see' the overriding item as a change, and send it back to the PIM + if($conflict && $this->flags == SYNC_CONFLICT_OVERWRITE_PIM) { + ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesDiff->ImportMessageDeletion('%s'): Conflict detected. Data from PIM will be dropped! Object was deleted.", $id)); + return false; + } + + $stat = $this->backend->DeleteMessage($this->folderid, $id, $this->contentparameters); + if(!$stat) + throw new StatusException(sprintf("ImportChangesDiff->ImportMessageDeletion('%s'): Unknown error in backend", $id), SYNC_STATUS_OBJECTNOTFOUND); + + return true; + } + + /** + * Imports a change in 'read' flag + * This can never conflict + * + * @param string $id + * @param int $flags - read/unread + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ImportMessageReadFlag($id, $flags) { + //do nothing if it is a dummy folder + if ($this->folderid == SYNC_FOLDER_TYPE_DUMMY) + throw new StatusException(sprintf("ImportChangesDiff->ImportMessageReadFlag('%s','%s'): can not be done on a dummy folder", $id, $flags), SYNC_STATUS_SYNCCANNOTBECOMPLETED); + + // Update client state + $change = array(); + $change["id"] = $id; + $change["flags"] = $flags; + $this->updateState("flags", $change); + + $stat = $this->backend->SetReadFlag($this->folderid, $id, $flags, $this->contentparameters); + if (!$stat) + throw new StatusException(sprintf("ImportChangesDiff->ImportMessageReadFlag('%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 + * + * @param string $id + * @param int $flags - read/unread + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ImportMessageMove($id, $newfolder) { + // don't move messages from or to a dummy folder (GetHierarchy compatibility) + if ($this->folderid == SYNC_FOLDER_TYPE_DUMMY || $newfolder == SYNC_FOLDER_TYPE_DUMMY) + throw new StatusException(sprintf("ImportChangesDiff->ImportMessageMove('%s'): can not be done on a dummy folder", $id), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); + + return $this->backend->MoveMessage($this->folderid, $id, $newfolder, $this->contentparameters); + } + + + /** + * Imports a change on a folder + * + * @param object $folder SyncFolder + * + * @access public + * @return string id of the folder + * @throws StatusException + */ + public function ImportFolderChange($folder) { + $id = $folder->serverid; + $parent = $folder->parentid; + $displayname = $folder->displayname; + $type = $folder->type; + + //do nothing if it is a dummy folder + if ($parent == SYNC_FOLDER_TYPE_DUMMY) + throw new StatusException(sprintf("ImportChangesDiff->ImportFolderChange('%s'): can not be done on a dummy folder", $id), SYNC_FSSTATUS_SERVERERROR); + + if($id) { + $change = array(); + $change["id"] = $id; + $change["mod"] = $displayname; + $change["parent"] = $parent; + $change["flags"] = 0; + $this->updateState("change", $change); + } + + $stat = $this->backend->ChangeFolder($parent, $id, $displayname, $type); + + if($stat) + $this->updateState("change", $stat); + + return $stat["id"]; + } + + /** + * Imports a folder deletion + * + * @param string $id + * @param string $parent id + * + * @access public + * @return int SYNC_FOLDERHIERARCHY_STATUS + * @throws StatusException + */ + public function ImportFolderDeletion($id, $parent = false) { + //do nothing if it is a dummy folder + if ($parent == SYNC_FOLDER_TYPE_DUMMY) + throw new StatusException(sprintf("ImportChangesDiff->ImportFolderDeletion('%s','%s'): can not be done on a dummy folder", $id, $parent), SYNC_FSSTATUS_SERVERERROR); + + // check the foldertype + $folder = $this->backend->GetFolder($id); + if (isset($folder->type) && Utils::IsSystemFolder($folder->type)) + throw new StatusException(sprintf("ImportChangesDiff->ImportFolderDeletion('%s','%s'): Error deleting system/default folder", $id, $parent), SYNC_FSSTATUS_SYSTEMFOLDER); + + $ret = $this->backend->DeleteFolder($id, $parent); + if (!$ret) + throw new StatusException(sprintf("ImportChangesDiff->ImportFolderDeletion('%s','%s'): can not be done on a dummy folder", $id, $parent), SYNC_FSSTATUS_FOLDERDOESNOTEXIST); + + $change = array(); + $change["id"] = $id; + + $this->updateState("delete", $change); + + return true; + } +} + +?> \ No newline at end of file diff --git a/sources/lib/default/filestatemachine.php b/sources/lib/default/filestatemachine.php new file mode 100644 index 0000000..363685c --- /dev/null +++ b/sources/lib/default/filestatemachine.php @@ -0,0 +1,494 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class FileStateMachine implements IStateMachine { + const SUPPORTED_STATE_VERSION = IStateMachine::STATEVERSION_02; + const VERSION = "version"; + + private $userfilename; + private $settingsfilename; + + /** + * Constructor + * + * Performs some basic checks and initilizes the state directory + * + * @access public + * @throws FatalMisconfigurationException + */ + public function FileStateMachine() { + if (!defined('STATE_DIR')) + throw new FatalMisconfigurationException("No configuration for the state directory available."); + + if (substr(STATE_DIR, -1,1) != "/") + throw new FatalMisconfigurationException("The configured state directory should terminate with a '/'"); + + if (!file_exists(STATE_DIR)) + throw new FatalMisconfigurationException("The configured state directory does not exist or can not be accessed: ". STATE_DIR); + // checks if the directory exists and tries to create the necessary subfolders if they do not exist + $this->getDirectoryForDevice(Request::GetDeviceID()); + $this->userfilename = STATE_DIR . 'users'; + $this->settingsfilename = STATE_DIR . 'settings'; + + if ((!file_exists($this->userfilename) && !touch($this->userfilename)) || !is_writable($this->userfilename)) + throw new FatalMisconfigurationException("Not possible to write to the configured state directory."); + Utils::FixFileOwner($this->userfilename); + } + + /** + * 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) { + $filename = $this->getFullFilePath($devid, $type, $key, $counter); + + // the filemodification time is enough to track changes + if(file_exists($filename)) + return filemtime($filename); + else + throw new StateNotFoundException(sprintf("FileStateMachine->GetStateHash(): Could not locate state '%s'",$filename)); + } + + /** + * 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) { + if ($counter && $cleanstates) + $this->CleanStates($devid, $type, $key, $counter); + + // Read current sync state + $filename = $this->getFullFilePath($devid, $type, $key, $counter); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("FileStateMachine->GetState() on file: '%s'", $filename)); + + if(file_exists($filename)) { + return unserialize(file_get_contents($filename)); + } + // throw an exception on all other states, but not FAILSAVE as it's most of the times not there by default + else if ($type !== IStateMachine::FAILSAVE) + throw new StateNotFoundException(sprintf("FileStateMachine->GetState(): Could not locate state '%s'",$filename)); + } + + /** + * 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) { + $state = serialize($state); + + $filename = $this->getFullFilePath($devid, $type, $key, $counter); + if (($bytes = file_put_contents($filename, $state)) === false) + throw new FatalMisconfigurationException(sprintf("FileStateMachine->SetState(): Could not write state '%s'",$filename)); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("FileStateMachine->SetState() written %d bytes on file: '%s'", $bytes, $filename)); + 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) { + $matching_files = glob($this->getFullFilePath($devid, $type, $key). "*", GLOB_NOSORT); + if (is_array($matching_files)) { + foreach($matching_files as $state) { + $file = false; + if($counter !== false && preg_match('/([0-9]+)$/', $state, $matches)) { + if($matches[1] < $counter) { + $candidate = $this->getFullFilePath($devid, $type, $key, (int)$matches[1]); + + if ($candidate == $state) + $file = $candidate; + } + } + else if ($counter === false) + $file = $this->getFullFilePath($devid, $type, $key); + + if ($file !== false) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("FileStateMachine->CleanStates(): Deleting file: '%s'", $file)); + unlink ($file); + } + } + } + } + + /** + * 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) { + include_once("simplemutex.php"); + $mutex = new SimpleMutex(); + $changed = false; + + // exclusive block + if ($mutex->Block()) { + $filecontents = @file_get_contents($this->userfilename); + + if ($filecontents) + $users = unserialize($filecontents); + else + $users = array(); + + // add user/device to the list + if (!isset($users[$username])) { + $users[$username] = array(); + $changed = true; + } + if (!isset($users[$username][$devid])) { + $users[$username][$devid] = 1; + $changed = true; + } + + if ($changed) { + $bytes = file_put_contents($this->userfilename, serialize($users)); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("FileStateMachine->LinkUserDevice(): wrote %d bytes to users file", $bytes)); + } + else + ZLog::Write(LOGLEVEL_DEBUG, "FileStateMachine->LinkUserDevice(): nothing changed"); + + $mutex->Release(); + } + return $changed; + } + + /** + * Unlinks a device from a user + * + * @param string $username + * @param string $devid + * + * @access public + * @return boolean + */ + public function UnLinkUserDevice($username, $devid) { + include_once("simplemutex.php"); + $mutex = new SimpleMutex(); + $changed = false; + + // exclusive block + if ($mutex->Block()) { + $filecontents = @file_get_contents($this->userfilename); + + if ($filecontents) + $users = unserialize($filecontents); + else + $users = array(); + + // is this user listed at all? + if (isset($users[$username])) { + if (isset($users[$username][$devid])) { + unset($users[$username][$devid]); + $changed = true; + } + + // if there is no device left, remove the user + if (empty($users[$username])) { + unset($users[$username]); + $changed = true; + } + } + + if ($changed) { + $bytes = file_put_contents($this->userfilename, serialize($users)); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("FileStateMachine->UnLinkUserDevice(): wrote %d bytes to users file", $bytes)); + } + else + ZLog::Write(LOGLEVEL_DEBUG, "FileStateMachine->UnLinkUserDevice(): nothing changed"); + + $mutex->Release(); + } + 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) { + $out = array(); + if ($username === false) { + foreach (glob(STATE_DIR. "/*/*/*-".IStateMachine::DEVICEDATA, GLOB_NOSORT) as $devdata) + if (preg_match('/\/([A-Za-z0-9]+)-'. IStateMachine::DEVICEDATA. '$/', $devdata, $matches)) + $out[] = $matches[1]; + return $out; + } + else { + $filecontents = file_get_contents($this->userfilename); + if ($filecontents) + $users = unserialize($filecontents); + else + $users = array(); + + // get device list for the user + if (isset($users[$username])) + return array_keys($users[$username]); + else + return array(); + } + } + + /** + * Returns the current version of the state files + * + * @access public + * @return int + */ + public function GetStateVersion() { + if (file_exists($this->settingsfilename)) { + $settings = unserialize(file_get_contents($this->settingsfilename)); + if (strtolower(gettype($settings) == "string") && strtolower($settings) == '2:1:{s:7:"version";s:1:"2";}') { + ZLog::Write(LOGLEVEL_INFO, "Broken state version file found. Attempt to autofix it. See https://jira.zarafa.com/browse/ZP-493 for more information."); + unlink($this->settingsfilename); + $this->SetStateVersion(IStateMachine::STATEVERSION_02); + $settings = array(self::VERSION => IStateMachine::STATEVERSION_02); + } + } + else { + $filecontents = @file_get_contents($this->userfilename); + if ($filecontents) + $settings = array(self::VERSION => IStateMachine::STATEVERSION_01); + else { + $settings = array(self::VERSION => self::SUPPORTED_STATE_VERSION); + $this->SetStateVersion(self::SUPPORTED_STATE_VERSION); + } + } + + return $settings[self::VERSION]; + } + + /** + * Sets the current version of the state files + * + * @param int $version the new supported version + * + * @access public + * @return boolean + */ + public function SetStateVersion($version) { + if (file_exists($this->settingsfilename)) + $settings = unserialize(file_get_contents($this->settingsfilename)); + else + $settings = array(self::VERSION => IStateMachine::STATEVERSION_01); + + $settings[self::VERSION] = $version; + ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->SetStateVersion() saving supported state version, value '%d'", $version)); + $status = file_put_contents($this->settingsfilename, serialize($settings)); + Utils::FixFileOwner($this->settingsfilename); + 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) { + $out = array(); + $devdir = $this->getDirectoryForDevice($devid) . "/$devid-"; + + foreach (glob($devdir . "*", GLOB_NOSORT) as $devdata) { + // cut the device dir away and split into parts + $parts = explode("-", substr($devdata, strlen($devdir))); + + $state = array('type' => false, 'counter' => false, 'uuid' => false); + + if (isset($parts[0]) && $parts[0] == IStateMachine::DEVICEDATA) + $state['type'] = IStateMachine::DEVICEDATA; + + if (isset($parts[0]) && strlen($parts[0]) == 8 && + isset($parts[1]) && strlen($parts[1]) == 4 && + isset($parts[2]) && strlen($parts[2]) == 4 && + isset($parts[3]) && strlen($parts[3]) == 4 && + isset($parts[4]) && strlen($parts[4]) == 12) + $state['uuid'] = $parts[0]."-".$parts[1]."-".$parts[2]."-".$parts[3]."-".$parts[4]; + + if (isset($parts[5]) && is_numeric($parts[5])) { + $state['counter'] = $parts[5]; + $state['type'] = ""; // default + } + + if (isset($parts[5])) { + if (is_int($parts[5])) + $state['counter'] = $parts[5]; + + else if (in_array($parts[5], array(IStateMachine::FOLDERDATA, IStateMachine::FAILSAVE, IStateMachine::HIERARCHY, IStateMachine::BACKENDSTORAGE))) + $state['type'] = $parts[5]; + } + if (isset($parts[6]) && is_numeric($parts[6])) + $state['counter'] = $parts[6]; + + $out[] = $state; + } + return $out; + } + + + /**---------------------------------------------------------------------------------------------------------- + * Private FileStateMachine stuff + */ + + /** + * Returns the full path incl. filename for a key (generally uuid) and a counter + * + * @param string $devid the device id + * @param string $type the state type + * @param string $key (opt) + * @param string $counter (opt) default false + * @param boolean $doNotCreateDirs (opt) indicates if missing subdirectories should be created, default false + * + * @access private + * @return string + * @throws StateInvalidException + */ + private function getFullFilePath($devid, $type, $key = false, $counter = false, $doNotCreateDirs = false) { + $testkey = $devid . (($key !== false)? "-". $key : "") . (($type !== "")? "-". $type : ""); + if (preg_match('/^[a-zA-Z0-9-]+$/', $testkey, $matches) || ($type == "" && $key === false)) + $internkey = $testkey . (($counter && is_int($counter))?"-".$counter:""); + else + throw new StateInvalidException("FileStateMachine->getFullFilePath(): Invalid state deviceid, type, key or in any combination"); + + return $this->getDirectoryForDevice($devid, $doNotCreateDirs) ."/". $internkey; + } + + /** + * Checks if the configured path exists and if a subfolder structure is available + * A two level deep subdirectory structure is build to save the states. + * The subdirectories where to save, are determined with device id + * + * @param string $devid the device id + * @param boolen $doNotCreateDirs (opt) by default false - indicates if the subdirs should be created + * + * @access private + * @return string/boolean returns the full directory of false if the dirs can not be created + * @throws FatalMisconfigurationException when configured directory is not writeable + */ + private function getDirectoryForDevice($devid, $doNotCreateDirs = false) { + $firstLevel = substr(strtolower($devid), -1, 1); + $secondLevel = substr(strtolower($devid), -2, 1); + + $dir = STATE_DIR . $firstLevel . "/" . $secondLevel; + if (is_dir($dir)) + return $dir; + + if ($doNotCreateDirs === false) { + // try to create the subdirectory structure necessary + $fldir = STATE_DIR . $firstLevel; + if (!is_dir($fldir)) { + $dirOK = mkdir($fldir); + if (!$dirOK) + throw new FatalMisconfigurationException("FileStateMachine->getDirectoryForDevice(): Not possible to create state sub-directory: ". $fldir); + } + + if (!is_dir($dir)) { + $dirOK = mkdir($dir); + if (!$dirOK) + throw new FatalMisconfigurationException("FileStateMachine->getDirectoryForDevice(): Not possible to create state sub-directory: ". $dir); + } + else + return $dir; + } + return false; + } + +} +?> \ No newline at end of file diff --git a/sources/lib/default/searchprovider.php b/sources/lib/default/searchprovider.php new file mode 100644 index 0000000..e30187b --- /dev/null +++ b/sources/lib/default/searchprovider.php @@ -0,0 +1,125 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +/********************************************************************* + * The SearchProvider is a stub to implement own search funtionality + * + * If you wish to implement an alternative search method, you should implement the + * ISearchProvider interface like the BackendSearchLDAP backend + */ +class SearchProvider implements ISearchProvider{ + + /** + * Constructor + * initializes the searchprovider to perform the search + * + * @access public + * @return + * @throws StatusException, FatalException + */ + public function SearchProvider() { + } + + /** + * 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) { + return ($searchtype == ISearchProvider::SEARCH_GAL); + } + + /** + * Searches the GAL + * + * @param string $searchquery string to be searched for + * @param string $searchrange specified searchrange + * + * @access public + * @return array search results + * @throws StatusException + */ + public function GetGALSearchResults($searchquery, $searchrange) { + return array(); + } + + /** + * Searches for the emails on the server + * + * @param ContentParameter $cpo + * + * @return array + */ + public function GetMailboxSearchResults($cpo){ + return array(); + } + + /** + * Terminates a search for a given PID + * + * @param int $pid + * + * @return boolean + */ + public function TerminateSearch($pid) { + return true; + } + + /** + * Disconnects from the current search provider + * + * @access public + * @return boolean + */ + public function Disconnect() { + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/default/simplemutex.php b/sources/lib/default/simplemutex.php new file mode 100644 index 0000000..98c74c2 --- /dev/null +++ b/sources/lib/default/simplemutex.php @@ -0,0 +1,89 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SimpleMutex extends InterProcessData { + /** + * Constructor + */ + public function SimpleMutex() { + // initialize super parameters + $this->allocate = 64; + $this->type = 5173; + parent::__construct(); + + if (!$this->IsActive()) { + ZLog::Write(LOGLEVEL_ERROR, "SimpleMutex not available as InterProcessData is not available. This is not recommended on duty systems and may result in corrupt user/device linking."); + } + } + + /** + * Blocks the mutex + * Method blocks until mutex is available! + * ATTENTION: make sure that you *always* release a blocked mutex! + * + * @access public + * @return boolean + */ + public function Block() { + if ($this->IsActive()) + return $this->blockMutex(); + + ZLog::Write(LOGLEVEL_WARN, "Could not enter mutex as InterProcessData is not available. This is not recommended on duty systems and may result in corrupt user/device linking!"); + return true; + } + + /** + * Releases the mutex + * After the release other processes are able to block the mutex themselfs + * + * @access public + * @return boolean + */ + public function Release() { + if ($this->IsActive()) + return $this->releaseMutex(); + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/exceptions/authenticationrequiredexception.php b/sources/lib/exceptions/authenticationrequiredexception.php new file mode 100644 index 0000000..87fa134 --- /dev/null +++ b/sources/lib/exceptions/authenticationrequiredexception.php @@ -0,0 +1,52 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class AuthenticationRequiredException extends HTTPReturnCodeException { + protected $defaultLogLevel = LOGLEVEL_INFO; + protected $httpReturnCode = HTTP_CODE_401; + protected $httpReturnMessage = "Unauthorized"; + protected $httpHeaders = array('WWW-Authenticate: Basic realm="ZPush"'); + protected $showLegal = true; +} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/exceptions.php b/sources/lib/exceptions/exceptions.php new file mode 100644 index 0000000..19c3f48 --- /dev/null +++ b/sources/lib/exceptions/exceptions.php @@ -0,0 +1,66 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +// main exception +include_once('zpushexception.php'); + +// Fatal exceptions +include_once('fatalexception.php'); +include_once('fatalmisconfigurationexception.php'); +include_once('fatalnotimplementedexception.php'); +include_once('wbxmlexception.php'); +include_once('nopostrequestexception.php'); +include_once('httpreturncodeexception.php'); +include_once('authenticationrequiredexception.php'); +include_once('provisioningrequiredexception.php'); + +// Non fatal exceptions +include_once('notimplementedexception.php'); +include_once('syncobjectbrokenexception.php'); +include_once('statusexception.php'); +include_once('statenotfoundexception.php'); +include_once('stateinvalidexception.php'); +include_once('nohierarchycacheavailableexception.php'); +include_once('statenotyetavailableexception.php'); + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/fatalexception.php b/sources/lib/exceptions/fatalexception.php new file mode 100644 index 0000000..3a4b393 --- /dev/null +++ b/sources/lib/exceptions/fatalexception.php @@ -0,0 +1,47 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class FatalException extends ZPushException {} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/fatalmisconfigurationexception.php b/sources/lib/exceptions/fatalmisconfigurationexception.php new file mode 100644 index 0000000..6ddeae9 --- /dev/null +++ b/sources/lib/exceptions/fatalmisconfigurationexception.php @@ -0,0 +1,46 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class FatalMisconfigurationException extends FatalException {} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/fatalnotimplementedexception.php b/sources/lib/exceptions/fatalnotimplementedexception.php new file mode 100644 index 0000000..61b3f82 --- /dev/null +++ b/sources/lib/exceptions/fatalnotimplementedexception.php @@ -0,0 +1,47 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class FatalNotImplementedException extends FatalException {} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/httpreturncodeexception.php b/sources/lib/exceptions/httpreturncodeexception.php new file mode 100644 index 0000000..9228d01 --- /dev/null +++ b/sources/lib/exceptions/httpreturncodeexception.php @@ -0,0 +1,56 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class HTTPReturnCodeException extends FatalException { + protected $defaultLogLevel = LOGLEVEL_ERROR; + protected $showLegal = false; + + public function HTTPReturnCodeException($message = "", $code = 0, $previous = NULL, $logLevel = false) { + if ($code) + $this->httpReturnCode = $code; + parent::__construct($message, (int) $code, $previous, $logLevel); + } +} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/nohierarchycacheavailableexception.php b/sources/lib/exceptions/nohierarchycacheavailableexception.php new file mode 100644 index 0000000..f7fc6dd --- /dev/null +++ b/sources/lib/exceptions/nohierarchycacheavailableexception.php @@ -0,0 +1,46 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class NoHierarchyCacheAvailableException extends StateNotFoundException {} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/nopostrequestexception.php b/sources/lib/exceptions/nopostrequestexception.php new file mode 100644 index 0000000..1130733 --- /dev/null +++ b/sources/lib/exceptions/nopostrequestexception.php @@ -0,0 +1,51 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class NoPostRequestException extends FatalException { + const OPTIONS_REQUEST = 1; + const GET_REQUEST = 2; + protected $defaultLogLevel = LOGLEVEL_DEBUG; +} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/notimplementedexception.php b/sources/lib/exceptions/notimplementedexception.php new file mode 100644 index 0000000..8377729 --- /dev/null +++ b/sources/lib/exceptions/notimplementedexception.php @@ -0,0 +1,49 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class NotImplementedException extends ZPushException { + protected $defaultLogLevel = LOGLEVEL_ERROR; +} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/provisioningrequiredexception.php b/sources/lib/exceptions/provisioningrequiredexception.php new file mode 100644 index 0000000..4a58d8c --- /dev/null +++ b/sources/lib/exceptions/provisioningrequiredexception.php @@ -0,0 +1,51 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class ProvisioningRequiredException extends HTTPReturnCodeException { + protected $defaultLogLevel = LOGLEVEL_INFO; + protected $httpReturnCode = HTTP_CODE_449; + protected $httpReturnMessage = "Retry after sending a PROVISION command"; +} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/stateinvalidexception.php b/sources/lib/exceptions/stateinvalidexception.php new file mode 100644 index 0000000..720c345 --- /dev/null +++ b/sources/lib/exceptions/stateinvalidexception.php @@ -0,0 +1,46 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class StateInvalidException extends StatusException {} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/statenotfoundexception.php b/sources/lib/exceptions/statenotfoundexception.php new file mode 100644 index 0000000..b15bcfc --- /dev/null +++ b/sources/lib/exceptions/statenotfoundexception.php @@ -0,0 +1,47 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class StateNotFoundException extends StatusException {} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/statenotyetavailableexception.php b/sources/lib/exceptions/statenotyetavailableexception.php new file mode 100644 index 0000000..0910185 --- /dev/null +++ b/sources/lib/exceptions/statenotyetavailableexception.php @@ -0,0 +1,46 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class StateNotYetAvailableException extends StatusException {} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/statusexception.php b/sources/lib/exceptions/statusexception.php new file mode 100644 index 0000000..b2d4f17 --- /dev/null +++ b/sources/lib/exceptions/statusexception.php @@ -0,0 +1,48 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class StatusException extends ZPushException { + protected $defaultLogLevel = LOGLEVEL_INFO; +} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/syncobjectbrokenexception.php b/sources/lib/exceptions/syncobjectbrokenexception.php new file mode 100644 index 0000000..0f014f0 --- /dev/null +++ b/sources/lib/exceptions/syncobjectbrokenexception.php @@ -0,0 +1,73 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncObjectBrokenException extends ZPushException { + protected $defaultLogLevel = LOGLEVEL_WARN; + private $syncObject; + + /** + * Returns the SyncObject which caused this Exception (if set) + * + * @access public + * @return SyncObject + */ + public function GetSyncObject() { + return isset($this->syncObject) ? $this->syncObject : false; + } + + /** + * Sets the SyncObject which caused the exception so it can be later retrieved + * + * @param SyncObject $syncobject + * + * @access public + * @return boolean + */ + public function SetSyncObject($syncobject) { + $this->syncObject = $syncobject; + return true; + } +} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/wbxmlexception.php b/sources/lib/exceptions/wbxmlexception.php new file mode 100644 index 0000000..418da3f --- /dev/null +++ b/sources/lib/exceptions/wbxmlexception.php @@ -0,0 +1,46 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class WBXMLException extends FatalNotImplementedException {} + +?> \ No newline at end of file diff --git a/sources/lib/exceptions/zpushexception.php b/sources/lib/exceptions/zpushexception.php new file mode 100644 index 0000000..f6009e8 --- /dev/null +++ b/sources/lib/exceptions/zpushexception.php @@ -0,0 +1,74 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class ZPushException extends Exception { + protected $defaultLogLevel = LOGLEVEL_FATAL; + protected $httpReturnCode = HTTP_CODE_500; + protected $httpReturnMessage = "Internal Server Error"; + protected $httpHeaders = array(); + protected $showLegal = true; + + public function ZPushException($message = "", $code = 0, $previous = NULL, $logLevel = false) { + if (! $message) + $message = $this->httpReturnMessage; + + if (!$logLevel) + $logLevel = $this->defaultLogLevel; + + ZLog::Write($logLevel, get_class($this) .': '. $message . ' - code: '.$code); + parent::__construct($message, (int) $code); + } + + public function getHTTPCodeString() { + return $this->httpReturnCode . " ". $this->httpReturnMessage; + } + + public function getHTTPHeaders() { + return $this->httpHeaders; + } + + public function showLegalNotice() { + return $this->showLegal; + } +} +?> \ No newline at end of file diff --git a/sources/lib/interface/ibackend.php b/sources/lib/interface/ibackend.php new file mode 100644 index 0000000..2ce018b --- /dev/null +++ b/sources/lib/interface/ibackend.php @@ -0,0 +1,294 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +interface IBackend { + /** + * Returns a IStateMachine implementation used to save states + * + * @access public + * @return boolean/object if false is returned, the default Statemachine is + * used else the implementation of IStateMachine + */ + public function GetStateMachine(); + + /** + * Returns a ISearchProvider implementation used for searches + * + * @access public + * @return object Implementation of ISearchProvider + */ + public function GetSearchProvider(); + + /** + * Indicates which AS version is supported by the backend. + * Depending on this value the supported AS version announced to the + * mobile device is set. + * + * @access public + * @return string AS version constant + */ + public function GetSupportedASVersion(); + + /** + * Authenticates the user + * + * @param string $username + * @param string $domain + * @param string $password + * + * @access public + * @return boolean + * @throws FatalException e.g. some required libraries are unavailable + */ + public function Logon($username, $domain, $password); + + /** + * Setup the backend to work on a specific store or checks ACLs there. + * If only the $store is submitted, all Import/Export/Fetch/Etc operations should be + * performed on this store (switch operations store). + * If the ACL check is enabled, this operation should just indicate the ACL status on + * the submitted store, without changing the store for operations. + * For the ACL status, the currently logged on user MUST have access rights on + * - the entire store - admin access if no folderid is sent, or + * - on a specific folderid in the store (secretary/full access rights) + * + * The ACLcheck MUST fail if a folder of the authenticated user is checked! + * + * @param string $store target store, could contain a "domain\user" value + * @param boolean $checkACLonly if set to true, Setup() should just check ACLs + * @param string $folderid if set, only ACLs on this folderid are relevant + * + * @access public + * @return boolean + */ + public function Setup($store, $checkACLonly = false, $folderid = false); + + /** + * Logs off + * non critical operations closing the session should be done here + * + * @access public + * @return boolean + */ + public function Logoff(); + + /** + * Returns an array of SyncFolder types with the entire folder hierarchy + * on the server (the array itself is flat, but refers to parents via the 'parent' property + * + * provides AS 1.0 compatibility + * + * @access public + * @return array SYNC_FOLDER + */ + public function GetHierarchy(); + + /** + * Returns the importer to process changes from the mobile + * If no $folderid is given, hierarchy data will be imported + * With a $folderid a content data will be imported + * + * @param string $folderid (opt) + * + * @access public + * @return object implements IImportChanges + * @throws StatusException + */ + public function GetImporter($folderid = false); + + /** + * Returns the exporter to send changes to the mobile + * If no $folderid is given, hierarchy data should be exported + * With a $folderid a content data is expected + * + * @param string $folderid (opt) + * + * @access public + * @return object implements IExportChanges + * @throws StatusException + */ + public function GetExporter($folderid = false); + + /** + * Sends an e-mail + * This messages needs to be saved into the 'sent items' folder + * + * Basically two things can be done + * 1) Send the message to an SMTP server as-is + * 2) Parse the message, and send it some other way + * + * @param SyncSendMail $sm SyncSendMail object + * + * @access public + * @return boolean + * @throws StatusException + */ + public function SendMail($sm); + + /** + * Returns all available data of a single message + * + * @param string $folderid + * @param string $id + * @param ContentParameters $contentparameters flag + * + * @access public + * @return object(SyncObject) + * @throws StatusException + */ + public function Fetch($folderid, $id, $contentparameters); + + /** + * Returns the waste basket + * + * The waste basked is used when deleting items; if this function returns a valid folder ID, + * then all deletes are handled as moves and are sent to the backend as a move. + * If it returns FALSE, then deletes are handled as real deletes + * + * @access public + * @return string + */ + public function GetWasteBasket(); + + /** + * Returns the content of the named attachment as stream. The passed attachment identifier is + * the exact string that is returned in the 'AttName' property of an SyncAttachment. + * Any information necessary to locate the attachment must be encoded in that 'attname' property. + * Data is written directly - 'print $data;' + * + * @param string $attname + * + * @access public + * @return SyncItemOperationsAttachment + * @throws StatusException + */ + public function GetAttachmentData($attname); + + /** + * 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); + + /** + * Processes a response to a meeting request. + * CalendarID is a reference and has to be set if a new calendar item is created + * + * @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); + + /** + * 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(); + + /** + * 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); + + /** + * 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); + + /** + * Applies settings to and gets informations from the device + * + * @param SyncObject $settings (SyncOOF or SyncUserInformation possible) + * + * @access public + * @return SyncObject $settings + */ + public function Settings($settings); + + /** + * Resolves recipients + * + * @param SyncObject $resolveRecipients + * + * @access public + * @return SyncObject $resolveRecipients + */ + public function ResolveRecipients($resolveRecipients); +} + +?> \ No newline at end of file diff --git a/sources/lib/interface/ichanges.php b/sources/lib/interface/ichanges.php new file mode 100644 index 0000000..3ef95fd --- /dev/null +++ b/sources/lib/interface/ichanges.php @@ -0,0 +1,86 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +interface IChanges { + /** + * Constructor + * + * @throws StatusException + */ + + /** + * Initializes the state and flags + * + * @param string $state + * @param int $flags + * + * @access public + * @return boolean status flag + * @throws StatusException + */ + public function Config($state, $flags = 0); + + /** + * Configures additional parameters used for content synchronization + * + * @param ContentParameters $contentparameters + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ConfigContentParameters($contentparameters); + + /** + * Reads and returns the current state + * + * @access public + * @return string + */ + public function GetState(); +} + +?> \ No newline at end of file diff --git a/sources/lib/interface/iexportchanges.php b/sources/lib/interface/iexportchanges.php new file mode 100644 index 0000000..dc323d1 --- /dev/null +++ b/sources/lib/interface/iexportchanges.php @@ -0,0 +1,76 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +interface IExportChanges extends IChanges { + /** + * Sets the importer where the exporter will sent its changes to + * This exporter should also be ready to accept calls after this + * + * @param object &$importer Implementation of IImportChanges + * + * @access public + * @return boolean + * @throws StatusException + */ + public function InitializeExporter(&$importer); + + /** + * Returns the amount of changes to be exported + * + * @access public + * @return int + */ + public function GetChangeCount(); + + /** + * Synchronizes a change to the configured importer + * + * @access public + * @return array with status information + */ + public function Synchronize(); +} + +?> \ No newline at end of file diff --git a/sources/lib/interface/iimportchanges.php b/sources/lib/interface/iimportchanges.php new file mode 100644 index 0000000..31da666 --- /dev/null +++ b/sources/lib/interface/iimportchanges.php @@ -0,0 +1,143 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +interface IImportChanges extends IChanges { + + /**---------------------------------------------------------------------------------------------------------- + * Methods for to import contents + */ + + /** + * Loads objects which are expected to be exported with the state + * Before importing/saving the actual message from the mobile, a conflict detection should be done + * + * @param ContentParameters $contentparameters + * @param string $state + * + * @access public + * @return boolean + * @throws StatusException + */ + public function LoadConflicts($contentparameters, $state); + + /** + * Imports a single message + * + * @param string $id + * @param SyncObject $message + * + * @access public + * @return boolean/string failure / id of message + * @throws StatusException + */ + public function ImportMessageChange($id, $message); + + /** + * Imports a deletion. This may conflict if the local object has been modified + * + * @param string $id + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ImportMessageDeletion($id); + + /** + * Imports a change in 'read' flag + * This can never conflict + * + * @param string $id + * @param int $flags + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ImportMessageReadFlag($id, $flags); + + /** + * Imports a move of a message. This occurs when a user moves an item to another folder + * + * @param string $id + * @param string $newfolder destination folder + * + * @access public + * @return boolean + * @throws StatusException + */ + public function ImportMessageMove($id, $newfolder); + + + /**---------------------------------------------------------------------------------------------------------- + * Methods to import hierarchy + */ + + /** + * Imports a change on a folder + * + * @param object $folder SyncFolder + * + * @access public + * @return boolean/string status/id of the folder + * @throws StatusException + */ + public function ImportFolderChange($folder); + + /** + * Imports a folder deletion + * + * @param string $id + * @param string $parent id + * + * @access public + * @return boolean/int success/SYNC_FOLDERHIERARCHY_STATUS + * @throws StatusException + */ + public function ImportFolderDeletion($id, $parent = false); + +} + +?> \ No newline at end of file diff --git a/sources/lib/interface/isearchprovider.php b/sources/lib/interface/isearchprovider.php new file mode 100644 index 0000000..d6a24af --- /dev/null +++ b/sources/lib/interface/isearchprovider.php @@ -0,0 +1,107 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +interface ISearchProvider { + const SEARCH_GAL = "GAL"; + const SEARCH_MAILBOX = "MAILBOX"; + const SEARCH_DOCUMENTLIBRARY = "DOCUMENTLIBRARY"; + + /** + * Constructor + * + * @throws StatusException, FatalException + */ + + /** + * Indicates if a search type is supported by this SearchProvider + * Currently only the type SEARCH_GAL (Global Address List) is implemented + * + * @param string $searchtype + * + * @access public + * @return boolean + */ + public function SupportsType($searchtype); + + /** + * Searches the GAL + * + * @param string $searchquery + * @param string $searchrange + * + * @access public + * @return array + * @throws StatusException + */ + public function GetGALSearchResults($searchquery, $searchrange); + + /** + * Searches for the emails on the server + * + * @param ContentParameter $cpo + * + * @return array + */ + public function GetMailboxSearchResults($cpo); + + /** + * Terminates a search for a given PID + * + * @param int $pid + * + * @return boolean + */ + public function TerminateSearch($pid); + + + /** + * Disconnects from the current search provider + * + * @access public + * @return boolean + */ + public function Disconnect(); +} + +?> \ No newline at end of file diff --git a/sources/lib/interface/istatemachine.php b/sources/lib/interface/istatemachine.php new file mode 100644 index 0000000..55c3986 --- /dev/null +++ b/sources/lib/interface/istatemachine.php @@ -0,0 +1,199 @@ +GetStateMachine(). + * Old sync states are not deleted until a new sync state + * is requested. + * At that moment, the PIM is apparently requesting an update + * since sync key X, so any sync states before X are already on + * the PIM, and can therefore be removed. This algorithm should be + * automatically enforced by the IStateMachine implementation. +* +* Created : 02.01.2012 +* +* Copyright 2007 - 2013 Zarafa Deutschland GmbH +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License, version 3, +* as published by the Free Software Foundation with the following additional +* term according to sec. 7: +* +* According to sec. 7 of the GNU Affero General Public License, version 3, +* the terms of the AGPL are supplemented with the following terms: +* +* "Zarafa" is a registered trademark of Zarafa B.V. +* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH +* The licensing of the Program under the AGPL does not imply a trademark license. +* Therefore any rights, title and interest in our trademarks remain entirely with us. +* +* However, if you propagate an unmodified version of the Program you are +* allowed to use the term "Z-Push" to indicate that you distribute the Program. +* Furthermore you may use our trademarks where it is necessary to indicate +* the intended purpose of a product or service provided you use it in accordance +* with honest practices in industrial or commercial matters. +* If you want to propagate modified versions of the Program under the name "Z-Push", +* you may only do so if you have a written permission by Zarafa Deutschland GmbH +* (to acquire a permission please contact Zarafa at trademark@zarafa.com). +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +* +* Consult LICENSE file for details +************************************************/ + +interface IStateMachine { + const DEFTYPE = ""; + const DEVICEDATA = "devicedata"; + const FOLDERDATA = "fd"; + const FAILSAVE = "fs"; + const HIERARCHY = "hc"; + const BACKENDSTORAGE = "bs"; + + const STATEVERSION_01 = "1"; // Z-Push 2.0.x - default value if unset + const STATEVERSION_02 = "2"; // Z-Push 2.1.0 Milestone 1 + + /** + * Constructor + * @throws FatalMisconfigurationException + */ + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * Unlinks a device from a user + * + * @param string $username + * @param string $devid + * + * @access public + * @return boolean + */ + public function UnLinkUserDevice($username, $devid); + + /** + * 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); + + /** + * Returns the current version of the state files + * + * @access public + * @return int + */ + public function GetStateVersion(); + + /** + * Sets the current version of the state files + * + * @param int $version the new supported version + * + * @access public + * @return boolean + */ + public function SetStateVersion($version); + + /** + * Returns all available states for a device id + * + * @param string $devid the device id + * + * @access public + * @return array(mixed) + */ + public function GetAllStatesForDevice($devid); +} + +?> \ No newline at end of file diff --git a/sources/lib/request/folderchange.php b/sources/lib/request/folderchange.php new file mode 100644 index 0000000..253c3f4 --- /dev/null +++ b/sources/lib/request/folderchange.php @@ -0,0 +1,247 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class FolderChange extends RequestProcessor { + + /** + * Handles creates, updates or deletes of a folder + * issued by the commands FolderCreate, FolderUpdate and FolderDelete + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle ($commandCode) { + $el = self::$decoder->getElement(); + + if($el[EN_TYPE] != EN_TYPE_STARTTAG) + return false; + + $create = $update = $delete = false; + if($el[EN_TAG] == SYNC_FOLDERHIERARCHY_FOLDERCREATE) + $create = true; + else if($el[EN_TAG] == SYNC_FOLDERHIERARCHY_FOLDERUPDATE) + $update = true; + else if($el[EN_TAG] == SYNC_FOLDERHIERARCHY_FOLDERDELETE) + $delete = true; + + if(!$create && !$update && !$delete) + return false; + + // SyncKey + if(!self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SYNCKEY)) + return false; + $synckey = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false; + + // ServerID + $serverid = false; + if(self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SERVERENTRYID)) { + $serverid = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false; + } + + // Parent + $parentid = false; + + // when creating or updating more information is necessary + if (!$delete) { + if(self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_PARENTID)) { + $parentid = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false; + } + + // Displayname + if(!self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_DISPLAYNAME)) + return false; + $displayname = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false; + + // Type + $type = false; + if(self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_TYPE)) { + $type = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false; + } + } + + // endtag foldercreate, folderupdate, folderdelete + if(!self::$decoder->getElementEndTag()) + return false; + + $status = SYNC_FSSTATUS_SUCCESS; + // Get state of hierarchy + try { + $syncstate = self::$deviceManager->GetStateManager()->GetSyncState($synckey); + $newsynckey = self::$deviceManager->GetStateManager()->GetNewSyncKey($synckey); + + // Over the ChangesWrapper the HierarchyCache is notified about all changes + $changesMem = self::$deviceManager->GetHierarchyChangesWrapper(); + + // the hierarchyCache should now fully be initialized - check for changes in the additional folders + $changesMem->Config(ZPush::GetAdditionalSyncFolders()); + + // there are unprocessed changes in the hierarchy, trigger resync + if ($changesMem->GetChangeCount() > 0) + throw new StatusException("HandleFolderChange() can not proceed as there are unprocessed hierarchy changes", SYNC_FSSTATUS_SERVERERROR); + + // any additional folders can not be modified! + if ($serverid !== false && ZPush::GetAdditionalSyncFolderStore($serverid)) + throw new StatusException("HandleFolderChange() can not change additional folders which are configured", SYNC_FSSTATUS_SYSTEMFOLDER); + + // switch user store if this this happens inside an additional folder + // if this is an additional folder the backend has to be setup correctly + if (!self::$backend->Setup(ZPush::GetAdditionalSyncFolderStore((($parentid != false)?$parentid:$serverid)))) + throw new StatusException(sprintf("HandleFolderChange() could not Setup() the backend for folder id '%s'", (($parentid != false)?$parentid:$serverid)), SYNC_FSSTATUS_SERVERERROR); + } + catch (StateNotFoundException $snfex) { + $status = SYNC_FSSTATUS_SYNCKEYERROR; + } + catch (StatusException $stex) { + $status = $stex->getCode(); + } + + // set $newsynckey in case of an error + if (!isset($newsynckey)) + $newsynckey = $synckey; + + if ($status == SYNC_FSSTATUS_SUCCESS) { + try { + // Configure importer with last state + $importer = self::$backend->GetImporter(); + $importer->Config($syncstate); + + // the messages from the PIM will be forwarded to the real importer + $changesMem->SetDestinationImporter($importer); + + // process incoming change + if (!$delete) { + // Send change + $folder = new SyncFolder(); + $folder->serverid = $serverid; + $folder->parentid = $parentid; + $folder->displayname = $displayname; + $folder->type = $type; + + $serverid = $changesMem->ImportFolderChange($folder); + } + else { + // delete folder + $changesMem->ImportFolderDeletion($serverid, 0); + } + } + catch (StatusException $stex) { + $status = $stex->getCode(); + } + } + + self::$encoder->startWBXML(); + if ($create) { + + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERCREATE); + { + { + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY); + self::$encoder->content($newsynckey); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_SERVERENTRYID); + self::$encoder->content($serverid); + self::$encoder->endTag(); + } + } + self::$encoder->endTag(); + } + + elseif ($update) { + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERUPDATE); + { + { + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY); + self::$encoder->content($newsynckey); + self::$encoder->endTag(); + } + } + self::$encoder->endTag(); + } + + elseif ($delete) { + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERDELETE); + { + { + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY); + self::$encoder->content($newsynckey); + self::$encoder->endTag(); + } + } + self::$encoder->endTag(); + } + + self::$topCollector->AnnounceInformation(sprintf("Operation status %d", $status), true); + + // Save the sync state for the next time + if (isset($importer)) + self::$deviceManager->GetStateManager()->SetSyncState($newsynckey, $importer->GetState()); + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/foldersync.php b/sources/lib/request/foldersync.php new file mode 100644 index 0000000..92ee92a --- /dev/null +++ b/sources/lib/request/foldersync.php @@ -0,0 +1,239 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class FolderSync extends RequestProcessor { + + /** + * Handles the FolderSync command + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle ($commandCode) { + // Maps serverid -> clientid for items that are received from the PIM + $map = array(); + + // Parse input + if(!self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC)) + return false; + + if(!self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SYNCKEY)) + return false; + + $synckey = self::$decoder->getElementContent(); + + if(!self::$decoder->getElementEndTag()) + return false; + + // every FolderSync with SyncKey 0 should return the supported AS version & command headers + if($synckey == "0") { + self::$specialHeaders = array(); + self::$specialHeaders[] = ZPush::GetSupportedProtocolVersions(); + self::$specialHeaders[] = ZPush::GetSupportedCommands(); + } + + $status = SYNC_FSSTATUS_SUCCESS; + $newsynckey = $synckey; + try { + $syncstate = self::$deviceManager->GetStateManager()->GetSyncState($synckey); + + // We will be saving the sync state under 'newsynckey' + $newsynckey = self::$deviceManager->GetStateManager()->GetNewSyncKey($synckey); + } + catch (StateNotFoundException $snfex) { + $status = SYNC_FSSTATUS_SYNCKEYERROR; + } + catch (StateInvalidException $sive) { + $status = SYNC_FSSTATUS_SYNCKEYERROR; + } + + // The ChangesWrapper caches all imports in-memory, so we can send a change count + // before sending the actual data. + // the HierarchyCache is notified and the changes from the PIM are transmitted to the actual backend + $changesMem = self::$deviceManager->GetHierarchyChangesWrapper(); + + // the hierarchyCache should now fully be initialized - check for changes in the additional folders + $changesMem->Config(ZPush::GetAdditionalSyncFolders()); + + // process incoming changes + if(self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_CHANGES)) { + // Ignore if present + if(self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_COUNT)) { + self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false; + } + + // Process the changes (either , , or ) + $element = self::$decoder->getElement(); + + if($element[EN_TYPE] != EN_TYPE_STARTTAG) + return false; + + $importer = false; + while(1) { + $folder = new SyncFolder(); + if(!$folder->Decode(self::$decoder)) + break; + + try { + if ($status == SYNC_FSSTATUS_SUCCESS && !$importer) { + // Configure the backends importer with last state + $importer = self::$backend->GetImporter(); + $importer->Config($syncstate); + // the messages from the PIM will be forwarded to the backend + $changesMem->forwardImporter($importer); + } + + if ($status == SYNC_FSSTATUS_SUCCESS) { + switch($element[EN_TAG]) { + case SYNC_ADD: + case SYNC_MODIFY: + $serverid = $changesMem->ImportFolderChange($folder); + break; + case SYNC_REMOVE: + $serverid = $changesMem->ImportFolderDeletion($folder); + break; + } + + // TODO what does $map?? + if($serverid) + $map[$serverid] = $folder->clientid; + } + else { + ZLog::Write(LOGLEVEL_WARN, sprintf("Request->HandleFolderSync(): ignoring incoming folderchange for folder '%s' as status indicates problem.", $folder->displayname)); + self::$topCollector->AnnounceInformation("Incoming change ignored", true); + } + } + catch (StatusException $stex) { + $status = $stex->getCode(); + } + } + + if(!self::$decoder->getElementEndTag()) + return false; + } + // no incoming changes + else { + // check for a potential process loop like described in Issue ZP-5 + if ($synckey != "0" && self::$deviceManager->IsHierarchyFullResyncRequired()) + $status = SYNC_FSSTATUS_SYNCKEYERROR; + self::$deviceManager->AnnounceProcessStatus(false, $status); + } + + if(!self::$decoder->getElementEndTag()) + return false; + + // We have processed incoming foldersync requests, now send the PIM + // our changes + + // Output our WBXML reply now + self::$encoder->StartWBXML(); + + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC); + { + if ($status == SYNC_FSSTATUS_SUCCESS) { + try { + // do nothing if this is an invalid device id (like the 'validate' Androids internal client sends) + if (!Request::IsValidDeviceID()) + throw new StatusException(sprintf("Request::IsValidDeviceID() indicated that '%s' is not a valid device id", Request::GetDeviceID()), SYNC_FSSTATUS_SERVERERROR); + + // Changes from backend are sent to the MemImporter and processed for the HierarchyCache. + // The state which is saved is from the backend, as the MemImporter is only a proxy. + $exporter = self::$backend->GetExporter(); + + $exporter->Config($syncstate); + $exporter->InitializeExporter($changesMem); + + // Stream all changes to the ImportExportChangesMem + while(is_array($exporter->Synchronize())); + + // get the new state from the backend + $newsyncstate = (isset($exporter))?$exporter->GetState():""; + } + catch (StatusException $stex) { + if ($stex->getCode() == SYNC_FSSTATUS_CODEUNKNOWN) + $status = SYNC_FSSTATUS_SYNCKEYERROR; + else + $status = $stex->getCode(); + } + } + + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); + + if ($status == SYNC_FSSTATUS_SUCCESS) { + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY); + $synckey = ($changesMem->IsStateChanged()) ? $newsynckey : $synckey; + self::$encoder->content($synckey); + self::$encoder->endTag(); + + // Stream folders directly to the PDA + $streamimporter = new ImportChangesStream(self::$encoder, false); + $changesMem->InitializeExporter($streamimporter); + $changeCount = $changesMem->GetChangeCount(); + + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_CHANGES); + { + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_COUNT); + self::$encoder->content($changeCount); + self::$encoder->endTag(); + while($changesMem->Synchronize()); + } + self::$encoder->endTag(); + self::$topCollector->AnnounceInformation(sprintf("Outgoing %d folders",$changeCount), true); + + // everything fine, save the sync state for the next time + if ($synckey == $newsynckey) + self::$deviceManager->GetStateManager()->SetSyncState($newsynckey, $newsyncstate); + } + } + self::$encoder->endTag(); + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/getattachment.php b/sources/lib/request/getattachment.php new file mode 100644 index 0000000..912eb69 --- /dev/null +++ b/sources/lib/request/getattachment.php @@ -0,0 +1,91 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class GetAttachment extends RequestProcessor { + + /** + * Handles the GetAttachment command + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + $attname = Request::GetGETAttachmentName(); + if(!$attname) + return false; + + try { + $attachment = self::$backend->GetAttachmentData($attname); + $stream = $attachment->data; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleGetAttachment(): attachment stream from backend: %s", $stream)); + + if ($stream == null) + throw new StatusException(sprintf("HandleGetAttachment(): No stream resource returned by backend for attachment: %s", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + + header("Content-Type: application/octet-stream"); + $l = 0; + while (!feof($stream)) { + $d = fgets($stream, 4096); + $l += strlen($d); + echo $d; + + // announce an update every 100K + if (($l/1024) % 100 == 0) + self::$topCollector->AnnounceInformation(sprintf("Streaming attachment: %d KB sent", round($l/1024))); + } + fclose($stream); + self::$topCollector->AnnounceInformation(sprintf("Streamed %d KB attachment", $l/1024), true); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleGetAttachment(): attachment with %d KB sent to mobile", $l/1024)); + + } + catch (StatusException $s) { + // StatusException already logged so we just need to pass it upwards to send a HTTP error + throw new HTTPReturnCodeException($s->getMessage(), HTTP_CODE_500, null, LOGLEVEL_DEBUG); + } + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/gethierarchy.php b/sources/lib/request/gethierarchy.php new file mode 100644 index 0000000..152062d --- /dev/null +++ b/sources/lib/request/gethierarchy.php @@ -0,0 +1,81 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class GetHierarchy extends RequestProcessor { + + /** + * Handles the GetHierarchy command + * simply returns current hierarchy of all folders + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + try { + $folders = self::$backend->GetHierarchy(); + if (!$folders || empty($folders)) + throw new StatusException("GetHierarchy() did not return any data."); + + // TODO execute $data->Check() to see if SyncObject is valid + + } + catch (StatusException $ex) { + return false; + } + + self::$encoder->StartWBXML(); + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERS); + foreach ($folders as $folder) { + self::$encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDER); + $folder->Encode(self::$encoder); + self::$encoder->endTag(); + } + self::$encoder->endTag(); + + // save hierarchy for upcoming syncing + return self::$deviceManager->InitializeFolderCache($folders); + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/getitemestimate.php b/sources/lib/request/getitemestimate.php new file mode 100644 index 0000000..6f00587 --- /dev/null +++ b/sources/lib/request/getitemestimate.php @@ -0,0 +1,287 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class GetItemEstimate extends RequestProcessor { + + /** + * Handles the GetItemEstimate command + * Returns an estimation of how many items will be synchronized at the next sync + * This is mostly used to show something in the progress bar + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + $sc = new SyncCollections(); + + if(!self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_GETITEMESTIMATE)) + return false; + + if(!self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERS)) + return false; + + while(self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDER)) { + $spa = new SyncParameters(); + $spastatus = false; + + // read the folder properties + while (1) { + if(self::$decoder->getElementStartTag(SYNC_SYNCKEY)) { + try { + $spa->SetSyncKey(self::$decoder->getElementContent()); + } + catch (StateInvalidException $siex) { + $spastatus = SYNC_GETITEMESTSTATUS_SYNCSTATENOTPRIMED; + } + + if(!self::$decoder->getElementEndTag()) + return false; + } + + elseif(self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERID)) { + $spa->SetFolderId( self::$decoder->getElementContent()); + + if(!self::$decoder->getElementEndTag()) + return false; + } + + // conversation mode requested + elseif(self::$decoder->getElementStartTag(SYNC_CONVERSATIONMODE)) { + $spa->SetConversationMode(true); + if(($conversationmode = self::$decoder->getElementContent()) !== false) { + $spa->SetConversationMode((boolean)$conversationmode); + if(!self::$decoder->getElementEndTag()) + return false; + } + } + + // get items estimate does not necessarily send the folder type + elseif(self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERTYPE)) { + $spa->SetContentClass(self::$decoder->getElementContent()); + + if(!self::$decoder->getElementEndTag()) + return false; + } + + //TODO AS 2.5 and filtertype not set + elseif(self::$decoder->getElementStartTag(SYNC_FILTERTYPE)) { + $spa->SetFilterType(self::$decoder->getElementContent()); + + if(!self::$decoder->getElementEndTag()) + return false; + } + + while(self::$decoder->getElementStartTag(SYNC_OPTIONS)) { + while(1) { + $firstOption = true; + // foldertype definition + if(self::$decoder->getElementStartTag(SYNC_FOLDERTYPE)) { + $foldertype = self::$decoder->getElementContent(); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleGetItemEstimate(): specified options block with foldertype '%s'", $foldertype)); + + // switch the foldertype for the next options + $spa->UseCPO($foldertype); + + // set to synchronize all changes. The mobile could overwrite this value + $spa->SetFilterType(SYNC_FILTERTYPE_ALL); + + if(!self::$decoder->getElementEndTag()) + return false; + } + // if no foldertype is defined, use default cpo + else if ($firstOption){ + $spa->UseCPO(); + // set to synchronize all changes. The mobile could overwrite this value + $spa->SetFilterType(SYNC_FILTERTYPE_ALL); + } + $firstOption = false; + + if(self::$decoder->getElementStartTag(SYNC_FILTERTYPE)) { + $spa->SetFilterType(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_MAXITEMS)) { + $spa->SetWindowSize($maxitems = self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + $e = self::$decoder->peek(); + if($e[EN_TYPE] == EN_TYPE_ENDTAG) { + self::$decoder->getElementEndTag(); + break; + } + } + } + + $e = self::$decoder->peek(); + if($e[EN_TYPE] == EN_TYPE_ENDTAG) { + self::$decoder->getElementEndTag(); //SYNC_GETITEMESTIMATE_FOLDER + break; + } + } + // Process folder data + + //In AS 14 request only collectionid is sent, without class + if (! $spa->HasContentClass() && $spa->HasFolderId()) { + try { + $spa->SetContentClass(self::$deviceManager->GetFolderClassFromCacheByID($spa->GetFolderId())); + } + catch (NoHierarchyCacheAvailableException $nhca) { + $spastatus = SYNC_GETITEMESTSTATUS_COLLECTIONINVALID; + } + } + + // compatibility mode AS 1.0 - get folderid which was sent during GetHierarchy() + if (! $spa->HasFolderId() && $spa->HasContentClass()) { + $spa->SetFolderId(self::$deviceManager->GetFolderIdFromCacheByClass($spa->GetContentClass())); + } + + // Add collection to SC and load state + $sc->AddCollection($spa); + if ($spastatus) { + // the CPO has a folder id now, so we can set the status + $sc->AddParameter($spa, "status", $spastatus); + } + else { + try { + $sc->AddParameter($spa, "state", self::$deviceManager->GetStateManager()->GetSyncState($spa->GetSyncKey())); + + // if this is an additional folder the backend has to be setup correctly + if (!self::$backend->Setup(ZPush::GetAdditionalSyncFolderStore($spa->GetFolderId()))) + throw new StatusException(sprintf("HandleGetItemEstimate() could not Setup() the backend for folder id '%s'", $spa->GetFolderId()), SYNC_GETITEMESTSTATUS_COLLECTIONINVALID); + } + catch (StateNotFoundException $snfex) { + // ok, the key is invalid. Question is, if the hierarchycache is still ok + //if not, we have to issue SYNC_GETITEMESTSTATUS_COLLECTIONINVALID which triggers a FolderSync + try { + self::$deviceManager->GetFolderClassFromCacheByID($spa->GetFolderId()); + // we got here, so the HierarchyCache is ok + $sc->AddParameter($spa, "status", SYNC_GETITEMESTSTATUS_SYNCKKEYINVALID); + } + catch (NoHierarchyCacheAvailableException $nhca) { + $sc->AddParameter($spa, "status", SYNC_GETITEMESTSTATUS_COLLECTIONINVALID); + } + + self::$topCollector->AnnounceInformation("StateNotFoundException ". $sc->GetParameter($spa, "status"), true); + } + catch (StatusException $stex) { + if ($stex->getCode() == SYNC_GETITEMESTSTATUS_COLLECTIONINVALID) + $sc->AddParameter($spa, "status", SYNC_GETITEMESTSTATUS_COLLECTIONINVALID); + else + $sc->AddParameter($spa, "status", SYNC_GETITEMESTSTATUS_SYNCSTATENOTPRIMED); + self::$topCollector->AnnounceInformation("StatusException ". $sc->GetParameter($spa, "status"), true); + } + } + + } + if(!self::$decoder->getElementEndTag()) + return false; //SYNC_GETITEMESTIMATE_FOLDERS + + if(!self::$decoder->getElementEndTag()) + return false; //SYNC_GETITEMESTIMATE_GETITEMESTIMATE + + self::$encoder->startWBXML(); + self::$encoder->startTag(SYNC_GETITEMESTIMATE_GETITEMESTIMATE); + { + $status = SYNC_GETITEMESTSTATUS_SUCCESS; + // look for changes in all collections + + try { + $sc->CountChanges(); + } + catch (StatusException $ste) { + $status = SYNC_GETITEMESTSTATUS_COLLECTIONINVALID; + } + $changes = $sc->GetChangedFolderIds(); + + foreach($sc as $folderid => $spa) { + self::$encoder->startTag(SYNC_GETITEMESTIMATE_RESPONSE); + { + if ($sc->GetParameter($spa, "status")) + $status = $sc->GetParameter($spa, "status"); + + self::$encoder->startTag(SYNC_GETITEMESTIMATE_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_GETITEMESTIMATE_FOLDER); + { + self::$encoder->startTag(SYNC_GETITEMESTIMATE_FOLDERTYPE); + self::$encoder->content($spa->GetContentClass()); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_GETITEMESTIMATE_FOLDERID); + self::$encoder->content($spa->GetFolderId()); + self::$encoder->endTag(); + + if (isset($changes[$folderid]) && $changes[$folderid] !== false) { + self::$encoder->startTag(SYNC_GETITEMESTIMATE_ESTIMATE); + self::$encoder->content($changes[$folderid]); + self::$encoder->endTag(); + + if ($changes[$folderid] > 0) + self::$topCollector->AnnounceInformation(sprintf("%s %d changes", $spa->GetContentClass(), $changes[$folderid]), true); + + // update the device data to mark folders as complete when synching with WM + if ($changes[$folderid] == 0) + self::$deviceManager->SetFolderSyncStatus($folderid, DeviceManager::FLD_SYNC_COMPLETED); + } + } + self::$encoder->endTag(); + } + self::$encoder->endTag(); + } + if (array_sum($changes) == 0) + self::$topCollector->AnnounceInformation("No changes found", true); + } + self::$encoder->endTag(); + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/itemoperations.php b/sources/lib/request/itemoperations.php new file mode 100644 index 0000000..d6dcd01 --- /dev/null +++ b/sources/lib/request/itemoperations.php @@ -0,0 +1,390 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class ItemOperations extends RequestProcessor { + + /** + * Handles the ItemOperations command + * Provides batched online handling for Fetch, EmptyFolderContents and Move + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + // Parse input + if(!self::$decoder->getElementStartTag(SYNC_ITEMOPERATIONS_ITEMOPERATIONS)) + return false; + + $itemoperations = array(); + //ItemOperations can either be Fetch, EmptyFolderContents or Move + while (1) { + //TODO check if multiple item operations are possible in one request + $el = self::$decoder->getElement(); + + if($el[EN_TYPE] != EN_TYPE_STARTTAG) + return false; + + $fetch = $efc = $move = false; + $operation = array(); + if($el[EN_TAG] == SYNC_ITEMOPERATIONS_FETCH) { + $fetch = true; + $operation['operation'] = SYNC_ITEMOPERATIONS_FETCH; + self::$topCollector->AnnounceInformation("Fetch", true); + } + else if($el[EN_TAG] == SYNC_ITEMOPERATIONS_EMPTYFOLDERCONTENTS) { + $efc = true; + $operation['operation'] = SYNC_ITEMOPERATIONS_EMPTYFOLDERCONTENTS; + self::$topCollector->AnnounceInformation("Empty Folder", true); + } + else if($el[EN_TAG] == SYNC_ITEMOPERATIONS_MOVE) { + $move = true; + $operation['operation'] = SYNC_ITEMOPERATIONS_MOVE; + self::$topCollector->AnnounceInformation("Move", true); + } + + if(!$fetch && !$efc && !$move) { + ZLog::Write(LOGLEVEL_DEBUG, "Unknown item operation:".print_r($el, 1)); + self::$topCollector->AnnounceInformation("Unknown operation", true); + 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 + + 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_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_ITEMOPERATIONS_OPTIONS)) { + //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; + } + + 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; + } + } + + 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; + } + } + } + + //break if it reached the endtag + $e = self::$decoder->peek(); + if($e[EN_TYPE] == EN_TYPE_ENDTAG) { + self::$decoder->getElementEndTag(); + break; + } + } + } + } + + 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 + + if(!self::$decoder->getElementEndTag()) + return false; //SYNC_ITEMOPERATIONS_FETCH or SYNC_ITEMOPERATIONS_EMPTYFOLDERCONTENTS or SYNC_ITEMOPERATIONS_MOVE + + $itemoperations[] = $operation; + //break if it reached the endtag + $e = self::$decoder->peek(); + if($e[EN_TYPE] == EN_TYPE_ENDTAG) { + self::$decoder->getElementEndTag(); //SYNC_ITEMOPERATIONS_ITEMOPERATIONS + break; + } + + } + +// if(!self::$decoder->getElementEndTag()) +// return false;//SYNC_ITEMOPERATIONS_ITEMOPERATIONS + + $status = SYNC_ITEMOPERATIONSSTATUS_SUCCESS; + + self::$encoder->startWBXML(); + + self::$encoder->startTag(SYNC_ITEMOPERATIONS_ITEMOPERATIONS); + + self::$encoder->startTag(SYNC_ITEMOPERATIONS_STATUS); + self::$encoder->content($status); + self::$encoder->endTag();//SYNC_ITEMOPERATIONS_STATUS + + // Stop here if something went wrong + if ($status != SYNC_ITEMOPERATIONSSTATUS_SUCCESS) { + self::$encoder->endTag();//SYNC_ITEMOPERATIONS_ITEMOPERATIONS + return true; + } + + self::$encoder->startTag(SYNC_ITEMOPERATIONS_RESPONSE); + + foreach ($itemoperations as $operation) { + // fetch response + if ($operation['operation'] == SYNC_ITEMOPERATIONS_FETCH) { + + $status = SYNC_ITEMOPERATIONSSTATUS_SUCCESS; + + // retrieve the data + // Fetch throws Sync status codes, - GetAttachmentData ItemOperations codes + if (isset($operation['filereference'])) { + try { + self::$topCollector->AnnounceInformation("Get attachment data from backend with file reference"); + $data = self::$backend->GetAttachmentData($operation['filereference']); + } + catch (StatusException $stex) { + $status = $stex->getCode(); + } + + } + else { + try { + if (isset($operation['folderid']) && isset($operation['serverid'])) { + self::$topCollector->AnnounceInformation("Fetching data from backend with item and folder id"); + $data = self::$backend->Fetch($operation['folderid'], $operation['serverid'], $operation["cpo"]); + } + else if (isset($operation['longid'])) { + self::$topCollector->AnnounceInformation("Fetching data from backend with long id"); + $tmp = explode(":", $operation['longid']); + $data = self::$backend->Fetch($tmp[0], $tmp[1], $operation["cpo"]); + } + } + catch (StatusException $stex) { + // the only option to return is that we could not retrieve it + $status = SYNC_ITEMOPERATIONSSTATUS_CONVERSIONFAILED; + } + } + + self::$encoder->startTag(SYNC_ITEMOPERATIONS_FETCH); + + self::$encoder->startTag(SYNC_ITEMOPERATIONS_STATUS); + self::$encoder->content($status); + self::$encoder->endTag();//SYNC_ITEMOPERATIONS_STATUS + + if (isset($operation['folderid']) && isset($operation['serverid'])) { + self::$encoder->startTag(SYNC_FOLDERID); + self::$encoder->content($operation['folderid']); + self::$encoder->endTag(); // end SYNC_FOLDERID + + self::$encoder->startTag(SYNC_SERVERENTRYID); + self::$encoder->content($operation['serverid']); + self::$encoder->endTag(); // end SYNC_SERVERENTRYID + + self::$encoder->startTag(SYNC_FOLDERTYPE); + self::$encoder->content("Email"); + self::$encoder->endTag(); + } + + if (isset($operation['longid'])) { + self::$encoder->startTag(SYNC_SEARCH_LONGID); + self::$encoder->content($operation['longid']); + self::$encoder->endTag(); // end SYNC_FOLDERID + + self::$encoder->startTag(SYNC_FOLDERTYPE); + self::$encoder->content("Email"); + self::$encoder->endTag(); + } + + if (isset($operation['filereference'])) { + self::$encoder->startTag(SYNC_AIRSYNCBASE_FILEREFERENCE); + self::$encoder->content($operation['filereference']); + self::$encoder->endTag(); // end SYNC_AIRSYNCBASE_FILEREFERENCE + } + + if (isset($data)) { + self::$topCollector->AnnounceInformation("Streaming data"); + + self::$encoder->startTag(SYNC_ITEMOPERATIONS_PROPERTIES); + $data->Encode(self::$encoder); + self::$encoder->endTag(); //SYNC_ITEMOPERATIONS_PROPERTIES + } + + self::$encoder->endTag();//SYNC_ITEMOPERATIONS_FETCH + } + // empty folder contents operation + else if ($operation['operation'] == SYNC_ITEMOPERATIONS_EMPTYFOLDERCONTENTS) { + try { + self::$topCollector->AnnounceInformation("Emptying folder"); + + // send request to backend + self::$backend->EmptyFolder($operation['folderid'], $operation['deletesubfolders']); + } + catch (StatusException $stex) { + $status = $stex->getCode(); + } + + self::$encoder->startTag(SYNC_ITEMOPERATIONS_EMPTYFOLDERCONTENTS); + + self::$encoder->startTag(SYNC_ITEMOPERATIONS_STATUS); + self::$encoder->content($status); + self::$encoder->endTag();//SYNC_ITEMOPERATIONS_STATUS + + if (isset($operation['folderid'])) { + self::$encoder->startTag(SYNC_FOLDERID); + self::$encoder->content($operation['folderid']); + self::$encoder->endTag(); // end SYNC_FOLDERID + } + self::$encoder->endTag();//SYNC_ITEMOPERATIONS_EMPTYFOLDERCONTENTS + } + // TODO implement ItemOperations Move + // move operation + else { + self::$topCollector->AnnounceInformation("not implemented", true); + + // reply with "can't do" + self::$encoder->startTag(SYNC_ITEMOPERATIONS_MOVE); + self::$encoder->startTag(SYNC_ITEMOPERATIONS_STATUS); + self::$encoder->content(SYNC_ITEMOPERATIONSSTATUS_SERVERERROR); + self::$encoder->endTag();//SYNC_ITEMOPERATIONS_STATUS + self::$encoder->endTag();//SYNC_ITEMOPERATIONS_MOVE + } + + } + self::$encoder->endTag();//SYNC_ITEMOPERATIONS_RESPONSE + self::$encoder->endTag();//SYNC_ITEMOPERATIONS_ITEMOPERATIONS + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/meetingresponse.php b/sources/lib/request/meetingresponse.php new file mode 100644 index 0000000..64115a8 --- /dev/null +++ b/sources/lib/request/meetingresponse.php @@ -0,0 +1,132 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class MeetingResponse extends RequestProcessor { + + /** + * Handles the MeetingResponse command + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + $requests = Array(); + + if(!self::$decoder->getElementStartTag(SYNC_MEETINGRESPONSE_MEETINGRESPONSE)) + return false; + + while(self::$decoder->getElementStartTag(SYNC_MEETINGRESPONSE_REQUEST)) { + $req = Array(); + while(1) { + if(self::$decoder->getElementStartTag(SYNC_MEETINGRESPONSE_USERRESPONSE)) { + $req["response"] = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_MEETINGRESPONSE_FOLDERID)) { + $req["folderid"] = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_MEETINGRESPONSE_REQUESTID)) { + $req["requestid"] = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + return false; + } + + $e = self::$decoder->peek(); + if($e[EN_TYPE] == EN_TYPE_ENDTAG) { + self::$decoder->getElementEndTag(); + break; + } + } + array_push($requests, $req); + } + + if(!self::$decoder->getElementEndTag()) + return false; + + // output the error code, plus the ID of the calendar item that was generated by the + // accept of the meeting response + self::$encoder->StartWBXML(); + self::$encoder->startTag(SYNC_MEETINGRESPONSE_MEETINGRESPONSE); + + foreach($requests as $req) { + $status = SYNC_MEETRESPSTATUS_SUCCESS; + + try { + $calendarid = self::$backend->MeetingResponse($req["requestid"], $req["folderid"], $req["response"]); + if ($calendarid === false) + throw new StatusException("HandleMeetingResponse() not possible", SYNC_MEETRESPSTATUS_SERVERERROR); + } + catch (StatusException $stex) { + $status = $stex->getCode(); + } + + self::$encoder->startTag(SYNC_MEETINGRESPONSE_RESULT); + self::$encoder->startTag(SYNC_MEETINGRESPONSE_REQUESTID); + self::$encoder->content($req["requestid"]); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_MEETINGRESPONSE_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); + + if($status == SYNC_MEETRESPSTATUS_SUCCESS && !empty($calendarid)) { + self::$encoder->startTag(SYNC_MEETINGRESPONSE_CALENDARID); + self::$encoder->content($calendarid); + self::$encoder->endTag(); + } + self::$encoder->endTag(); + self::$topCollector->AnnounceInformation(sprintf("Operation status %d", $status), true); + } + self::$encoder->endTag(); + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/moveitems.php b/sources/lib/request/moveitems.php new file mode 100644 index 0000000..7fe5691 --- /dev/null +++ b/sources/lib/request/moveitems.php @@ -0,0 +1,138 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class MoveItems extends RequestProcessor { + + /** + * Handles the MoveItems command + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + if(!self::$decoder->getElementStartTag(SYNC_MOVE_MOVES)) + return false; + + $moves = array(); + while(self::$decoder->getElementStartTag(SYNC_MOVE_MOVE)) { + $move = array(); + if(self::$decoder->getElementStartTag(SYNC_MOVE_SRCMSGID)) { + $move["srcmsgid"] = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + break; + } + if(self::$decoder->getElementStartTag(SYNC_MOVE_SRCFLDID)) { + $move["srcfldid"] = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + break; + } + if(self::$decoder->getElementStartTag(SYNC_MOVE_DSTFLDID)) { + $move["dstfldid"] = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) + break; + } + array_push($moves, $move); + + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(!self::$decoder->getElementEndTag()) + return false; + + self::$encoder->StartWBXML(); + + self::$encoder->startTag(SYNC_MOVE_MOVES); + + foreach($moves as $move) { + self::$encoder->startTag(SYNC_MOVE_RESPONSE); + self::$encoder->startTag(SYNC_MOVE_SRCMSGID); + self::$encoder->content($move["srcmsgid"]); + self::$encoder->endTag(); + + $status = SYNC_MOVEITEMSSTATUS_SUCCESS; + $result = false; + try { + // if the source folder is an additional folder the backend has to be setup correctly + if (!self::$backend->Setup(ZPush::GetAdditionalSyncFolderStore($move["srcfldid"]))) + throw new StatusException(sprintf("HandleMoveItems() could not Setup() the backend for folder id '%s'", $move["srcfldid"]), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); + + $importer = self::$backend->GetImporter($move["srcfldid"]); + if ($importer === false) + throw new StatusException(sprintf("HandleMoveItems() could not get an importer for folder id '%s'", $move["srcfldid"]), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); + + // get saved SyncParameters for this folder + $spa = self::$deviceManager->GetStateManager()->GetSynchedFolderState($move["srcfldid"]); + if (!$spa->HasSyncKey()) + throw new StatusException(sprintf("MoveItems(): Source folder id '%s' is not fully synchronized. Unable to perform operation.", $move["srcfldid"]), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); + $importer->ConfigContentParameters($spa->GetCPO()); + + $result = $importer->ImportMessageMove($move["srcmsgid"], $move["dstfldid"]); + // We discard the importer state for now. + } + catch (StatusException $stex) { + if ($stex->getCode() == SYNC_STATUS_FOLDERHIERARCHYCHANGED) // same as SYNC_FSSTATUS_CODEUNKNOWN + $status = SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID; + else + $status = $stex->getCode(); + } + + self::$topCollector->AnnounceInformation(sprintf("Operation status: %s", $status), true); + + self::$encoder->startTag(SYNC_MOVE_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_MOVE_DSTMSGID); + self::$encoder->content( (($result !== false ) ? $result : $move["srcmsgid"])); + self::$encoder->endTag(); + self::$encoder->endTag(); + } + + self::$encoder->endTag(); + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/notify.php b/sources/lib/request/notify.php new file mode 100644 index 0000000..66a8058 --- /dev/null +++ b/sources/lib/request/notify.php @@ -0,0 +1,83 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class Notify extends RequestProcessor { + + /** + * Handles the Notify command + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + if(!self::$decoder->getElementStartTag(SYNC_AIRNOTIFY_NOTIFY)) + return false; + + if(!self::$decoder->getElementStartTag(SYNC_AIRNOTIFY_DEVICEINFO)) + return false; + + if(!self::$decoder->getElementEndTag()) + return false; + + if(!self::$decoder->getElementEndTag()) + return false; + + self::$encoder->StartWBXML(); + + self::$encoder->startTag(SYNC_AIRNOTIFY_NOTIFY); + { + self::$encoder->startTag(SYNC_AIRNOTIFY_STATUS); + self::$encoder->content(1); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_AIRNOTIFY_VALIDCARRIERPROFILES); + self::$encoder->endTag(); + } + self::$encoder->endTag(); + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/ping.php b/sources/lib/request/ping.php new file mode 100644 index 0000000..5cfacff --- /dev/null +++ b/sources/lib/request/ping.php @@ -0,0 +1,215 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class Ping extends RequestProcessor { + + /** + * Handles the Ping command + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + $interval = (defined('PING_INTERVAL') && PING_INTERVAL > 0) ? PING_INTERVAL : 30; + $pingstatus = false; + $fakechanges = array(); + $foundchanges = false; + + // Contains all requested folders (containers) + $sc = new SyncCollections(); + + // Load all collections - do load states and check permissions + try { + $sc->LoadAllCollections(true, true, true); + } + catch (StateNotFoundException $snfex) { + $pingstatus = SYNC_PINGSTATUS_FOLDERHIERSYNCREQUIRED; + self::$topCollector->AnnounceInformation("StateNotFoundException: require HierarchySync", true); + } + catch (StateInvalidException $snfex) { + // we do not have a ping status for this, but SyncCollections should have generated fake changes for the folders which are broken + $fakechanges = $sc->GetChangedFolderIds(); + $foundchanges = true; + + self::$topCollector->AnnounceInformation("StateInvalidException: force sync", true); + } + catch (StatusException $stex) { + $pingstatus = SYNC_PINGSTATUS_FOLDERHIERSYNCREQUIRED; + self::$topCollector->AnnounceInformation("StatusException: require HierarchySync", true); + } + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandlePing(): reference PolicyKey for PING: %s", $sc->GetReferencePolicyKey())); + + // receive PING initialization data + if(self::$decoder->getElementStartTag(SYNC_PING_PING)) { + self::$topCollector->AnnounceInformation("Processing PING data"); + ZLog::Write(LOGLEVEL_DEBUG, "HandlePing(): initialization data received"); + + if(self::$decoder->getElementStartTag(SYNC_PING_LIFETIME)) { + $sc->SetLifetime(self::$decoder->getElementContent()); + self::$decoder->getElementEndTag(); + } + + if(($el = self::$decoder->getElementStartTag(SYNC_PING_FOLDERS)) && $el[EN_FLAGS] & EN_FLAGS_CONTENT) { + // remove PingableFlag from all collections + foreach ($sc as $folderid => $spa) + $spa->DelPingableFlag(); + + while(self::$decoder->getElementStartTag(SYNC_PING_FOLDER)) { + while(1) { + if(self::$decoder->getElementStartTag(SYNC_PING_SERVERENTRYID)) { + $folderid = self::$decoder->getElementContent(); + self::$decoder->getElementEndTag(); + } + if(self::$decoder->getElementStartTag(SYNC_PING_FOLDERTYPE)) { + $class = self::$decoder->getElementContent(); + self::$decoder->getElementEndTag(); + } + + $e = self::$decoder->peek(); + if($e[EN_TYPE] == EN_TYPE_ENDTAG) { + self::$decoder->getElementEndTag(); + break; + } + } + + $spa = $sc->GetCollection($folderid); + if (! $spa) { + // The requested collection is not synchronized. + // check if the HierarchyCache is available, if not, trigger a HierarchySync + try { + self::$deviceManager->GetFolderClassFromCacheByID($folderid); + } + catch (NoHierarchyCacheAvailableException $nhca) { + ZLog::Write(LOGLEVEL_INFO, sprintf("HandlePing(): unknown collection '%s', triggering HierarchySync", $folderid)); + $pingstatus = SYNC_PINGSTATUS_FOLDERHIERSYNCREQUIRED; + } + + // Trigger a Sync request because then the device will be forced to resync this folder. + $fakechanges[$folderid] = 1; + $foundchanges = true; + } + else if ($class == $spa->GetContentClass()) { + $spa->SetPingableFlag(true); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandlePing(): using saved sync state for '%s' id '%s'", $spa->GetContentClass(), $folderid)); + } + + } + if(!self::$decoder->getElementEndTag()) + return false; + } + if(!self::$decoder->getElementEndTag()) + return false; + + // save changed data + foreach ($sc as $folderid => $spa) + $sc->SaveCollection($spa); + } // END SYNC_PING_PING + else { + // if no ping initialization data was sent, we check if we have pingable folders + // if not, we indicate that there is nothing to do. + if (! $sc->PingableFolders()) { + $pingstatus = SYNC_PINGSTATUS_FAILINGPARAMS; + ZLog::Write(LOGLEVEL_DEBUG, "HandlePing(): no pingable folders found and no initialization data sent. Returning SYNC_PINGSTATUS_FAILINGPARAMS."); + } + } + + // Check for changes on the default LifeTime, set interval and ONLY on pingable collections + try { + if (!$pingstatus && empty($fakechanges)) { + $foundchanges = $sc->CheckForChanges($sc->GetLifetime(), $interval, true); + } + } + catch (StatusException $ste) { + switch($ste->getCode()) { + case SyncCollections::ERROR_NO_COLLECTIONS: + $pingstatus = SYNC_PINGSTATUS_FAILINGPARAMS; + break; + case SyncCollections::ERROR_WRONG_HIERARCHY: + $pingstatus = SYNC_PINGSTATUS_FOLDERHIERSYNCREQUIRED; + self::$deviceManager->AnnounceProcessStatus(false, $pingstatus); + break; + case SyncCollections::OBSOLETE_CONNECTION: + $foundchanges = false; + break; + } + } + + self::$encoder->StartWBXML(); + self::$encoder->startTag(SYNC_PING_PING); + { + self::$encoder->startTag(SYNC_PING_STATUS); + if (isset($pingstatus) && $pingstatus) + self::$encoder->content($pingstatus); + else + self::$encoder->content($foundchanges ? SYNC_PINGSTATUS_CHANGES : SYNC_PINGSTATUS_HBEXPIRED); + self::$encoder->endTag(); + + if (! $pingstatus) { + self::$encoder->startTag(SYNC_PING_FOLDERS); + + if (empty($fakechanges)) + $changes = $sc->GetChangedFolderIds(); + else + $changes = $fakechanges; + + foreach ($changes as $folderid => $changecount) { + if ($changecount > 0) { + self::$encoder->startTag(SYNC_PING_FOLDER); + self::$encoder->content($folderid); + self::$encoder->endTag(); + if (empty($fakechanges)) + self::$topCollector->AnnounceInformation(sprintf("Found change in %s", $sc->GetCollection($folderid)->GetContentClass()), true); + } + } + self::$encoder->endTag(); + } + } + self::$encoder->endTag(); + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/provisioning.php b/sources/lib/request/provisioning.php new file mode 100644 index 0000000..fe2df6e --- /dev/null +++ b/sources/lib/request/provisioning.php @@ -0,0 +1,230 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class Provisioning extends RequestProcessor { + + /** + * Handles the Provisioning command + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + $status = SYNC_PROVISION_STATUS_SUCCESS; + $policystatus = SYNC_PROVISION_POLICYSTATUS_SUCCESS; + + $rwstatus = self::$deviceManager->GetProvisioningWipeStatus(); + $rwstatusWiped = false; + + // if this is a regular provisioning require that an authenticated remote user + if ($rwstatus < SYNC_PROVISION_RWSTATUS_PENDING) { + ZLog::Write(LOGLEVEL_DEBUG, "RequestProcessor::HandleProvision(): Forcing delayed Authentication"); + self::Authenticate(); + } + + $phase2 = true; + + if(!self::$decoder->getElementStartTag(SYNC_PROVISION_PROVISION)) + return false; + + //handle android remote wipe. + if (self::$decoder->getElementStartTag(SYNC_PROVISION_REMOTEWIPE)) { + if(!self::$decoder->getElementStartTag(SYNC_PROVISION_STATUS)) + return false; + + $instatus = self::$decoder->getElementContent(); + + if(!self::$decoder->getElementEndTag()) + return false; + + if(!self::$decoder->getElementEndTag()) + return false; + + $phase2 = false; + $rwstatusWiped = true; + } + else { + + if(!self::$decoder->getElementStartTag(SYNC_PROVISION_POLICIES)) + return false; + + if(!self::$decoder->getElementStartTag(SYNC_PROVISION_POLICY)) + return false; + + if(!self::$decoder->getElementStartTag(SYNC_PROVISION_POLICYTYPE)) + return false; + + $policytype = self::$decoder->getElementContent(); + if ($policytype != 'MS-WAP-Provisioning-XML' && $policytype != 'MS-EAS-Provisioning-WBXML') { + $status = SYNC_PROVISION_STATUS_SERVERERROR; + } + if(!self::$decoder->getElementEndTag()) //policytype + return false; + + if (self::$decoder->getElementStartTag(SYNC_PROVISION_POLICYKEY)) { + $devpolicykey = self::$decoder->getElementContent(); + + if(!self::$decoder->getElementEndTag()) + return false; + + if(!self::$decoder->getElementStartTag(SYNC_PROVISION_STATUS)) + return false; + + $instatus = self::$decoder->getElementContent(); + + if(!self::$decoder->getElementEndTag()) + return false; + + $phase2 = false; + } + + if(!self::$decoder->getElementEndTag()) //policy + return false; + + if(!self::$decoder->getElementEndTag()) //policies + return false; + + if (self::$decoder->getElementStartTag(SYNC_PROVISION_REMOTEWIPE)) { + if(!self::$decoder->getElementStartTag(SYNC_PROVISION_STATUS)) + return false; + + $status = self::$decoder->getElementContent(); + + if(!self::$decoder->getElementEndTag()) + return false; + + if(!self::$decoder->getElementEndTag()) + return false; + + $rwstatusWiped = true; + } + } + if(!self::$decoder->getElementEndTag()) //provision + return false; + + if (PROVISIONING !== true) { + ZLog::Write(LOGLEVEL_INFO, "No policies deployed to device"); + $policystatus = SYNC_PROVISION_POLICYSTATUS_NOPOLICY; + } + + self::$encoder->StartWBXML(); + + //set the new final policy key in the device manager + // START ADDED dw2412 Android provisioning fix + if (!$phase2) { + $policykey = self::$deviceManager->GenerateProvisioningPolicyKey(); + self::$deviceManager->SetProvisioningPolicyKey($policykey); + self::$topCollector->AnnounceInformation("Policies deployed", true); + } + else { + // just create a temporary key (i.e. iPhone OS4 Beta does not like policykey 0 in response) + $policykey = self::$deviceManager->GenerateProvisioningPolicyKey(); + } + // END ADDED dw2412 Android provisioning fix + + self::$encoder->startTag(SYNC_PROVISION_PROVISION); + { + self::$encoder->startTag(SYNC_PROVISION_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_PROVISION_POLICIES); + self::$encoder->startTag(SYNC_PROVISION_POLICY); + + if(isset($policytype)) { + self::$encoder->startTag(SYNC_PROVISION_POLICYTYPE); + self::$encoder->content($policytype); + self::$encoder->endTag(); + } + + self::$encoder->startTag(SYNC_PROVISION_STATUS); + self::$encoder->content($policystatus); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_PROVISION_POLICYKEY); + self::$encoder->content($policykey); + self::$encoder->endTag(); + + if ($phase2 && $policystatus === SYNC_PROVISION_POLICYSTATUS_SUCCESS) { + self::$encoder->startTag(SYNC_PROVISION_DATA); + if ($policytype == 'MS-WAP-Provisioning-XML') { + self::$encoder->content(''); + } + elseif ($policytype == 'MS-EAS-Provisioning-WBXML') { + self::$encoder->startTag(SYNC_PROVISION_EASPROVISIONDOC); + + $prov = self::$deviceManager->GetProvisioningObject(); + if (!$prov->Check()) + throw new FatalException("Invalid policies!"); + + $prov->Encode(self::$encoder); + self::$encoder->endTag(); + } + else { + ZLog::Write(LOGLEVEL_WARN, "Wrong policy type"); + self::$topCollector->AnnounceInformation("Policytype not supported", true); + return false; + } + self::$topCollector->AnnounceInformation("Updated provisiong", true); + + self::$encoder->endTag();//data + } + self::$encoder->endTag();//policy + self::$encoder->endTag(); //policies + } + + //wipe data if a higher RWSTATUS is requested + if ($rwstatus > SYNC_PROVISION_RWSTATUS_OK && $policystatus === SYNC_PROVISION_POLICYSTATUS_SUCCESS) { + self::$encoder->startTag(SYNC_PROVISION_REMOTEWIPE, false, true); + self::$deviceManager->SetProvisioningWipeStatus(($rwstatusWiped)?SYNC_PROVISION_RWSTATUS_WIPED:SYNC_PROVISION_RWSTATUS_REQUESTED); + self::$topCollector->AnnounceInformation(sprintf("Remote wipe %s", ($rwstatusWiped)?"executed":"requested"), true); + } + + self::$encoder->endTag();//provision + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/request.php b/sources/lib/request/request.php new file mode 100644 index 0000000..d37b4b4 --- /dev/null +++ b/sources/lib/request/request.php @@ -0,0 +1,596 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class Request { + const UNKNOWN = "unknown"; + + /** + * self::filterEvilInput() options + */ + const LETTERS_ONLY = 1; + const HEX_ONLY = 2; + const WORDCHAR_ONLY = 3; + const NUMBERS_ONLY = 4; + const NUMBERSDOT_ONLY = 5; + const HEX_EXTENDED = 6; + + /** + * Command parameters for base64 encoded requests (AS >= 12.1) + */ + const COMMANDPARAM_ATTACHMENTNAME = 0; + const COMMANDPARAM_COLLECTIONID = 1; //deprecated + const COMMANDPARAM_COLLECTIONNAME = 2; //deprecated + const COMMANDPARAM_ITEMID = 3; + const COMMANDPARAM_LONGID = 4; + const COMMANDPARAM_PARENTID = 5; //deprecated + const COMMANDPARAM_OCCURRENCE = 6; + const COMMANDPARAM_OPTIONS = 7; //used by SmartReply, SmartForward, SendMail, ItemOperations + const COMMANDPARAM_USER = 8; //used by any command + //possible bitflags for COMMANDPARAM_OPTIONS + const COMMANDPARAM_OPTIONS_SAVEINSENT = 0x01; + const COMMANDPARAM_OPTIONS_ACCEPTMULTIPART = 0x02; + + static private $input; + static private $output; + static private $headers; + static private $getparameters; + static private $command; + static private $device; + static private $method; + static private $remoteAddr; + static private $getUser; + static private $devid; + static private $devtype; + static private $authUser; + static private $authDomain; + static private $authPassword; + static private $asProtocolVersion; + static private $policykey; + static private $useragent; + static private $attachmentName; + static private $collectionId; + static private $itemId; + static private $longId; //TODO + static private $occurence; //TODO + static private $saveInSent; + static private $acceptMultipart; + + + /** + * Initializes request data + * + * @access public + * @return + */ + static public function Initialize() { + // try to open stdin & stdout + self::$input = fopen("php://input", "r"); + self::$output = fopen("php://output", "w+"); + + // Parse the standard GET parameters + if(isset($_GET["Cmd"])) + self::$command = self::filterEvilInput($_GET["Cmd"], self::LETTERS_ONLY); + + // getUser is unfiltered, as everything is allowed.. even "/", "\" or ".." + if(isset($_GET["User"])) + self::$getUser = strtolower($_GET["User"]); + if(isset($_GET["DeviceId"])) + self::$devid = strtolower(self::filterEvilInput($_GET["DeviceId"], self::WORDCHAR_ONLY)); + if(isset($_GET["DeviceType"])) + self::$devtype = self::filterEvilInput($_GET["DeviceType"], self::LETTERS_ONLY); + if (isset($_GET["AttachmentName"])) + self::$attachmentName = self::filterEvilInput($_GET["AttachmentName"], self::HEX_EXTENDED); + if (isset($_GET["CollectionId"])) + self::$collectionId = self::filterEvilInput($_GET["CollectionId"], self::HEX_ONLY); + if (isset($_GET["ItemId"])) + self::$itemId = self::filterEvilInput($_GET["ItemId"], self::HEX_ONLY); + if (isset($_GET["SaveInSent"]) && $_GET["SaveInSent"] == "T") + self::$saveInSent = true; + + if(isset($_SERVER["REQUEST_METHOD"])) + self::$method = self::filterEvilInput($_SERVER["REQUEST_METHOD"], self::LETTERS_ONLY); + // TODO check IPv6 addresses + if(isset($_SERVER["REMOTE_ADDR"])) + self::$remoteAddr = self::filterEvilInput($_SERVER["REMOTE_ADDR"], self::NUMBERSDOT_ONLY); + + // in protocol version > 14 mobile send these inputs as encoded query string + if (!isset(self::$command) && !empty($_SERVER['QUERY_STRING']) && Utils::IsBase64String($_SERVER['QUERY_STRING'])) { + $query = Utils::DecodeBase64URI($_SERVER['QUERY_STRING']); + if (!isset(self::$command) && isset($query['Command'])) + self::$command = Utils::GetCommandFromCode($query['Command']); + + if (!isset(self::$getUser) && isset($query[self::COMMANDPARAM_USER])) + self::$getUser = strtolower($query[self::COMMANDPARAM_USER]); + + if (!isset(self::$devid) && isset($query['DevID'])) + self::$devid = strtolower(self::filterEvilInput($query['DevID'], self::WORDCHAR_ONLY)); + + if (!isset(self::$devtype) && isset($query['DevType'])) + self::$devtype = self::filterEvilInput($query['DevType'], self::LETTERS_ONLY); + + if (isset($query['PolKey'])) + self::$policykey = (int) self::filterEvilInput($query['PolKey'], self::NUMBERS_ONLY); + + if (isset($query['ProtVer'])) + self::$asProtocolVersion = self::filterEvilInput($query['ProtVer'], self::NUMBERS_ONLY) / 10; + + if (isset($query[self::COMMANDPARAM_ATTACHMENTNAME])) + self::$attachmentName = self::filterEvilInput($query[self::COMMANDPARAM_ATTACHMENTNAME], self::HEX_EXTENDED); + + if (isset($query[self::COMMANDPARAM_COLLECTIONID])) + self::$collectionId = self::filterEvilInput($query[self::COMMANDPARAM_COLLECTIONID], self::HEX_ONLY); + + if (isset($query[self::COMMANDPARAM_ITEMID])) + self::$itemId = self::filterEvilInput($query[self::COMMANDPARAM_ITEMID], self::HEX_ONLY); + + if (isset($query[self::COMMANDPARAM_OPTIONS]) && (ord($query[self::COMMANDPARAM_OPTIONS]) & self::COMMANDPARAM_OPTIONS_SAVEINSENT)) + self::$saveInSent = true; + + if (isset($query[self::COMMANDPARAM_OPTIONS]) && (ord($query[self::COMMANDPARAM_OPTIONS]) & self::COMMANDPARAM_OPTIONS_ACCEPTMULTIPART)) + self::$acceptMultipart = true; + } + + // in base64 encoded query string user is not necessarily set + if (!isset(self::$getUser) && isset($_SERVER['PHP_AUTH_USER'])) + list(self::$getUser,) = Utils::SplitDomainUser(strtolower($_SERVER['PHP_AUTH_USER'])); + } + + /** + * Reads and processes the request headers + * + * @access public + * @return + */ + static public function ProcessHeaders() { + self::$headers = array_change_key_case(apache_request_headers(), CASE_LOWER); + self::$useragent = (isset(self::$headers["user-agent"]))? self::$headers["user-agent"] : self::UNKNOWN; + if (!isset(self::$asProtocolVersion)) + self::$asProtocolVersion = (isset(self::$headers["ms-asprotocolversion"]))? self::filterEvilInput(self::$headers["ms-asprotocolversion"], self::NUMBERSDOT_ONLY) : ZPush::GetLatestSupportedASVersion(); + + //if policykey is not yet set, try to set it from the header + //the policy key might be set in Request::Initialize from the base64 encoded query + if (!isset(self::$policykey)) { + if (isset(self::$headers["x-ms-policykey"])) + self::$policykey = (int) self::filterEvilInput(self::$headers["x-ms-policykey"], self::NUMBERS_ONLY); + else + self::$policykey = 0; + } + + if (!empty($_SERVER['QUERY_STRING']) && Utils::IsBase64String($_SERVER['QUERY_STRING'])) { + ZLog::Write(LOGLEVEL_DEBUG, "Using data from base64 encoded query string"); + if (isset(self::$policykey)) + self::$headers["x-ms-policykey"] = self::$policykey; + + if (isset(self::$asProtocolVersion)) + self::$headers["ms-asprotocolversion"] = self::$asProtocolVersion; + } + + if (!isset(self::$acceptMultipart) && isset(self::$headers["ms-asacceptmultipart"]) && strtoupper(self::$headers["ms-asacceptmultipart"]) == "T") { + self::$acceptMultipart = true; + } + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Request::ProcessHeaders() ASVersion: %s", self::$asProtocolVersion)); + + if (defined('USE_X_FORWARDED_FOR_HEADER') && USE_X_FORWARDED_FOR_HEADER == true && isset(self::$headers["x-forwarded-for"])) { + $forwardedIP = self::filterEvilInput(self::$headers["x-forwarded-for"], self::NUMBERSDOT_ONLY); + if ($forwardedIP) { + self::$remoteAddr = $forwardedIP; + ZLog::Write(LOGLEVEL_INFO, sprintf("'X-Forwarded-for' indicates remote IP: %s", self::$remoteAddr)); + } + } + } + + /** + * Reads and parses the HTTP-Basic-Auth data + * + * @access public + * @return boolean data sent or not + */ + static public function AuthenticationInfo() { + // split username & domain if received as one + if (isset($_SERVER['PHP_AUTH_USER'])) { + list(self::$authUser, self::$authDomain) = Utils::SplitDomainUser($_SERVER['PHP_AUTH_USER']); + self::$authPassword = (isset($_SERVER['PHP_AUTH_PW']))?$_SERVER['PHP_AUTH_PW'] : ""; + } + // authUser & authPassword are unfiltered! + return (self::$authUser != "" && self::$authPassword != ""); + } + + + /**---------------------------------------------------------------------------------------------------------- + * Getter & Checker + */ + + /** + * Returns the input stream + * + * @access public + * @return handle/boolean false if not available + */ + static public function GetInputStream() { + if (isset(self::$input)) + return self::$input; + else + return false; + } + + /** + * Returns the output stream + * + * @access public + * @return handle/boolean false if not available + */ + static public function GetOutputStream() { + if (isset(self::$output)) + return self::$output; + else + return false; + } + + /** + * Returns the request method + * + * @access public + * @return string + */ + static public function GetMethod() { + if (isset(self::$method)) + return self::$method; + else + return self::UNKNOWN; + } + + /** + * Returns the value of the user parameter of the querystring + * + * @access public + * @return string/boolean false if not available + */ + static public function GetGETUser() { + if (isset(self::$getUser)) + return self::$getUser; + else + return self::UNKNOWN; + } + + /** + * Returns the value of the ItemId parameter of the querystring + * + * @access public + * @return string/boolean false if not available + */ + static public function GetGETItemId() { + if (isset(self::$itemId)) + return self::$itemId; + else + return false; + } + + /** + * Returns the value of the CollectionId parameter of the querystring + * + * @access public + * @return string/boolean false if not available + */ + static public function GetGETCollectionId() { + if (isset(self::$collectionId)) + return self::$collectionId; + else + return false; + } + + /** + * Returns if the SaveInSent parameter of the querystring is set + * + * @access public + * @return boolean + */ + static public function GetGETSaveInSent() { + if (isset(self::$saveInSent)) + return self::$saveInSent; + else + return true; + } + + /** + * Returns if the AcceptMultipart parameter of the querystring is set + * + * @access public + * @return boolean + */ + static public function GetGETAcceptMultipart() { + if (isset(self::$acceptMultipart)) + return self::$acceptMultipart; + else + return false; + } + + /** + * Returns the value of the AttachmentName parameter of the querystring + * + * @access public + * @return string/boolean false if not available + */ + static public function GetGETAttachmentName() { + if (isset(self::$attachmentName)) + return self::$attachmentName; + else + return false; + } + + /** + * Returns the authenticated user + * + * @access public + * @return string/boolean false if not available + */ + static public function GetAuthUser() { + if (isset(self::$authUser)) + return self::$authUser; + else + return false; + } + + /** + * Returns the authenticated domain for the user + * + * @access public + * @return string/boolean false if not available + */ + static public function GetAuthDomain() { + if (isset(self::$authDomain)) + return self::$authDomain; + else + return false; + } + + /** + * Returns the transmitted password + * + * @access public + * @return string/boolean false if not available + */ + static public function GetAuthPassword() { + if (isset(self::$authPassword)) + return self::$authPassword; + else + return false; + } + + /** + * Returns the RemoteAddress + * + * @access public + * @return string + */ + static public function GetRemoteAddr() { + if (isset(self::$remoteAddr)) + return self::$remoteAddr; + else + return "UNKNOWN"; + } + + /** + * Returns the command to be executed + * + * @access public + * @return string/boolean false if not available + */ + static public function GetCommand() { + if (isset(self::$command)) + return self::$command; + else + return false; + } + + /** + * Returns the command code which is being executed + * + * @access public + * @return string/boolean false if not available + */ + static public function GetCommandCode() { + if (isset(self::$command)) + return Utils::GetCodeFromCommand(self::$command); + else + return false; + } + + /** + * Returns the device id transmitted + * + * @access public + * @return string/boolean false if not available + */ + static public function GetDeviceID() { + if (isset(self::$devid)) + return self::$devid; + else + return false; + } + + /** + * Returns the device type if transmitted + * + * @access public + * @return string/boolean false if not available + */ + static public function GetDeviceType() { + if (isset(self::$devtype)) + return self::$devtype; + else + return false; + } + + /** + * Returns the value of supported AS protocol from the headers + * + * @access public + * @return string/boolean false if not available + */ + static public function GetProtocolVersion() { + if (isset(self::$asProtocolVersion)) + return self::$asProtocolVersion; + else + return false; + } + + /** + * Returns the user agent sent in the headers + * + * @access public + * @return string/boolean false if not available + */ + static public function GetUserAgent() { + if (isset(self::$useragent)) + return self::$useragent; + else + return self::UNKNOWN; + } + + /** + * Returns policy key sent by the device + * + * @access public + * @return int/boolean false if not available + */ + static public function GetPolicyKey() { + if (isset(self::$policykey)) + return self::$policykey; + else + return false; + } + + /** + * Indicates if a policy key was sent by the device + * + * @access public + * @return boolean + */ + static public function WasPolicyKeySent() { + return isset(self::$headers["x-ms-policykey"]); + } + + /** + * Indicates if Z-Push was called with a POST request + * + * @access public + * @return boolean + */ + static public function IsMethodPOST() { + return (self::$method == "POST"); + } + + /** + * Indicates if Z-Push was called with a GET request + * + * @access public + * @return boolean + */ + static public function IsMethodGET() { + return (self::$method == "GET"); + } + + /** + * Indicates if Z-Push was called with a OPTIONS request + * + * @access public + * @return boolean + */ + static public function IsMethodOPTIONS() { + return (self::$method == "OPTIONS"); + } + + /** + * Sometimes strange device ids are sumbitted + * No device information should be saved when this happens + * + * @access public + * @return boolean false if invalid + */ + static public function IsValidDeviceID() { + if (self::GetDeviceID() === "validate" || self::GetDeviceID() === "webservice") + return false; + else + return true; + } + + /** + * Returns the amount of data sent in this request (from the headers) + * + * @access public + * @return int + */ + static public function GetContentLength() { + return (isset(self::$headers["content-length"]))? (int) self::$headers["content-length"] : 0; + } + + + /**---------------------------------------------------------------------------------------------------------- + * Private stuff + */ + + /** + * Replaces all not allowed characters in a string + * + * @param string $input the input string + * @param int $filter one of the predefined filters: LETTERS_ONLY, HEX_ONLY, WORDCHAR_ONLY, NUMBERS_ONLY, NUMBERSDOT_ONLY + * @param char $replacevalue (opt) a character the filtered characters should be replaced with + * + * @access public + * @return string + */ + static private function filterEvilInput($input, $filter, $replacevalue = '') { + $re = false; + if ($filter == self::LETTERS_ONLY) $re = "/[^A-Za-z]/"; + else if ($filter == self::HEX_ONLY) $re = "/[^A-Fa-f0-9]/"; + else if ($filter == self::WORDCHAR_ONLY) $re = "/[^A-Za-z0-9]/"; + else if ($filter == self::NUMBERS_ONLY) $re = "/[^0-9]/"; + else if ($filter == self::NUMBERSDOT_ONLY) $re = "/[^0-9\.]/"; + else if ($filter == self::HEX_EXTENDED) $re = "/[^A-Fa-f0-9\:]/"; + + return ($re) ? preg_replace($re, $replacevalue, $input) : ''; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/requestprocessor.php b/sources/lib/request/requestprocessor.php new file mode 100644 index 0000000..c3a898e --- /dev/null +++ b/sources/lib/request/requestprocessor.php @@ -0,0 +1,157 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +abstract class RequestProcessor { + static protected $backend; + static protected $deviceManager; + static protected $topCollector; + static protected $decoder; + static protected $encoder; + static protected $userIsAuthenticated; + static protected $specialHeaders; + + /** + * Authenticates the remote user + * The sent HTTP authentication information is used to on Backend->Logon(). + * As second step the GET-User verified by Backend->Setup() for permission check + * Request::GetGETUser() is usually the same as the Request::GetAuthUser(). + * If the GETUser is different from the AuthUser, the AuthUser MUST HAVE admin + * permissions on GETUsers data store. Only then the Setup() will be sucessfull. + * This allows the user 'john' to do operations as user 'joe' if he has sufficient privileges. + * + * @access public + * @return + * @throws AuthenticationRequiredException + */ + static public function Authenticate() { + self::$userIsAuthenticated = false; + + // when a certificate is sent, allow authentication only as the certificate owner + if(defined("CERTIFICATE_OWNER_PARAMETER") && isset($_SERVER[CERTIFICATE_OWNER_PARAMETER]) && strtolower($_SERVER[CERTIFICATE_OWNER_PARAMETER]) != strtolower(Request::GetAuthUser())) + throw new AuthenticationRequiredException(sprintf("Access denied. Access is allowed only for the certificate owner '%s'", $_SERVER[CERTIFICATE_OWNER_PARAMETER])); + + $backend = ZPush::GetBackend(); + if($backend->Logon(Request::GetAuthUser(), Request::GetAuthDomain(), Request::GetAuthPassword()) == false) + throw new AuthenticationRequiredException("Access denied. Username or password incorrect"); + + // mark this request as "authenticated" + self::$userIsAuthenticated = true; + + // check Auth-User's permissions on GETUser's store + if($backend->Setup(Request::GetGETUser(), true) == false) + throw new AuthenticationRequiredException(sprintf("Not enough privileges of '%s' to setup for user '%s': Permission denied", Request::GetAuthUser(), Request::GetGETUser())); + } + + /** + * Indicates if the user was "authenticated" + * + * @access public + * @return boolean + */ + static public function isUserAuthenticated() { + if (!isset(self::$userIsAuthenticated)) + return false; + return self::$userIsAuthenticated; + } + + /** + * Initialize the RequestProcessor + * + * @access public + * @return + */ + static public function Initialize() { + self::$backend = ZPush::GetBackend(); + self::$deviceManager = ZPush::GetDeviceManager(); + self::$topCollector = ZPush::GetTopCollector(); + + if (!ZPush::CommandNeedsPlainInput(Request::GetCommandCode())) + self::$decoder = new WBXMLDecoder(Request::GetInputStream()); + + self::$encoder = new WBXMLEncoder(Request::GetOutputStream(), Request::GetGETAcceptMultipart()); + } + + /** + * Loads the command handler and processes a command sent from the mobile + * + * @access public + * @return boolean + */ + static public function HandleRequest() { + $handler = ZPush::GetRequestHandlerForCommand(Request::GetCommandCode()); + + // TODO handle WBXML exceptions here and print stack + return $handler->Handle(Request::GetCommandCode()); + } + + /** + * Returns any additional headers which should be sent to the mobile + * + * @access public + * @return array + */ + static public function GetSpecialHeaders() { + if (!isset(self::$specialHeaders) || !is_array(self::$specialHeaders)) + return array(); + + return self::$specialHeaders; + } + + /** + * Handles a command + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + abstract public function Handle($commandCode); +} +?> \ No newline at end of file diff --git a/sources/lib/request/resolverecipients.php b/sources/lib/request/resolverecipients.php new file mode 100755 index 0000000..9c19d5b --- /dev/null +++ b/sources/lib/request/resolverecipients.php @@ -0,0 +1,104 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class ResolveRecipients extends RequestProcessor { + + /** + * Handles the ResolveRecipients command + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + // Parse input + if(!self::$decoder->getElementStartTag(SYNC_RESOLVERECIPIENTS_RESOLVERECIPIENTS)) + return false; + + $resolveRecipients = new SyncResolveRecipients(); + $resolveRecipients->Decode(self::$decoder); + + if(!self::$decoder->getElementEndTag()) + return false; // SYNC_RESOLVERECIPIENTS_RESOLVERECIPIENTS + + $resolveRecipients = self::$backend->ResolveRecipients($resolveRecipients); + + + self::$encoder->startWBXML(); + self::$encoder->startTag(SYNC_RESOLVERECIPIENTS_RESOLVERECIPIENTS); + + self::$encoder->startTag(SYNC_RESOLVERECIPIENTS_STATUS); + self::$encoder->content($resolveRecipients->status); + self::$encoder->endTag(); // SYNC_RESOLVERECIPIENTS_STATUS + + + foreach ($resolveRecipients->to as $i => $to) { + self::$encoder->startTag(SYNC_RESOLVERECIPIENTS_RESPONSE); + self::$encoder->startTag(SYNC_RESOLVERECIPIENTS_TO); + self::$encoder->content($to); + self::$encoder->endTag(); // SYNC_RESOLVERECIPIENTS_TO + + self::$encoder->startTag(SYNC_RESOLVERECIPIENTS_STATUS); + self::$encoder->content($resolveRecipients->status); + self::$encoder->endTag(); + + // do only if recipient is resolved + if ($resolveRecipients->status != SYNC_RESOLVERECIPSSTATUS_RESPONSE_UNRESOLVEDRECIP) { + self::$encoder->startTag(SYNC_RESOLVERECIPIENTS_RECIPIENTCOUNT); + self::$encoder->content(count($resolveRecipients->recipient)); + self::$encoder->endTag(); // SYNC_RESOLVERECIPIENTS_RECIPIENTCOUNT + + self::$encoder->startTag(SYNC_RESOLVERECIPIENTS_RECIPIENT); + $resolveRecipients->recipient[$i]->Encode(self::$encoder); + self::$encoder->endTag(); // SYNC_RESOLVERECIPIENTS_RECIPIENT + } + + self::$encoder->endTag(); // SYNC_RESOLVERECIPIENTS_RESPONSE + } + + self::$encoder->endTag(); // SYNC_RESOLVERECIPIENTS_RESOLVERECIPIENTS + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/search.php b/sources/lib/request/search.php new file mode 100644 index 0000000..643a7b6 --- /dev/null +++ b/sources/lib/request/search.php @@ -0,0 +1,446 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class Search extends RequestProcessor { + + /** + * Handles the Search command + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + $searchrange = '0'; + $cpo = new ContentParameters(); + + if(!self::$decoder->getElementStartTag(SYNC_SEARCH_SEARCH)) + return false; + + // TODO check: possible to search in other stores? + if(!self::$decoder->getElementStartTag(SYNC_SEARCH_STORE)) + return false; + + if(!self::$decoder->getElementStartTag(SYNC_SEARCH_NAME)) + return false; + $searchname = strtoupper(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + + if(!self::$decoder->getElementStartTag(SYNC_SEARCH_QUERY)) + return false; + + // check if it is a content of an element (= GAL search) + // or a starttag (= mailbox or documentlibrary search) + $searchquery = self::$decoder->getElementContent(); + if($searchquery && !self::$decoder->getElementEndTag()) + return false; + + if ($searchquery === false) { + $cpo->SetSearchName($searchname); + if (self::$decoder->getElementStartTag(SYNC_SEARCH_AND)) { + if (self::$decoder->getElementStartTag(SYNC_FOLDERID)) { + $searchfolderid = self::$decoder->getElementContent(); + $cpo->SetSearchFolderid($searchfolderid); + if(!self::$decoder->getElementEndTag()) // SYNC_FOLDERTYPE + return false; + } + + + if (self::$decoder->getElementStartTag(SYNC_FOLDERTYPE)) { + $searchclass = self::$decoder->getElementContent(); + $cpo->SetSearchClass($searchclass); + if(!self::$decoder->getElementEndTag()) // SYNC_FOLDERTYPE + return false; + } + + if (self::$decoder->getElementStartTag(SYNC_FOLDERID)) { + $searchfolderid = self::$decoder->getElementContent(); + $cpo->SetSearchFolderid($searchfolderid); + if(!self::$decoder->getElementEndTag()) // SYNC_FOLDERTYPE + return false; + } + + if (self::$decoder->getElementStartTag(SYNC_SEARCH_FREETEXT)) { + $searchfreetext = self::$decoder->getElementContent(); + $cpo->SetSearchFreeText($searchfreetext); + if(!self::$decoder->getElementEndTag()) // SYNC_SEARCH_FREETEXT + return false; + } + + //TODO - review + if (self::$decoder->getElementStartTag(SYNC_SEARCH_GREATERTHAN)) { + if(self::$decoder->getElementStartTag(SYNC_POOMMAIL_DATERECEIVED)) { + $datereceivedgreater = true; + if (($dam = self::$decoder->getElementContent()) !== false) { + $datereceivedgreater = true; + if(!self::$decoder->getElementEndTag()) { + return false; + } + } + $cpo->SetSearchDateReceivedGreater($datereceivedgreater); + } + + if(self::$decoder->getElementStartTag(SYNC_SEARCH_VALUE)) { + $searchvalue = self::$decoder->getElementContent(); + $cpo->SetSearchValueGreater($searchvalue); + if(!self::$decoder->getElementEndTag()) // SYNC_SEARCH_VALUE + return false; + } + + if(!self::$decoder->getElementEndTag()) // SYNC_SEARCH_GREATERTHAN + return false; + } + + if (self::$decoder->getElementStartTag(SYNC_SEARCH_LESSTHAN)) { + if(self::$decoder->getElementStartTag(SYNC_POOMMAIL_DATERECEIVED)) { + $datereceivedless = true; + if (($dam = self::$decoder->getElementContent()) !== false) { + $datereceivedless = true; + if(!self::$decoder->getElementEndTag()) { + return false; + } + } + $cpo->SetSearchDateReceivedLess($datereceivedless); + } + + if(self::$decoder->getElementStartTag(SYNC_SEARCH_VALUE)) { + $searchvalue = self::$decoder->getElementContent(); + $cpo->SetSearchValueLess($searchvalue); + if(!self::$decoder->getElementEndTag()) // SYNC_SEARCH_VALUE + return false; + } + + if(!self::$decoder->getElementEndTag()) // SYNC_SEARCH_LESSTHAN + return false; + } + + if (self::$decoder->getElementStartTag(SYNC_SEARCH_FREETEXT)) { + $searchfreetext = self::$decoder->getElementContent(); + $cpo->SetSearchFreeText($searchfreetext); + if(!self::$decoder->getElementEndTag()) // SYNC_SEARCH_FREETEXT + return false; + } + + if(!self::$decoder->getElementEndTag()) // SYNC_SEARCH_AND + return false; + } + elseif (self::$decoder->getElementStartTag(SYNC_SEARCH_EQUALTO)) { + // linkid can be an empty tag as well as have value + if(self::$decoder->getElementStartTag(SYNC_DOCUMENTLIBRARY_LINKID)) { + if (($linkId = self::$decoder->getElementContent()) !== false) { + $cpo->SetLinkId($linkId); + if(!self::$decoder->getElementEndTag()) { // SYNC_DOCUMENTLIBRARY_LINKID + return false; + } + } + } + + if(self::$decoder->getElementStartTag(SYNC_SEARCH_VALUE)) { + $searchvalue = self::$decoder->getElementContent(); + $cpo->SetSearchValueLess($searchvalue); + if(!self::$decoder->getElementEndTag()) // SYNC_SEARCH_VALUE + return false; + } + + if(!self::$decoder->getElementEndTag()) // SYNC_SEARCH_EQUALTO + return false; + } + + if(!self::$decoder->getElementEndTag()) // SYNC_SEARCH_QUERY + return false; + + } + + if(self::$decoder->getElementStartTag(SYNC_SEARCH_OPTIONS)) { + while(1) { + if(self::$decoder->getElementStartTag(SYNC_SEARCH_RANGE)) { + $searchrange = self::$decoder->getElementContent(); + $cpo->SetSearchRange($searchrange); + if(!self::$decoder->getElementEndTag()) + return false; + } + + + if(self::$decoder->getElementStartTag(SYNC_SEARCH_REBUILDRESULTS)) { + $rebuildresults = true; + if (($dam = self::$decoder->getElementContent()) !== false) { + $rebuildresults = true; + if(!self::$decoder->getElementEndTag()) { + return false; + } + } + $cpo->SetSearchRebuildResults($rebuildresults); + } + + if(self::$decoder->getElementStartTag(SYNC_SEARCH_DEEPTRAVERSAL)) { + $deeptraversal = true; + if (($dam = self::$decoder->getElementContent()) !== false) { + $deeptraversal = true; + if(!self::$decoder->getElementEndTag()) { + return false; + } + } + $cpo->SetSearchDeepTraversal($deeptraversal); + } + + if(self::$decoder->getElementStartTag(SYNC_MIMESUPPORT)) { + $cpo->SetMimeSupport(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + //TODO body preferences + while (self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_BODYPREFERENCE)) { + if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_TYPE)) { + $bptype = self::$decoder->getElementContent(); + $cpo->BodyPreference($bptype); + if(!self::$decoder->getElementEndTag()) { + return false; + } + } + + if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_TRUNCATIONSIZE)) { + $cpo->BodyPreference($bptype)->SetTruncationSize(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_ALLORNONE)) { + $cpo->BodyPreference($bptype)->SetAllOrNone(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_PREVIEW)) { + $cpo->BodyPreference($bptype)->SetPreview(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(!self::$decoder->getElementEndTag()) + return false; + } + + $e = self::$decoder->peek(); + if($e[EN_TYPE] == EN_TYPE_ENDTAG) { + self::$decoder->getElementEndTag(); + break; + } + } + } + if(!self::$decoder->getElementEndTag()) //store + return false; + + if(!self::$decoder->getElementEndTag()) //search + return false; + + // get SearchProvider + $searchprovider = ZPush::GetSearchProvider(); + $status = SYNC_SEARCHSTATUS_SUCCESS; + $rows = array(); + + // TODO support other searches + if ($searchprovider->SupportsType($searchname)) { + $storestatus = SYNC_SEARCHSTATUS_STORE_SUCCESS; + try { + if ($searchname == ISearchProvider::SEARCH_GAL) { + //get search results from the searchprovider + $rows = $searchprovider->GetGALSearchResults($searchquery, $searchrange); + } + elseif ($searchname == ISearchProvider::SEARCH_MAILBOX) { + $rows = $searchprovider->GetMailboxSearchResults($cpo); + } + } + catch (StatusException $stex) { + $storestatus = $stex->getCode(); + } + } + else { + $rows = array('searchtotal' => 0); + $status = SYNC_SEARCHSTATUS_SERVERERROR; + ZLog::Write(LOGLEVEL_WARN, sprintf("Searchtype '%s' is not supported.", $searchname)); + self::$topCollector->AnnounceInformation(sprintf("Unsupported type '%s''", $searchname), true); + } + $searchprovider->Disconnect(); + + self::$topCollector->AnnounceInformation(sprintf("'%s' search found %d results", $searchname, $rows['searchtotal']), true); + + self::$encoder->startWBXML(); + self::$encoder->startTag(SYNC_SEARCH_SEARCH); + + self::$encoder->startTag(SYNC_SEARCH_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); + + if ($status == SYNC_SEARCHSTATUS_SUCCESS) { + self::$encoder->startTag(SYNC_SEARCH_RESPONSE); + self::$encoder->startTag(SYNC_SEARCH_STORE); + + self::$encoder->startTag(SYNC_SEARCH_STATUS); + self::$encoder->content($storestatus); + self::$encoder->endTag(); + + if (isset($rows['range'])) { + $searchrange = $rows['range']; + unset($rows['range']); + } + if (isset($rows['searchtotal'])) { + $searchtotal = $rows['searchtotal']; + unset($rows['searchtotal']); + } + if ($searchname == ISearchProvider::SEARCH_GAL) { + if (is_array($rows) && !empty($rows)) { + foreach ($rows as $u) { + self::$encoder->startTag(SYNC_SEARCH_RESULT); + self::$encoder->startTag(SYNC_SEARCH_PROPERTIES); + + self::$encoder->startTag(SYNC_GAL_DISPLAYNAME); + self::$encoder->content((isset($u[SYNC_GAL_DISPLAYNAME]))?$u[SYNC_GAL_DISPLAYNAME]:"No name"); + self::$encoder->endTag(); + + if (isset($u[SYNC_GAL_PHONE])) { + self::$encoder->startTag(SYNC_GAL_PHONE); + self::$encoder->content($u[SYNC_GAL_PHONE]); + self::$encoder->endTag(); + } + + if (isset($u[SYNC_GAL_OFFICE])) { + self::$encoder->startTag(SYNC_GAL_OFFICE); + self::$encoder->content($u[SYNC_GAL_OFFICE]); + self::$encoder->endTag(); + } + + if (isset($u[SYNC_GAL_TITLE])) { + self::$encoder->startTag(SYNC_GAL_TITLE); + self::$encoder->content($u[SYNC_GAL_TITLE]); + self::$encoder->endTag(); + } + + if (isset($u[SYNC_GAL_COMPANY])) { + self::$encoder->startTag(SYNC_GAL_COMPANY); + self::$encoder->content($u[SYNC_GAL_COMPANY]); + self::$encoder->endTag(); + } + + if (isset($u[SYNC_GAL_ALIAS])) { + self::$encoder->startTag(SYNC_GAL_ALIAS); + self::$encoder->content($u[SYNC_GAL_ALIAS]); + self::$encoder->endTag(); + } + + // Always send the firstname, even empty. Nokia needs this to display the entry + self::$encoder->startTag(SYNC_GAL_FIRSTNAME); + self::$encoder->content((isset($u[SYNC_GAL_FIRSTNAME]))?$u[SYNC_GAL_FIRSTNAME]:""); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_GAL_LASTNAME); + self::$encoder->content((isset($u[SYNC_GAL_LASTNAME]))?$u[SYNC_GAL_LASTNAME]:"No name"); + self::$encoder->endTag(); + + if (isset($u[SYNC_GAL_HOMEPHONE])) { + self::$encoder->startTag(SYNC_GAL_HOMEPHONE); + self::$encoder->content($u[SYNC_GAL_HOMEPHONE]); + self::$encoder->endTag(); + } + + if (isset($u[SYNC_GAL_MOBILEPHONE])) { + self::$encoder->startTag(SYNC_GAL_MOBILEPHONE); + self::$encoder->content($u[SYNC_GAL_MOBILEPHONE]); + self::$encoder->endTag(); + } + + self::$encoder->startTag(SYNC_GAL_EMAILADDRESS); + self::$encoder->content((isset($u[SYNC_GAL_EMAILADDRESS]))?$u[SYNC_GAL_EMAILADDRESS]:""); + self::$encoder->endTag(); + + self::$encoder->endTag();//result + self::$encoder->endTag();//properties + } + } + } + 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(); + + 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 + } + } + // it seems that android 4 requires range and searchtotal + // or it won't display the search results + if (isset($searchrange)) { + self::$encoder->startTag(SYNC_SEARCH_RANGE); + self::$encoder->content($searchrange); + self::$encoder->endTag(); + } + if (isset($searchtotal) && $searchtotal > 0) { + self::$encoder->startTag(SYNC_SEARCH_TOTAL); + self::$encoder->content($searchtotal); + self::$encoder->endTag(); + } + + self::$encoder->endTag();//store + self::$encoder->endTag();//response + } + self::$encoder->endTag();//search + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/sendmail.php b/sources/lib/request/sendmail.php new file mode 100644 index 0000000..fbdb8b3 --- /dev/null +++ b/sources/lib/request/sendmail.php @@ -0,0 +1,139 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SendMail extends RequestProcessor { + + /** + * Handles the SendMail, SmartReply and SmartForward command + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + $status = SYNC_COMMONSTATUS_SUCCESS; + $sm = new SyncSendMail(); + + $reply = $forward = $parent = $sendmail = $smartreply = $smartforward = false; + if (Request::GetGETCollectionId()) + $parent = Request::GetGETCollectionId(); + if ($commandCode == ZPush::COMMAND_SMARTFORWARD) + $forward = Request::GetGETItemId(); + else if ($commandCode == ZPush::COMMAND_SMARTREPLY) + $reply = Request::GetGETItemId(); + + if (self::$decoder->IsWBXML()) { + $el = self::$decoder->getElement(); + + if($el[EN_TYPE] != EN_TYPE_STARTTAG) + return false; + + + if($el[EN_TAG] == SYNC_COMPOSEMAIL_SENDMAIL) + $sendmail = true; + else if($el[EN_TAG] == SYNC_COMPOSEMAIL_SMARTREPLY) + $smartreply = true; + else if($el[EN_TAG] == SYNC_COMPOSEMAIL_SMARTFORWARD) + $smartforward = true; + + if(!$sendmail && !$smartreply && !$smartforward) + return false; + + $sm->Decode(self::$decoder); + } + else { + $sm->mime = self::$decoder->GetPlainInputStream(); + // no wbxml output is provided, only a http OK + $sm->saveinsent = Request::GetGETSaveInSent(); + } + // Check if it is a reply or forward. Two cases are possible: + // 1. Either $smartreply or $smartforward are set after reading WBXML + // 2. Either $reply or $forward are set after geting the request parameters + if ($reply || $smartreply || $forward || $smartforward) { + // If the mobile sends an email in WBXML data the variables below + // should be set. If it is a RFC822 message, get the reply/forward message id + // from the request as they are always available there + if (!isset($sm->source)) $sm->source = new SyncSendMailSource(); + if (!isset($sm->source->itemid)) $sm->source->itemid = Request::GetGETItemId(); + if (!isset($sm->source->folderid)) $sm->source->folderid = Request::GetGETCollectionId(); + + // replyflag and forward flags are actually only for the correct icon. + // Even if they are a part of SyncSendMail object, they won't be streamed. + if ($smartreply || $reply) + $sm->replyflag = true; + else + $sm->forwardflag = true; + + if (!isset($sm->source->folderid)) + ZLog::Write(LOGLEVEL_ERROR, sprintf("No parent folder id while replying or forwarding message:'%s'", (($reply) ? $reply : $forward))); + } + + self::$topCollector->AnnounceInformation(sprintf("Sending email with %d bytes", strlen($sm->mime)), true); + + try { + $status = self::$backend->SendMail($sm); + } + catch (StatusException $se) { + $status = $se->getCode(); + $statusMessage = $se->getMessage(); + } + + if ($status != SYNC_COMMONSTATUS_SUCCESS) { + if (self::$decoder->IsWBXML()) { + // TODO check no WBXML on SmartReply and SmartForward + self::$encoder->StartWBXML(); + self::$encoder->startTag(SYNC_COMPOSEMAIL_SENDMAIL); + self::$encoder->startTag(SYNC_COMPOSEMAIL_STATUS); + self::$encoder->content($status); //TODO return the correct status + self::$encoder->endTag(); + self::$encoder->endTag(); + } + else + throw new HTTPReturnCodeException($statusMessage, HTTP_CODE_500, null, LOGLEVEL_WARN); + } + + return $status; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/settings.php b/sources/lib/request/settings.php new file mode 100644 index 0000000..1266b4c --- /dev/null +++ b/sources/lib/request/settings.php @@ -0,0 +1,232 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class Settings extends RequestProcessor { + + /** + * Handles the Settings command + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + if (!self::$decoder->getElementStartTag(SYNC_SETTINGS_SETTINGS)) + return false; + + //save the request parameters + $request = array(); + + // Loop through properties. Possible are: + // - Out of office + // - DevicePassword + // - DeviceInformation + // - UserInformation + // Each of them should only be once per request. Each property must be processed in order. + while (1) { + $propertyName = ""; + if (self::$decoder->getElementStartTag(SYNC_SETTINGS_OOF)) { + $propertyName = SYNC_SETTINGS_OOF; + } + if (self::$decoder->getElementStartTag(SYNC_SETTINGS_DEVICEPW)) { + $propertyName = SYNC_SETTINGS_DEVICEPW; + } + if (self::$decoder->getElementStartTag(SYNC_SETTINGS_DEVICEINFORMATION)) { + $propertyName = SYNC_SETTINGS_DEVICEINFORMATION; + } + if (self::$decoder->getElementStartTag(SYNC_SETTINGS_USERINFORMATION)) { + $propertyName = SYNC_SETTINGS_USERINFORMATION; + } + //TODO - check if it is necessary + //no property name available - break + if (!$propertyName) + break; + + //the property name is followed by either get or set + if (self::$decoder->getElementStartTag(SYNC_SETTINGS_GET)) { + //get is only available for OOF and user information + switch ($propertyName) { + case SYNC_SETTINGS_OOF: + $oofGet = new SyncOOF(); + $oofGet->Decode(self::$decoder); + if(!self::$decoder->getElementEndTag()) + return false; // SYNC_SETTINGS_GET + break; + + case SYNC_SETTINGS_USERINFORMATION: + $userInformation = new SyncUserInformation(); + break; + + default: + //TODO: a special status code needed? + ZLog::Write(LOGLEVEL_WARN, sprintf ("This property ('%s') is not allowed to use get in request", $propertyName)); + } + } + elseif (self::$decoder->getElementStartTag(SYNC_SETTINGS_SET)) { + //set is available for OOF, device password and device information + switch ($propertyName) { + case SYNC_SETTINGS_OOF: + $oofSet = new SyncOOF(); + $oofSet->Decode(self::$decoder); + //TODO check - do it after while(1) finished? + break; + + case SYNC_SETTINGS_DEVICEPW: + //TODO device password + $devicepassword = new SyncDevicePassword(); + $devicepassword->Decode(self::$decoder); + break; + + case SYNC_SETTINGS_DEVICEINFORMATION: + $deviceinformation = new SyncDeviceInformation(); + $deviceinformation->Decode(self::$decoder); + $deviceinformation->Status = SYNC_SETTINGSSTATUS_SUCCESS; + self::$deviceManager->SaveDeviceInformation($deviceinformation); + break; + + default: + //TODO: a special status code needed? + ZLog::Write(LOGLEVEL_WARN, sprintf ("This property ('%s') is not allowed to use set in request", $propertyName)); + } + + if(!self::$decoder->getElementEndTag()) + return false; // SYNC_SETTINGS_SET + } + else { + ZLog::Write(LOGLEVEL_WARN, sprintf("Neither get nor set found for property '%s'", $propertyName)); + return false; + } + + if(!self::$decoder->getElementEndTag()) + return false; // SYNC_SETTINGS_OOF or SYNC_SETTINGS_DEVICEPW or SYNC_SETTINGS_DEVICEINFORMATION or SYNC_SETTINGS_USERINFORMATION + + //break if it reached the endtag + $e = self::$decoder->peek(); + if($e[EN_TYPE] == EN_TYPE_ENDTAG) { + self::$decoder->getElementEndTag(); //SYNC_SETTINGS_SETTINGS + break; + } + } + + $status = SYNC_SETTINGSSTATUS_SUCCESS; + + //TODO put it in try catch block + //TODO implement Settings in the backend + //TODO save device information in device manager + //TODO status handling +// $data = self::$backend->Settings($request); + + self::$encoder->startWBXML(); + self::$encoder->startTag(SYNC_SETTINGS_SETTINGS); + + self::$encoder->startTag(SYNC_SETTINGS_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); //SYNC_SETTINGS_STATUS + + //get oof settings + if (isset($oofGet)) { + $oofGet = self::$backend->Settings($oofGet); + self::$encoder->startTag(SYNC_SETTINGS_OOF); + self::$encoder->startTag(SYNC_SETTINGS_STATUS); + self::$encoder->content($oofGet->Status); + self::$encoder->endTag(); //SYNC_SETTINGS_STATUS + + self::$encoder->startTag(SYNC_SETTINGS_GET); + $oofGet->Encode(self::$encoder); + self::$encoder->endTag(); //SYNC_SETTINGS_GET + self::$encoder->endTag(); //SYNC_SETTINGS_OOF + } + + //get user information + //TODO none email address found + if (isset($userInformation)) { + self::$backend->Settings($userInformation); + self::$encoder->startTag(SYNC_SETTINGS_USERINFORMATION); + self::$encoder->startTag(SYNC_SETTINGS_STATUS); + self::$encoder->content($userInformation->Status); + self::$encoder->endTag(); //SYNC_SETTINGS_STATUS + + self::$encoder->startTag(SYNC_SETTINGS_GET); + $userInformation->Encode(self::$encoder); + self::$encoder->endTag(); //SYNC_SETTINGS_GET + self::$encoder->endTag(); //SYNC_SETTINGS_USERINFORMATION + } + + //set out of office + if (isset($oofSet)) { + $oofSet = self::$backend->Settings($oofSet); + self::$encoder->startTag(SYNC_SETTINGS_OOF); + self::$encoder->startTag(SYNC_SETTINGS_STATUS); + self::$encoder->content($oofSet->Status); + self::$encoder->endTag(); //SYNC_SETTINGS_STATUS + self::$encoder->endTag(); //SYNC_SETTINGS_OOF + } + + //set device passwort + if (isset($devicepassword)) { + self::$encoder->startTag(SYNC_SETTINGS_DEVICEPW); + self::$encoder->startTag(SYNC_SETTINGS_SET); + self::$encoder->startTag(SYNC_SETTINGS_STATUS); + self::$encoder->content($devicepassword->Status); + self::$encoder->endTag(); //SYNC_SETTINGS_STATUS + self::$encoder->endTag(); //SYNC_SETTINGS_SET + self::$encoder->endTag(); //SYNC_SETTINGS_DEVICEPW + } + + //set device information + if (isset($deviceinformation)) { + self::$encoder->startTag(SYNC_SETTINGS_DEVICEINFORMATION); + self::$encoder->startTag(SYNC_SETTINGS_STATUS); + self::$encoder->content($deviceinformation->Status); + self::$encoder->endTag(); //SYNC_SETTINGS_STATUS + self::$encoder->endTag(); //SYNC_SETTINGS_DEVICEINFORMATION + } + + + self::$encoder->endTag(); //SYNC_SETTINGS_SETTINGS + + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/request/sync.php b/sources/lib/request/sync.php new file mode 100644 index 0000000..6317f1e --- /dev/null +++ b/sources/lib/request/sync.php @@ -0,0 +1,1222 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class Sync extends RequestProcessor { + // Ignored SMS identifier + const ZPUSHIGNORESMS = "ZPISMS"; + private $importer; + + /** + * Handles the Sync command + * Performs the synchronization of messages + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + // Contains all requested folders (containers) + $sc = new SyncCollections(); + $status = SYNC_STATUS_SUCCESS; + $wbxmlproblem = false; + $emptysync = false; + + // Start Synchronize + if(self::$decoder->getElementStartTag(SYNC_SYNCHRONIZE)) { + + // AS 1.0 sends version information in WBXML + if(self::$decoder->getElementStartTag(SYNC_VERSION)) { + $sync_version = self::$decoder->getElementContent(); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("WBXML sync version: '%s'", $sync_version)); + if(!self::$decoder->getElementEndTag()) + return false; + } + + // Synching specified folders + // Android still sends heartbeat sync even if all syncfolders are disabled. + // Check if Folders tag is empty () and only sync if there are + // some folders in the request. See ZP-172 + $startTag = self::$decoder->getElementStartTag(SYNC_FOLDERS); + if(isset($startTag[EN_FLAGS]) && $startTag[EN_FLAGS]) { + while(self::$decoder->getElementStartTag(SYNC_FOLDER)) { + $actiondata = array(); + $actiondata["requested"] = true; + $actiondata["clientids"] = array(); + $actiondata["modifyids"] = array(); + $actiondata["removeids"] = array(); + $actiondata["fetchids"] = array(); + $actiondata["statusids"] = array(); + + // read class, synckey and folderid without SyncParameters Object for now + $class = $synckey = $folderid = false; + + //for AS versions < 2.5 + if(self::$decoder->getElementStartTag(SYNC_FOLDERTYPE)) { + $class = self::$decoder->getElementContent(); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Sync folder: '%s'", $class)); + + if(!self::$decoder->getElementEndTag()) + return false; + } + + // SyncKey + if(self::$decoder->getElementStartTag(SYNC_SYNCKEY)) { + $synckey = "0"; + if (($synckey = self::$decoder->getElementContent()) !== false) { + if(!self::$decoder->getElementEndTag()) { + return false; + } + } + } + else + return false; + + // FolderId + if(self::$decoder->getElementStartTag(SYNC_FOLDERID)) { + $folderid = self::$decoder->getElementContent(); + + if(!self::$decoder->getElementEndTag()) + return false; + } + + // compatibility mode AS 1.0 - get folderid which was sent during GetHierarchy() + if (! $folderid && $class) { + $folderid = self::$deviceManager->GetFolderIdFromCacheByClass($class); + } + + // folderid HAS TO BE known by now, so we retrieve the correct SyncParameters object for an update + try { + $spa = self::$deviceManager->GetStateManager()->GetSynchedFolderState($folderid); + + // TODO remove resync of folders for < Z-Push 2 beta4 users + // this forces a resync of all states previous to Z-Push 2 beta4 + if (! $spa instanceof SyncParameters) + throw new StateInvalidException("Saved state are not of type SyncParameters"); + + // new/resync requested + if ($synckey == "0") + $spa->RemoveSyncKey(); + else if ($synckey !== false) + $spa->SetSyncKey($synckey); + } + catch (StateInvalidException $stie) { + $spa = new SyncParameters(); + $status = SYNC_STATUS_INVALIDSYNCKEY; + self::$topCollector->AnnounceInformation("State invalid - Resync folder", true); + self::$deviceManager->ForceFolderResync($folderid); + } + + // update folderid.. this might be a new object + $spa->SetFolderId($folderid); + + if ($class !== false) + $spa->SetContentClass($class); + + // Get class for as versions >= 12.0 + if (! $spa->HasContentClass()) { + try { + $spa->SetContentClass(self::$deviceManager->GetFolderClassFromCacheByID($spa->GetFolderId())); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("GetFolderClassFromCacheByID from Device Manager: '%s' for id:'%s'", $spa->GetContentClass(), $spa->GetFolderId())); + } + catch (NoHierarchyCacheAvailableException $nhca) { + $status = SYNC_STATUS_FOLDERHIERARCHYCHANGED; + self::$deviceManager->ForceFullResync(); + } + } + + // done basic SPA initialization/loading -> add to SyncCollection + $sc->AddCollection($spa); + $sc->AddParameter($spa, "requested", true); + + if ($spa->HasContentClass()) + self::$topCollector->AnnounceInformation(sprintf("%s request", $spa->GetContentClass()), true); + else + ZLog::Write(LOGLEVEL_WARN, "Not possible to determine class of request. Request did not contain class and apparently there is an issue with the HierarchyCache."); + + // SUPPORTED properties + if(($se = self::$decoder->getElementStartTag(SYNC_SUPPORTED)) !== false) { + // ZP-481: LG phones send an empty supported tag, so only read the contents if available here + // if is received, it's as no supported fields would have been sent at all. + // unsure if this is the correct approach, or if in this case some default list should be used + if ($se[EN_FLAGS] & EN_FLAGS_CONTENT) { + $supfields = array(); + while(1) { + $el = self::$decoder->getElement(); + + if($el[EN_TYPE] == EN_TYPE_ENDTAG) + break; + else + $supfields[] = $el[EN_TAG]; + } + self::$deviceManager->SetSupportedFields($spa->GetFolderId(), $supfields); + } + } + + // Deletes as moves can be an empty tag as well as have value + if(self::$decoder->getElementStartTag(SYNC_DELETESASMOVES)) { + $spa->SetDeletesAsMoves(true); + if (($dam = self::$decoder->getElementContent()) !== false) { + $spa->SetDeletesAsMoves((boolean)$dam); + if(!self::$decoder->getElementEndTag()) { + return false; + } + } + } + + // Get changes can be an empty tag as well as have value + // code block partly contributed by dw2412 + if(self::$decoder->getElementStartTag(SYNC_GETCHANGES)) { + $sc->AddParameter($spa, "getchanges", true); + if (($gc = self::$decoder->getElementContent()) !== false) { + $sc->AddParameter($spa, "getchanges", $gc); + if(!self::$decoder->getElementEndTag()) { + return false; + } + } + } + + if(self::$decoder->getElementStartTag(SYNC_WINDOWSIZE)) { + $ws = self::$decoder->getElementContent(); + // normalize windowsize - see ZP-477 + if ($ws == 0 || $ws > 512) + $ws = 512; + + $spa->SetWindowSize($ws); + + // also announce the currently requested window size to the DeviceManager + self::$deviceManager->SetWindowSize($spa->GetFolderId(), $spa->GetWindowSize()); + + if(!self::$decoder->getElementEndTag()) + return false; + } + + // conversation mode requested + if(self::$decoder->getElementStartTag(SYNC_CONVERSATIONMODE)) { + $spa->SetConversationMode(true); + if(($conversationmode = self::$decoder->getElementContent()) !== false) { + $spa->SetConversationMode((boolean)$conversationmode); + if(!self::$decoder->getElementEndTag()) + return false; + } + } + + // Do not truncate by default + $spa->SetTruncation(SYNC_TRUNCATION_ALL); + + // use default conflict handling if not specified by the mobile + $spa->SetConflict(SYNC_CONFLICT_DEFAULT); + + while(self::$decoder->getElementStartTag(SYNC_OPTIONS)) { + $firstOption = true; + while(1) { + // foldertype definition + if(self::$decoder->getElementStartTag(SYNC_FOLDERTYPE)) { + $foldertype = self::$decoder->getElementContent(); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): specified options block with foldertype '%s'", $foldertype)); + + // switch the foldertype for the next options + $spa->UseCPO($foldertype); + + // set to synchronize all changes. The mobile could overwrite this value + $spa->SetFilterType(SYNC_FILTERTYPE_ALL); + + if(!self::$decoder->getElementEndTag()) + return false; + } + // if no foldertype is defined, use default cpo + else if ($firstOption){ + $spa->UseCPO(); + // set to synchronize all changes. The mobile could overwrite this value + $spa->SetFilterType(SYNC_FILTERTYPE_ALL); + } + $firstOption = false; + + if(self::$decoder->getElementStartTag(SYNC_FILTERTYPE)) { + $spa->SetFilterType(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + if(self::$decoder->getElementStartTag(SYNC_TRUNCATION)) { + $spa->SetTruncation(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + if(self::$decoder->getElementStartTag(SYNC_RTFTRUNCATION)) { + $spa->SetRTFTruncation(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_MIMESUPPORT)) { + $spa->SetMimeSupport(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_MIMETRUNCATION)) { + $spa->SetMimeTruncation(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_CONFLICT)) { + $spa->SetConflict(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + while (self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_BODYPREFERENCE)) { + if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_TYPE)) { + $bptype = self::$decoder->getElementContent(); + $spa->BodyPreference($bptype); + if(!self::$decoder->getElementEndTag()) { + return false; + } + } + + if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_TRUNCATIONSIZE)) { + $spa->BodyPreference($bptype)->SetTruncationSize(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_ALLORNONE)) { + $spa->BodyPreference($bptype)->SetAllOrNone(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_PREVIEW)) { + $spa->BodyPreference($bptype)->SetPreview(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) + return false; + } + + if(!self::$decoder->getElementEndTag()) + return false; + } + + $e = self::$decoder->peek(); + if($e[EN_TYPE] == EN_TYPE_ENDTAG) { + self::$decoder->getElementEndTag(); + break; + } + } + } + + // limit items to be synchronized to the mobiles if configured + if (defined('SYNC_FILTERTIME_MAX') && SYNC_FILTERTIME_MAX > SYNC_FILTERTYPE_ALL && + (!$spa->HasFilterType() || $spa->GetFilterType() == SYNC_FILTERTYPE_ALL || $spa->GetFilterType() > SYNC_FILTERTIME_MAX)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SYNC_FILTERTIME_MAX defined. Filter set to value: %s", SYNC_FILTERTIME_MAX)); + $spa->SetFilterType(SYNC_FILTERTIME_MAX); + } + + // Check if the hierarchycache is available. If not, trigger a HierarchySync + if (self::$deviceManager->IsHierarchySyncRequired()) { + $status = SYNC_STATUS_FOLDERHIERARCHYCHANGED; + ZLog::Write(LOGLEVEL_DEBUG, "HierarchyCache is also not available. Triggering HierarchySync to device"); + } + + if(($el = self::$decoder->getElementStartTag(SYNC_PERFORM)) && ($el[EN_FLAGS] & EN_FLAGS_CONTENT)) { + // We can not proceed here as the content class is unknown + if ($status != SYNC_STATUS_SUCCESS) { + ZLog::Write(LOGLEVEL_WARN, "Ignoring all incoming actions as global status indicates problem."); + $wbxmlproblem = true; + break; + } + + $performaction = true; + + // unset the importer + $this->importer = false; + + $nchanges = 0; + while(1) { + // ADD, MODIFY, REMOVE or FETCH + $element = self::$decoder->getElement(); + + if($element[EN_TYPE] != EN_TYPE_STARTTAG) { + self::$decoder->ungetElement($element); + break; + } + + if ($status == SYNC_STATUS_SUCCESS) + $nchanges++; + + // Foldertype sent when synching SMS + if(self::$decoder->getElementStartTag(SYNC_FOLDERTYPE)) { + $foldertype = self::$decoder->getElementContent(); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): incoming data with foldertype '%s'", $foldertype)); + + if(!self::$decoder->getElementEndTag()) + return false; + } + else + $foldertype = false; + + $serverid = false; + if(self::$decoder->getElementStartTag(SYNC_SERVERENTRYID)) { + if (($serverid = self::$decoder->getElementContent()) !== false) { + if(!self::$decoder->getElementEndTag()) { // end serverid + return false; + } + } + } + + if(self::$decoder->getElementStartTag(SYNC_CLIENTENTRYID)) { + $clientid = self::$decoder->getElementContent(); + + if(!self::$decoder->getElementEndTag()) // end clientid + return false; + } + else + $clientid = false; + + // Get the SyncMessage if sent + if(self::$decoder->getElementStartTag(SYNC_DATA)) { + $message = ZPush::getSyncObjectFromFolderClass($spa->GetContentClass()); + $message->Decode(self::$decoder); + + // set Ghosted fields + $message->emptySupported(self::$deviceManager->GetSupportedFields($spa->GetFolderId())); + if(!self::$decoder->getElementEndTag()) // end applicationdata + return false; + } + else + $message = false; + + switch($element[EN_TAG]) { + case SYNC_FETCH: + array_push($actiondata["fetchids"], $serverid); + break; + default: + // get the importer + if ($this->importer == false) + $status = $this->getImporter($sc, $spa, $actiondata); + + if ($status == SYNC_STATUS_SUCCESS) + $this->importMessage($spa, $actiondata, $element[EN_TAG], $message, $clientid, $serverid, $foldertype, $nchanges); + else + ZLog::Write(LOGLEVEL_WARN, "Ignored incoming change, global status indicates problem."); + + break; + } + + if ($actiondata["fetchids"]) + self::$topCollector->AnnounceInformation(sprintf("Fetching %d", $nchanges)); + else + self::$topCollector->AnnounceInformation(sprintf("Incoming %d", $nchanges)); + + if(!self::$decoder->getElementEndTag()) // end add/change/delete/move + return false; + } + + if ($status == SYNC_STATUS_SUCCESS && $this->importer !== false) { + ZLog::Write(LOGLEVEL_INFO, sprintf("Processed '%d' incoming changes", $nchanges)); + if (!$actiondata["fetchids"]) + self::$topCollector->AnnounceInformation(sprintf("%d incoming", $nchanges), true); + + try { + // Save the updated state, which is used for the exporter later + $sc->AddParameter($spa, "state", $this->importer->GetState()); + } + catch (StatusException $stex) { + $status = $stex->getCode(); + } + } + + if(!self::$decoder->getElementEndTag()) // end PERFORM + return false; + } + + // save the failsave state + if (!empty($actiondata["statusids"])) { + unset($actiondata["failstate"]); + $actiondata["failedsyncstate"] = $sc->GetParameter($spa, "state"); + self::$deviceManager->GetStateManager()->SetSyncFailState($actiondata); + } + + // save actiondata + $sc->AddParameter($spa, "actiondata", $actiondata); + + if(!self::$decoder->getElementEndTag()) // end collection + return false; + + // AS14 does not send GetChanges anymore. We should do it if there were no incoming changes + if (!isset($performaction) && !$sc->GetParameter($spa, "getchanges") && $spa->HasSyncKey()) + $sc->AddParameter($spa, "getchanges", true); + } // END FOLDER + + if(!$wbxmlproblem && !self::$decoder->getElementEndTag()) // end collections + return false; + } // end FOLDERS + + if (self::$decoder->getElementStartTag(SYNC_HEARTBEATINTERVAL)) { + $hbinterval = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) // SYNC_HEARTBEATINTERVAL + return false; + } + + if (self::$decoder->getElementStartTag(SYNC_WAIT)) { + $wait = self::$decoder->getElementContent(); + if(!self::$decoder->getElementEndTag()) // SYNC_WAIT + return false; + + // internally the heartbeat interval and the wait time are the same + // heartbeat is in seconds, wait in minutes + $hbinterval = $wait * 60; + } + + if (self::$decoder->getElementStartTag(SYNC_WINDOWSIZE)) { + $sc->SetGlobalWindowSize(self::$decoder->getElementContent()); + if(!self::$decoder->getElementEndTag()) // SYNC_WINDOWSIZE + return false; + } + + if(self::$decoder->getElementStartTag(SYNC_PARTIAL)) + $partial = true; + else + $partial = false; + + if(!$wbxmlproblem && !self::$decoder->getElementEndTag()) // end sync + return false; + } + // we did not receive a SYNCHRONIZE block - assume empty sync + else { + $emptysync = true; + } + // END SYNCHRONIZE + + // check heartbeat/wait time + if (isset($hbinterval)) { + if ($hbinterval < 60 || $hbinterval > 3540) { + $status = SYNC_STATUS_INVALIDWAITORHBVALUE; + ZLog::Write(LOGLEVEL_WARN, sprintf("HandleSync(): Invalid heartbeat or wait value '%s'", $hbinterval)); + } + } + + // Partial & Empty Syncs need saved data to proceed with synchronization + if ($status == SYNC_STATUS_SUCCESS && ($emptysync === true || $partial === true) ) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): Partial or Empty sync requested. Retrieving data of synchronized folders.")); + + // Load all collections - do not overwrite existing (received!), load states and check permissions + try { + $sc->LoadAllCollections(false, true, true); + } + catch (StateNotFoundException $snfex) { + $status = SYNC_STATUS_INVALIDSYNCKEY; + self::$topCollector->AnnounceInformation("StateNotFoundException", true); + } + catch (StatusException $stex) { + $status = SYNC_STATUS_FOLDERHIERARCHYCHANGED; + self::$topCollector->AnnounceInformation(sprintf("StatusException code: %d", $status), true); + } + + // update a few values + foreach($sc as $folderid => $spa) { + // manually set getchanges parameter for this collection + $sc->AddParameter($spa, "getchanges", true); + + // set new global windowsize without marking the SPA as changed + if ($sc->GetGlobalWindowSize()) + $spa->SetWindowSize($sc->GetGlobalWindowSize(), false); + + // announce WindowSize to DeviceManager + self::$deviceManager->SetWindowSize($folderid, $spa->GetWindowSize()); + } + if (!$sc->HasCollections()) + $status = SYNC_STATUS_SYNCREQUESTINCOMPLETE; + } + + // HEARTBEAT & Empty sync + if ($status == SYNC_STATUS_SUCCESS && (isset($hbinterval) || $emptysync == true)) { + $interval = (defined('PING_INTERVAL') && PING_INTERVAL > 0) ? PING_INTERVAL : 30; + + if (isset($hbinterval)) + $sc->SetLifetime($hbinterval); + + // states are lazy loaded - we have to make sure that they are there! + $loadstatus = SYNC_STATUS_SUCCESS; + foreach($sc as $folderid => $spa) { + // some androids do heartbeat on the OUTBOX folder, with weird results - ZP-362 + // we do not load the state so we will never get relevant changes on the OUTBOX folder + if (self::$deviceManager->GetFolderTypeFromCacheById($folderid) == SYNC_FOLDER_TYPE_OUTBOX) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): Heartbeat on Outbox folder not allowed")); + continue; + } + + $fad = array(); + // if loading the states fails, we do not enter heartbeat, but we keep $status on SYNC_STATUS_SUCCESS + // so when the changes are exported the correct folder gets an SYNC_STATUS_INVALIDSYNCKEY + if ($loadstatus == SYNC_STATUS_SUCCESS) + $loadstatus = $this->loadStates($sc, $spa, $fad); + } + + if ($loadstatus == SYNC_STATUS_SUCCESS) { + $foundchanges = false; + + try { + // always check for changes + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): Entering Heartbeat mode")); + $foundchanges = $sc->CheckForChanges($sc->GetLifetime(), $interval); + } + catch (StatusException $stex) { + if ($stex->getCode() == SyncCollections::OBSOLETE_CONNECTION) { + $status = SYNC_COMMONSTATUS_SYNCSTATEVERSIONINVALID; + } + else { + $status = SYNC_STATUS_FOLDERHIERARCHYCHANGED; + self::$topCollector->AnnounceInformation(sprintf("StatusException code: %d", $status), true); + } + } + + // in case there are no changes, we can reply with an empty response + if (!$foundchanges && $status == SYNC_STATUS_SUCCESS){ + ZLog::Write(LOGLEVEL_DEBUG, "No changes found. Replying with empty response and closing connection."); + self::$specialHeaders = array(); + self::$specialHeaders[] = "Connection: close"; + return true; + } + + if ($foundchanges) { + foreach ($sc->GetChangedFolderIds() as $folderid => $changecount) { + // check if there were other sync requests for a folder during the heartbeat + $spa = $sc->GetCollection($folderid); + if ($changecount > 0 && $sc->WaitedForChanges() && self::$deviceManager->CheckHearbeatStateIntegrity($spa->GetFolderId(), $spa->GetUuid(), $spa->GetUuidCounter())) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): heartbeat: found %d changes in '%s' which was already synchronized. Heartbeat aborted!", $changecount, $folderid)); + $status = SYNC_COMMONSTATUS_SYNCSTATEVERSIONINVALID; + } + else + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): heartbeat: found %d changes in '%s'", $changecount, $folderid)); + } + } + } + } + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): Start Output")); + + // Start the output + self::$encoder->startWBXML(); + self::$encoder->startTag(SYNC_SYNCHRONIZE); + { + // global status + // SYNC_COMMONSTATUS_* start with values from 101 + if ($status != SYNC_COMMONSTATUS_SUCCESS && $status > 100) { + self::$encoder->startTag(SYNC_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); + } + else { + self::$encoder->startTag(SYNC_FOLDERS); + { + foreach($sc as $folderid => $spa) { + // get actiondata + $actiondata = $sc->GetParameter($spa, "actiondata"); + + if ($status == SYNC_STATUS_SUCCESS && (!$spa->GetContentClass() || !$spa->GetFolderId())) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("HandleSync(): no content class or folderid found for collection.")); + continue; + } + + if (! $sc->GetParameter($spa, "requested")) + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): partial sync for folder class '%s' with id '%s'", $spa->GetContentClass(), $spa->GetFolderId())); + + // initialize exporter to get changecount + $changecount = false; + if (isset($exporter)) + unset($exporter); + + // TODO we could check against $sc->GetChangedFolderIds() on heartbeat so we do not need to configure all exporter again + if($status == SYNC_STATUS_SUCCESS && ($sc->GetParameter($spa, "getchanges") || ! $spa->HasSyncKey())) { + + //make sure the states are loaded + $status = $this->loadStates($sc, $spa, $actiondata); + + if($status == SYNC_STATUS_SUCCESS) { + try { + // if this is an additional folder the backend has to be setup correctly + if (!self::$backend->Setup(ZPush::GetAdditionalSyncFolderStore($spa->GetFolderId()))) + throw new StatusException(sprintf("HandleSync() could not Setup() the backend for folder id '%s'", $spa->GetFolderId()), SYNC_STATUS_FOLDERHIERARCHYCHANGED); + + // Use the state from the importer, as changes may have already happened + $exporter = self::$backend->GetExporter($spa->GetFolderId()); + + if ($exporter === false) + throw new StatusException(sprintf("HandleSync() could not get an exporter for folder id '%s'", $spa->GetFolderId()), SYNC_STATUS_FOLDERHIERARCHYCHANGED); + } + catch (StatusException $stex) { + $status = $stex->getCode(); + } + try { + // Stream the messages directly to the PDA + $streamimporter = new ImportChangesStream(self::$encoder, ZPush::getSyncObjectFromFolderClass($spa->GetContentClass())); + + if ($exporter !== false) { + $exporter->Config($sc->GetParameter($spa, "state")); + $exporter->ConfigContentParameters($spa->GetCPO()); + $exporter->InitializeExporter($streamimporter); + + $changecount = $exporter->GetChangeCount(); + } + } + catch (StatusException $stex) { + if ($stex->getCode() === SYNC_FSSTATUS_CODEUNKNOWN && $spa->HasSyncKey()) + $status = SYNC_STATUS_INVALIDSYNCKEY; + else + $status = $stex->getCode(); + } + + if (! $spa->HasSyncKey()) { + self::$topCollector->AnnounceInformation(sprintf("Exporter registered. %d objects queued.", $changecount), true); + // update folder status as initialized + $spa->SetFolderSyncTotal($changecount); + $spa->SetFolderSyncRemaining($changecount); + if ($changecount > 0) { + self::$deviceManager->SetFolderSyncStatus($folderid, DeviceManager::FLD_SYNC_INITIALIZED); + } + } + else if ($status != SYNC_STATUS_SUCCESS) + self::$topCollector->AnnounceInformation(sprintf("StatusException code: %d", $status), true); + + } + } + + if (isset($hbinterval) && $changecount == 0 && $status == SYNC_STATUS_SUCCESS) { + ZLog::Write(LOGLEVEL_DEBUG, "No changes found for heartbeat folder. Omitting empty output."); + continue; + } + + // Get a new sync key to output to the client if any changes have been send or will are available + if (!empty($actiondata["modifyids"]) || + !empty($actiondata["clientids"]) || + !empty($actiondata["removeids"]) || + $changecount > 0 || (! $spa->HasSyncKey() && $status == SYNC_STATUS_SUCCESS)) + $spa->SetNewSyncKey(self::$deviceManager->GetStateManager()->GetNewSyncKey($spa->GetSyncKey())); + + self::$encoder->startTag(SYNC_FOLDER); + + if($spa->HasContentClass()) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Folder type: %s", $spa->GetContentClass())); + // AS 12.0 devices require content class + if (Request::GetProtocolVersion() < 12.1) { + self::$encoder->startTag(SYNC_FOLDERTYPE); + self::$encoder->content($spa->GetContentClass()); + self::$encoder->endTag(); + } + } + + self::$encoder->startTag(SYNC_SYNCKEY); + if($status == SYNC_STATUS_SUCCESS && $spa->HasNewSyncKey()) + self::$encoder->content($spa->GetNewSyncKey()); + else + self::$encoder->content($spa->GetSyncKey()); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_FOLDERID); + self::$encoder->content($spa->GetFolderId()); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); + + // announce failing status to the process loop detection + if ($status !== SYNC_STATUS_SUCCESS) + self::$deviceManager->AnnounceProcessStatus($spa->GetFolderId(), $status); + + // Output IDs and status for incoming items & requests + if($status == SYNC_STATUS_SUCCESS && ( + !empty($actiondata["clientids"]) || + !empty($actiondata["modifyids"]) || + !empty($actiondata["removeids"]) || + !empty($actiondata["fetchids"]) )) { + + self::$encoder->startTag(SYNC_REPLIES); + // output result of all new incoming items + foreach($actiondata["clientids"] as $clientid => $serverid) { + self::$encoder->startTag(SYNC_ADD); + self::$encoder->startTag(SYNC_CLIENTENTRYID); + self::$encoder->content($clientid); + self::$encoder->endTag(); + if ($serverid) { + self::$encoder->startTag(SYNC_SERVERENTRYID); + self::$encoder->content($serverid); + self::$encoder->endTag(); + } + self::$encoder->startTag(SYNC_STATUS); + self::$encoder->content((isset($actiondata["statusids"][$clientid])?$actiondata["statusids"][$clientid]:SYNC_STATUS_CLIENTSERVERCONVERSATIONERROR)); + self::$encoder->endTag(); + self::$encoder->endTag(); + } + + // loop through modify operations which were not a success, send status + foreach($actiondata["modifyids"] as $serverid) { + if (isset($actiondata["statusids"][$serverid]) && $actiondata["statusids"][$serverid] !== SYNC_STATUS_SUCCESS) { + self::$encoder->startTag(SYNC_MODIFY); + self::$encoder->startTag(SYNC_SERVERENTRYID); + self::$encoder->content($serverid); + self::$encoder->endTag(); + self::$encoder->startTag(SYNC_STATUS); + self::$encoder->content($actiondata["statusids"][$serverid]); + self::$encoder->endTag(); + self::$encoder->endTag(); + } + } + + // loop through remove operations which were not a success, send status + foreach($actiondata["removeids"] as $serverid) { + if (isset($actiondata["statusids"][$serverid]) && $actiondata["statusids"][$serverid] !== SYNC_STATUS_SUCCESS) { + self::$encoder->startTag(SYNC_REMOVE); + self::$encoder->startTag(SYNC_SERVERENTRYID); + self::$encoder->content($serverid); + self::$encoder->endTag(); + self::$encoder->startTag(SYNC_STATUS); + self::$encoder->content($actiondata["statusids"][$serverid]); + self::$encoder->endTag(); + self::$encoder->endTag(); + } + } + + if (!empty($actiondata["fetchids"])) + self::$topCollector->AnnounceInformation(sprintf("Fetching %d objects ", count($actiondata["fetchids"])), true); + + foreach($actiondata["fetchids"] as $id) { + $data = false; + try { + $fetchstatus = SYNC_STATUS_SUCCESS; + + // if this is an additional folder the backend has to be setup correctly + if (!self::$backend->Setup(ZPush::GetAdditionalSyncFolderStore($spa->GetFolderId()))) + throw new StatusException(sprintf("HandleSync(): could not Setup() the backend to fetch in folder id '%s'", $spa->GetFolderId()), SYNC_STATUS_OBJECTNOTFOUND); + + $data = self::$backend->Fetch($spa->GetFolderId(), $id, $spa->GetCPO()); + + // check if the message is broken + if (ZPush::GetDeviceManager(false) && ZPush::GetDeviceManager()->DoNotStreamMessage($id, $data)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): message not to be streamed as requested by DeviceManager.", $id)); + $fetchstatus = SYNC_STATUS_CLIENTSERVERCONVERSATIONERROR; + } + } + catch (StatusException $stex) { + $fetchstatus = $stex->getCode(); + } + + self::$encoder->startTag(SYNC_FETCH); + self::$encoder->startTag(SYNC_SERVERENTRYID); + self::$encoder->content($id); + self::$encoder->endTag(); + + self::$encoder->startTag(SYNC_STATUS); + self::$encoder->content($fetchstatus); + self::$encoder->endTag(); + + if($data !== false && $status == SYNC_STATUS_SUCCESS) { + self::$encoder->startTag(SYNC_DATA); + $data->Encode(self::$encoder); + self::$encoder->endTag(); + } + else + ZLog::Write(LOGLEVEL_WARN, sprintf("Unable to Fetch '%s'", $id)); + self::$encoder->endTag(); + + } + self::$encoder->endTag(); + } + + if($sc->GetParameter($spa, "getchanges") && $spa->HasFolderId() && $spa->HasContentClass() && $spa->HasSyncKey()) { + $windowSize = self::$deviceManager->GetWindowSize($spa->GetFolderId(), $spa->GetContentClass(), $spa->GetUuid(), $spa->GetUuidCounter(), $changecount); + + if($changecount > $windowSize) { + self::$encoder->startTag(SYNC_MOREAVAILABLE, false, true); + } + } + + // Stream outgoing changes + if($status == SYNC_STATUS_SUCCESS && $sc->GetParameter($spa, "getchanges") == true && $windowSize > 0) { + self::$topCollector->AnnounceInformation(sprintf("Streaming data of %d objects", (($changecount > $windowSize)?$windowSize:$changecount))); + + // Output message changes per folder + self::$encoder->startTag(SYNC_PERFORM); + + $n = 0; + while(1) { + try { + $progress = $exporter->Synchronize(); + if(!is_array($progress)) + break; + $n++; + } + catch (SyncObjectBrokenException $mbe) { + $brokenSO = $mbe->GetSyncObject(); + if (!$brokenSO) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("HandleSync(): Catched SyncObjectBrokenException but broken SyncObject not available. This should be fixed in the backend.")); + } + else { + if (!isset($brokenSO->id)) { + $brokenSO->id = "Unknown ID"; + ZLog::Write(LOGLEVEL_ERROR, sprintf("HandleSync(): Catched SyncObjectBrokenException but no ID of object set. This should be fixed in the backend.")); + } + self::$deviceManager->AnnounceIgnoredMessage($spa->GetFolderId(), $brokenSO->id, $brokenSO); + } + } + + if($n >= $windowSize) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): Exported maxItems of messages: %d / %d", $n, $changecount)); + break; + } + } + + // $progress is not an array when exporting the last message + // so we get the number to display from the streamimporter + if (isset($streamimporter)) { + $n = $streamimporter->GetImportedMessages(); + } + + self::$encoder->endTag(); + self::$topCollector->AnnounceInformation(sprintf("Outgoing %d objects%s", $n, ($n >= $windowSize)?" of ".$changecount:""), true); + + // update folder status + $spa->SetFolderSyncRemaining($changecount); + // changecount is initialized with 'false', so 0 means no changes! + if ($changecount === 0 || ($changecount !== false && $changecount <= $windowSize)) + self::$deviceManager->SetFolderSyncStatus($folderid, DeviceManager::FLD_SYNC_COMPLETED); + else + self::$deviceManager->SetFolderSyncStatus($folderid, DeviceManager::FLD_SYNC_INPROGRESS); + } + + self::$encoder->endTag(); + + // Save the sync state for the next time + if($spa->HasNewSyncKey()) { + self::$topCollector->AnnounceInformation("Saving state"); + + try { + if (isset($exporter) && $exporter) + $state = $exporter->GetState(); + + // nothing exported, but possibly imported - get the importer state + else if ($sc->GetParameter($spa, "state") !== null) + $state = $sc->GetParameter($spa, "state"); + + // if a new request without state information (hierarchy) save an empty state + else if (! $spa->HasSyncKey()) + $state = ""; + } + catch (StatusException $stex) { + $status = $stex->getCode(); + } + + + if (isset($state) && $status == SYNC_STATUS_SUCCESS) + self::$deviceManager->GetStateManager()->SetSyncState($spa->GetNewSyncKey(), $state, $spa->GetFolderId()); + else + ZLog::Write(LOGLEVEL_ERROR, sprintf("HandleSync(): error saving '%s' - no state information available", $spa->GetNewSyncKey())); + } + + // reset status for the next folder + $status = SYNC_STATUS_SUCCESS; + + // save SyncParameters + if ($status == SYNC_STATUS_SUCCESS && empty($actiondata["fetchids"])) + $sc->SaveCollection($spa); + + } // END foreach collection + } + self::$encoder->endTag(); //SYNC_FOLDERS + } + } + self::$encoder->endTag(); //SYNC_SYNCHRONIZE + + return true; + } + + /** + * Loads the states and writes them into the SyncCollection Object and the actiondata failstate + * + * @param SyncCollection $sc SyncCollection object + * @param SyncParameters $spa SyncParameters object + * @param array $actiondata Actiondata array + * @param boolean $loadFailsave (opt) default false - indicates if the failsave states should be loaded + * + * @access private + * @return status indicating if there were errors. If no errors, status is SYNC_STATUS_SUCCESS + */ + private function loadStates($sc, $spa, &$actiondata, $loadFailsave = false) { + $status = SYNC_STATUS_SUCCESS; + + if ($sc->GetParameter($spa, "state") == null) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Sync->loadStates(): loading states for folder '%s'",$spa->GetFolderId())); + + try { + $sc->AddParameter($spa, "state", self::$deviceManager->GetStateManager()->GetSyncState($spa->GetSyncKey())); + + if ($loadFailsave) { + // if this request was made before, there will be a failstate available + $actiondata["failstate"] = self::$deviceManager->GetStateManager()->GetSyncFailState(); + } + + // if this is an additional folder the backend has to be setup correctly + if (!self::$backend->Setup(ZPush::GetAdditionalSyncFolderStore($spa->GetFolderId()))) + throw new StatusException(sprintf("HandleSync() could not Setup() the backend for folder id '%s'", $spa->GetFolderId()), SYNC_STATUS_FOLDERHIERARCHYCHANGED); + } + catch (StateNotFoundException $snfex) { + $status = SYNC_STATUS_INVALIDSYNCKEY; + self::$topCollector->AnnounceInformation("StateNotFoundException", true); + } + catch (StatusException $stex) { + $status = $stex->getCode(); + self::$topCollector->AnnounceInformation(sprintf("StatusException code: %d", $status), true); + } + } + + return $status; + } + + /** + * Initializes the importer for the SyncParameters folder, loads necessary + * states (incl. failsave states) and initializes the conflict detection + * + * @param SyncCollection $sc SyncCollection object + * @param SyncParameters $spa SyncParameters object + * @param array $actiondata Actiondata array + * + * @access private + * @return status indicating if there were errors. If no errors, status is SYNC_STATUS_SUCCESS + */ + private function getImporter($sc, $spa, &$actiondata) { + ZLog::Write(LOGLEVEL_DEBUG, "Sync->getImporter(): initialize importer"); + $status = SYNC_STATUS_SUCCESS; + + // load the states with failsave data + $status = $this->loadStates($sc, $spa, $actiondata, true); + + try { + // Configure importer with last state + $this->importer = self::$backend->GetImporter($spa->GetFolderId()); + + // if something goes wrong, ask the mobile to resync the hierarchy + if ($this->importer === false) + throw new StatusException(sprintf("Sync->getImporter(): no importer for folder id '%s'", $spa->GetFolderId()), SYNC_STATUS_FOLDERHIERARCHYCHANGED); + + // if there is a valid state obtained after importing changes in a previous loop, we use that state + if (isset($actiondata["failstate"]) && isset($actiondata["failstate"]["failedsyncstate"])) { + $this->importer->Config($actiondata["failstate"]["failedsyncstate"], $spa->GetConflict()); + } + else + $this->importer->Config($sc->GetParameter($spa, "state"), $spa->GetConflict()); + + // the CPO is also needed by the importer to check if imported changes are inside the sync window - see ZP-258 + $this->importer->ConfigContentParameters($spa->GetCPO()); + } + catch (StatusException $stex) { + $status = $stex->getCode(); + } + + $this->importer->LoadConflicts($spa->GetCPO(), $sc->GetParameter($spa, "state")); + + return $status; + } + + /** + * Imports a message + * + * @param SyncParameters $spa SyncParameters object + * @param array $actiondata Actiondata array + * @param integer $todo WBXML flag indicating how message should be imported. + * Valid values: SYNC_ADD, SYNC_MODIFY, SYNC_REMOVE + * @param SyncObject $message SyncObject message to be imported + * @param string $clientid Client message identifier + * @param string $serverid Server message identifier + * @param string $foldertype On sms sync, this says "SMS", else false + * @param integer $messageCount Counter of already imported messages + * + * @access private + * @throws StatusException in case the importer is not available + * @return - Message related status are returned in the actiondata. + */ + private function importMessage($spa, &$actiondata, $todo, $message, $clientid, $serverid, $foldertype, $messageCount) { + // the importer needs to be available! + if ($this->importer == false) + throw StatusException(sprintf("Sync->importMessage(): importer not available", SYNC_STATUS_SERVERERROR)); + + // mark this state as used, e.g. for HeartBeat + self::$deviceManager->SetHeartbeatStateIntegrity($spa->GetFolderId(), $spa->GetUuid(), $spa->GetUuidCounter()); + + // Detect incoming loop + // messages which were created/removed before will not have the same action executed again + // if a message is edited we perform this action "again", as the message could have been changed on the mobile in the meantime + $ignoreMessage = false; + if ($actiondata["failstate"]) { + // message was ADDED before, do NOT add it again + if ($todo == SYNC_ADD && isset($actiondata["failstate"]["clientids"][$clientid])) { + $ignoreMessage = true; + + // make sure no messages are sent back + self::$deviceManager->SetWindowSize($spa->GetFolderId(), 0); + + $actiondata["clientids"][$clientid] = $actiondata["failstate"]["clientids"][$clientid]; + $actiondata["statusids"][$clientid] = $actiondata["failstate"]["statusids"][$clientid]; + + ZLog::Write(LOGLEVEL_WARN, sprintf("Mobile loop detected! Incoming new message '%s' was created on the server before. Replying with known new server id: %s", $clientid, $actiondata["clientids"][$clientid])); + } + + // message was REMOVED before, do NOT attemp to remove it again + if ($todo == SYNC_REMOVE && isset($actiondata["failstate"]["removeids"][$serverid])) { + $ignoreMessage = true; + + // make sure no messages are sent back + self::$deviceManager->SetWindowSize($spa->GetFolderId(), 0); + + $actiondata["removeids"][$serverid] = $actiondata["failstate"]["removeids"][$serverid]; + $actiondata["statusids"][$serverid] = $actiondata["failstate"]["statusids"][$serverid]; + + ZLog::Write(LOGLEVEL_WARN, sprintf("Mobile loop detected! Message '%s' was deleted by the mobile before. Replying with known status: %s", $clientid, $actiondata["statusids"][$serverid])); + } + } + + if (!$ignoreMessage) { + switch($todo) { + case SYNC_MODIFY: + self::$topCollector->AnnounceInformation(sprintf("Saving modified message %d", $messageCount)); + try { + $actiondata["modifyids"][] = $serverid; + + // ignore sms messages + if ($foldertype == "SMS" || stripos($serverid, self::ZPUSHIGNORESMS) !== false) { + ZLog::Write(LOGLEVEL_DEBUG, "SMS sync are not supported. Ignoring message."); + // TODO we should update the SMS + $actiondata["statusids"][$serverid] = SYNC_STATUS_SUCCESS; + } + // check incoming message without logging WARN messages about errors + else if (!($message instanceof SyncObject) || !$message->Check(true)) { + $actiondata["statusids"][$serverid] = SYNC_STATUS_CLIENTSERVERCONVERSATIONERROR; + } + else { + if(isset($message->read)) { + // Currently, 'read' is only sent by the PDA when it is ONLY setting the read flag. + $this->importer->ImportMessageReadFlag($serverid, $message->read); + } + elseif (!isset($message->flag)) { + $this->importer->ImportMessageChange($serverid, $message); + } + + // email todoflags - some devices send todos flags together with read flags, + // so they have to be handled separately + if (isset($message->flag)){ + $this->importer->ImportMessageChange($serverid, $message); + } + + $actiondata["statusids"][$serverid] = SYNC_STATUS_SUCCESS; + } + } + catch (StatusException $stex) { + $actiondata["statusids"][$serverid] = $stex->getCode(); + } + + break; + case SYNC_ADD: + self::$topCollector->AnnounceInformation(sprintf("Creating new message from mobile %d", $messageCount)); + try { + // ignore sms messages + if ($foldertype == "SMS") { + ZLog::Write(LOGLEVEL_DEBUG, "SMS sync are not supported. Ignoring message."); + // TODO we should create the SMS + // return a fake serverid which we can identify later + $actiondata["clientids"][$clientid] = self::ZPUSHIGNORESMS . $clientid; + $actiondata["statusids"][$clientid] = SYNC_STATUS_SUCCESS; + } + // check incoming message without logging WARN messages about errors + else if (!($message instanceof SyncObject) || !$message->Check(true)) { + $actiondata["clientids"][$clientid] = false; + $actiondata["statusids"][$clientid] = SYNC_STATUS_CLIENTSERVERCONVERSATIONERROR; + } + else { + $actiondata["clientids"][$clientid] = false; + $actiondata["clientids"][$clientid] = $this->importer->ImportMessageChange(false, $message); + $actiondata["statusids"][$clientid] = SYNC_STATUS_SUCCESS; + } + } + catch (StatusException $stex) { + $actiondata["statusids"][$clientid] = $stex->getCode(); + } + break; + case SYNC_REMOVE: + self::$topCollector->AnnounceInformation(sprintf("Deleting message removed on mobile %d", $messageCount)); + try { + $actiondata["removeids"][] = $serverid; + // ignore sms messages + if ($foldertype == "SMS" || stripos($serverid, self::ZPUSHIGNORESMS) !== false) { + ZLog::Write(LOGLEVEL_DEBUG, "SMS sync are not supported. Ignoring message."); + // TODO we should delete the SMS + $actiondata["statusids"][$serverid] = SYNC_STATUS_SUCCESS; + } + else { + // if message deletions are to be moved, move them + if($spa->GetDeletesAsMoves()) { + $folderid = self::$backend->GetWasteBasket(); + + if($folderid) { + $this->importer->ImportMessageMove($serverid, $folderid); + $actiondata["statusids"][$serverid] = SYNC_STATUS_SUCCESS; + break; + } + else + ZLog::Write(LOGLEVEL_WARN, "Message should be moved to WasteBasket, but the Backend did not return a destination ID. Message is hard deleted now!"); + } + + $this->importer->ImportMessageDeletion($serverid); + $actiondata["statusids"][$serverid] = SYNC_STATUS_SUCCESS; + } + } + catch (StatusException $stex) { + $actiondata["statusids"][$serverid] = $stex->getCode(); + } + break; + } + ZLog::Write(LOGLEVEL_DEBUG, "Sync->importMessage(): message imported"); + } + } +} + +?> \ No newline at end of file diff --git a/sources/lib/request/validatecert.php b/sources/lib/request/validatecert.php new file mode 100755 index 0000000..4a827ed --- /dev/null +++ b/sources/lib/request/validatecert.php @@ -0,0 +1,90 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class ValidateCert extends RequestProcessor { + + /** + * Handles the ValidateCert command + * + * @param int $commandCode + * + * @access public + * @return boolean + */ + public function Handle($commandCode) { + // Parse input + if(!self::$decoder->getElementStartTag(SYNC_VALIDATECERT_VALIDATECERT)) + return false; + + $validateCert = new SyncValidateCert(); + $validateCert->Decode(self::$decoder); + $cert_der = base64_decode($validateCert->certificates[0]); + $cert_pem = "-----BEGIN CERTIFICATE-----\n".chunk_split(base64_encode($cert_der), 64, "\n")."-----END CERTIFICATE-----\n"; + + $checkpurpose = (defined('CAINFO') && CAINFO) ? openssl_x509_checkpurpose($cert_pem, X509_PURPOSE_SMIME_SIGN, array(CAINFO)) : openssl_x509_checkpurpose($cert_pem, X509_PURPOSE_SMIME_SIGN); + if ($checkpurpose === true) + $status = SYNC_VALIDATECERTSTATUS_SUCCESS; + else + $status = SYNC_VALIDATECERTSTATUS_CANTVALIDATESIG; + + if(!self::$decoder->getElementEndTag()) + return false; // SYNC_VALIDATECERT_VALIDATECERT + + self::$encoder->startWBXML(); + self::$encoder->startTag(SYNC_VALIDATECERT_VALIDATECERT); + + self::$encoder->startTag(SYNC_VALIDATECERT_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); // SYNC_VALIDATECERT_STATUS + + self::$encoder->startTag(SYNC_VALIDATECERT_CERTIFICATE); + self::$encoder->startTag(SYNC_VALIDATECERT_STATUS); + self::$encoder->content($status); + self::$encoder->endTag(); // SYNC_VALIDATECERT_STATUS + self::$encoder->endTag(); // SYNC_VALIDATECERT_CERTIFICATE + + self::$encoder->endTag(); // SYNC_VALIDATECERT_VALIDATECERT + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncappointment.php b/sources/lib/syncobjects/syncappointment.php new file mode 100644 index 0000000..879469c --- /dev/null +++ b/sources/lib/syncobjects/syncappointment.php @@ -0,0 +1,222 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncAppointment extends SyncObject { + public $timezone; + public $dtstamp; + public $starttime; + public $subject; + public $uid; + public $organizername; + public $organizeremail; + public $location; + public $endtime; + public $recurrence; + public $sensitivity; + public $busystatus; + public $alldayevent; + public $reminder; + public $rtf; + public $meetingstatus; + public $attendees; + public $body; + public $bodytruncated; + public $exceptions; + public $deleted; + public $exceptionstarttime; + public $categories; + + // AS 12.0 props + public $asbody; + public $nativebodytype; + + // AS 14.0 props + public $disallownewtimeprop; + public $responsetype; + public $responserequested; + + + function SyncAppointment() { + $mapping = array( + SYNC_POOMCAL_TIMEZONE => array ( self::STREAMER_VAR => "timezone"), + + SYNC_POOMCAL_DTSTAMP => array ( self::STREAMER_VAR => "dtstamp", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE, + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO)), + + SYNC_POOMCAL_STARTTIME => array ( self::STREAMER_VAR => "starttime", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE, + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO, + self::STREAMER_CHECK_CMPLOWER => SYNC_POOMCAL_ENDTIME ) ), + + + SYNC_POOMCAL_SUBJECT => array ( self::STREAMER_VAR => "subject", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETEMPTY)), + + SYNC_POOMCAL_UID => array ( self::STREAMER_VAR => "uid"), + SYNC_POOMCAL_ORGANIZERNAME => array ( self::STREAMER_VAR => "organizername"), // verified below + SYNC_POOMCAL_ORGANIZEREMAIL => array ( self::STREAMER_VAR => "organizeremail"), // verified below + SYNC_POOMCAL_LOCATION => array ( self::STREAMER_VAR => "location"), + SYNC_POOMCAL_ENDTIME => array ( self::STREAMER_VAR => "endtime", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE, + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETONE, + self::STREAMER_CHECK_CMPHIGHER => SYNC_POOMCAL_STARTTIME ) ), + + SYNC_POOMCAL_RECURRENCE => array ( self::STREAMER_VAR => "recurrence", + self::STREAMER_TYPE => "SyncRecurrence"), + + // Sensitivity values + // 0 = Normal + // 1 = Personal + // 2 = Private + // 3 = Confident + SYNC_POOMCAL_SENSITIVITY => array ( self::STREAMER_VAR => "sensitivity", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2,3) )), + + // Busystatus values + // 0 = Free + // 1 = Tentative + // 2 = Busy + // 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) )), + + SYNC_POOMCAL_ALLDAYEVENT => array ( self::STREAMER_VAR => "alldayevent", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ZEROORONE => self::STREAMER_CHECK_SETZERO)), + + SYNC_POOMCAL_REMINDER => array ( self::STREAMER_VAR => "reminder", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => -1)), + + SYNC_POOMCAL_RTF => array ( self::STREAMER_VAR => "rtf"), + + // 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 + SYNC_POOMCAL_MEETINGSTATUS => array ( self::STREAMER_VAR => "meetingstatus", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1,3,5,7,9,11,13,15) )), + + SYNC_POOMCAL_ATTENDEES => array ( self::STREAMER_VAR => "attendees", + self::STREAMER_TYPE => "SyncAttendee", + self::STREAMER_ARRAY => SYNC_POOMCAL_ATTENDEE), + + SYNC_POOMCAL_BODY => array ( self::STREAMER_VAR => "body"), + SYNC_POOMCAL_BODYTRUNCATED => array ( self::STREAMER_VAR => "bodytruncated"), + SYNC_POOMCAL_EXCEPTIONS => array ( self::STREAMER_VAR => "exceptions", + self::STREAMER_TYPE => "SyncAppointmentException", + self::STREAMER_ARRAY => SYNC_POOMCAL_EXCEPTION), + + SYNC_POOMCAL_CATEGORIES => array ( self::STREAMER_VAR => "categories", + self::STREAMER_ARRAY => SYNC_POOMCAL_CATEGORY), + ); + + if (Request::GetProtocolVersion() >= 12.0) { + $mapping[SYNC_AIRSYNCBASE_BODY] = array ( self::STREAMER_VAR => "asbody", + self::STREAMER_TYPE => "SyncBaseBody"); + + $mapping[SYNC_AIRSYNCBASE_NATIVEBODYTYPE] = array ( self::STREAMER_VAR => "nativebodytype"); + + //unset these properties because airsyncbase body and attachments will be used instead + unset($mapping[SYNC_POOMCAL_BODY], $mapping[SYNC_POOMCAL_BODYTRUNCATED]); + } + + if(Request::GetProtocolVersion() >= 14.0) { + $mapping[SYNC_POOMCAL_DISALLOWNEWTIMEPROPOSAL] = array ( self::STREAMER_VAR => "disallownewtimeprop"); + $mapping[SYNC_POOMCAL_RESPONSEREQUESTED] = array ( self::STREAMER_VAR => "responserequested"); + $mapping[SYNC_POOMCAL_RESPONSETYPE] = array ( self::STREAMER_VAR => "responsetype"); + } + + parent::SyncObject($mapping); + } + + /** + * Method checks if the object has the minimum of required parameters + * and fullfills semantic dependencies + * + * This overloads the general check() with special checks to be executed + * Checks if SYNC_POOMCAL_ORGANIZERNAME and SYNC_POOMCAL_ORGANIZEREMAIL are correctly set + * + * @param boolean $logAsDebug (opt) default is false, so messages are logged in WARN log level + * + * @access public + * @return boolean + */ + public function Check($logAsDebug = false) { + $ret = parent::Check($logAsDebug); + + // semantic checks general "turn off switch" + if (defined("DO_SEMANTIC_CHECKS") && DO_SEMANTIC_CHECKS === false) + return $ret; + + if (!$ret) + return false; + + if ($this->meetingstatus > 0) { + if (!isset($this->organizername) || !isset($this->organizeremail)) { + ZLog::Write(LOGLEVEL_WARN, "SyncAppointment->Check(): Parameter 'organizername' and 'organizeremail' should be set for a meeting request"); + } + } + + // do not sync a recurrent appointment without a timezone + if (isset($this->recurrence) && !isset($this->timezone)) { + ZLog::Write(LOGLEVEL_ERROR, "SyncAppointment->Check(): timezone for a recurring appointment is not set."); + return false; + } + + return true; + } +} + +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncappointmentexception.php b/sources/lib/syncobjects/syncappointmentexception.php new file mode 100644 index 0000000..e8d95aa --- /dev/null +++ b/sources/lib/syncobjects/syncappointmentexception.php @@ -0,0 +1,78 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncAppointmentException extends SyncAppointment { + public $deleted; + public $exceptionstarttime; + + function SyncAppointmentException() { + parent::SyncAppointment(); + + $this->mapping += array( + SYNC_POOMCAL_DELETED => array ( self::STREAMER_VAR => "deleted", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ZEROORONE => self::STREAMER_CHECK_SETZERO)), + + SYNC_POOMCAL_EXCEPTIONSTARTTIME => array ( self::STREAMER_VAR => "exceptionstarttime", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE, + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETONE)), + ); + + // some parameters are not required in an exception, others are not allowed to be set in SyncAppointmentExceptions + $this->mapping[SYNC_POOMCAL_TIMEZONE][self::STREAMER_CHECKS] = array(); + $this->mapping[SYNC_POOMCAL_DTSTAMP][self::STREAMER_CHECKS] = array(); + $this->mapping[SYNC_POOMCAL_STARTTIME][self::STREAMER_CHECKS] = array(self::STREAMER_CHECK_CMPLOWER => SYNC_POOMCAL_ENDTIME); + $this->mapping[SYNC_POOMCAL_SUBJECT][self::STREAMER_CHECKS] = array(); + $this->mapping[SYNC_POOMCAL_ENDTIME][self::STREAMER_CHECKS] = array(self::STREAMER_CHECK_CMPHIGHER => SYNC_POOMCAL_STARTTIME); + $this->mapping[SYNC_POOMCAL_BUSYSTATUS][self::STREAMER_CHECKS] = array(self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2,3) ); + $this->mapping[SYNC_POOMCAL_REMINDER][self::STREAMER_CHECKS] = array(self::STREAMER_CHECK_CMPHIGHER => -1); + $this->mapping[SYNC_POOMCAL_EXCEPTIONS][self::STREAMER_CHECKS] = array(self::STREAMER_CHECK_NOTALLOWED => true); + + } +} + +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncattachment.php b/sources/lib/syncobjects/syncattachment.php new file mode 100644 index 0000000..653d42d --- /dev/null +++ b/sources/lib/syncobjects/syncattachment.php @@ -0,0 +1,77 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncAttachment extends SyncObject { + public $attmethod; + public $attsize; + public $displayname; + public $attname; + public $attoid; + public $attremoved; + + function SyncAttachment() { + $mapping = array( + SYNC_POOMMAIL_ATTMETHOD => array ( self::STREAMER_VAR => "attmethod"), + SYNC_POOMMAIL_ATTSIZE => array ( self::STREAMER_VAR => "attsize", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO, + self::STREAMER_CHECK_CMPHIGHER => -1 )), + + SYNC_POOMMAIL_DISPLAYNAME => array ( self::STREAMER_VAR => "displayname", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETEMPTY)), + + SYNC_POOMMAIL_ATTNAME => array ( self::STREAMER_VAR => "attname", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETEMPTY)), + + SYNC_POOMMAIL_ATTOID => array ( self::STREAMER_VAR => "attoid"), + SYNC_POOMMAIL_ATTREMOVED => array ( self::STREAMER_VAR => "attremoved"), + ); + + parent::SyncObject($mapping); + } +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncattendee.php b/sources/lib/syncobjects/syncattendee.php new file mode 100644 index 0000000..15f44cd --- /dev/null +++ b/sources/lib/syncobjects/syncattendee.php @@ -0,0 +1,71 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncAttendee extends SyncObject { + public $email; + public $name; + + function SyncAttendee() { + $mapping = array( + SYNC_POOMCAL_EMAIL => array ( self::STREAMER_VAR => "email", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETEMPTY)), + + SYNC_POOMCAL_NAME => array ( self::STREAMER_VAR => "name", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETEMPTY) ) + ); + + if (Request::GetProtocolVersion() >= 12.0) { + $mapping[SYNC_POOMCAL_ATTENDEESTATUS] = array ( self::STREAMER_VAR => "attendeestatus"); + $mapping[SYNC_POOMCAL_ATTENDEETYPE] = array ( self::STREAMER_VAR => "attendeetype"); + } + + parent::SyncObject($mapping); + } +} + +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncbaseattachment.php b/sources/lib/syncobjects/syncbaseattachment.php new file mode 100644 index 0000000..61c7667 --- /dev/null +++ b/sources/lib/syncobjects/syncbaseattachment.php @@ -0,0 +1,71 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncBaseAttachment extends SyncObject { + public $displayname; + public $filereference; + public $method; + public $estimatedDataSize; + public $contentid; + public $contentlocation; + public $isinline; + + function SyncBaseAttachment() { + $mapping = array( + SYNC_AIRSYNCBASE_DISPLAYNAME => array (self::STREAMER_VAR => "displayname"), + SYNC_AIRSYNCBASE_FILEREFERENCE => array (self::STREAMER_VAR => "filereference"), + SYNC_AIRSYNCBASE_METHOD => array (self::STREAMER_VAR => "method"), + SYNC_AIRSYNCBASE_ESTIMATEDDATASIZE => array (self::STREAMER_VAR => "estimatedDataSize"), + SYNC_AIRSYNCBASE_CONTENTID => array (self::STREAMER_VAR => "contentid"), + SYNC_AIRSYNCBASE_CONTENTLOCATION => array (self::STREAMER_VAR => "contentlocation"), + SYNC_AIRSYNCBASE_ISINLINE => array (self::STREAMER_VAR => "isinline"), + ); + + parent::SyncObject($mapping); + } +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncbasebody.php b/sources/lib/syncobjects/syncbasebody.php new file mode 100644 index 0000000..833d752 --- /dev/null +++ b/sources/lib/syncobjects/syncbasebody.php @@ -0,0 +1,68 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncBaseBody extends SyncObject { + public $type; //Possible types are plain text, html, rtf and mime + public $estimatedDataSize; + public $truncated; + public $data; + public $preview; + + function SyncBaseBody() { + $mapping = array( + SYNC_AIRSYNCBASE_TYPE => array (self::STREAMER_VAR => "type"), + SYNC_AIRSYNCBASE_ESTIMATEDDATASIZE => array (self::STREAMER_VAR => "estimatedDataSize"), + SYNC_AIRSYNCBASE_TRUNCATED => array (self::STREAMER_VAR => "truncated"), + SYNC_AIRSYNCBASE_DATA => array (self::STREAMER_VAR => "data"), + ); + if(Request::GetProtocolVersion() >= 14.0) { + $mapping[SYNC_AIRSYNCBASE_PREVIEW] = array (self::STREAMER_VAR => "preview"); + } + + parent::SyncObject($mapping); + } +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/synccontact.php b/sources/lib/syncobjects/synccontact.php new file mode 100644 index 0000000..454953f --- /dev/null +++ b/sources/lib/syncobjects/synccontact.php @@ -0,0 +1,208 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncContact extends SyncObject { + public $anniversary; + public $assistantname; + public $assistnamephonenumber; + public $birthday; + public $body; + public $bodysize; + public $bodytruncated; + public $business2phonenumber; + public $businesscity; + public $businesscountry; + public $businesspostalcode; + public $businessstate; + public $businessstreet; + public $businessfaxnumber; + public $businessphonenumber; + public $carphonenumber; + public $children; + public $companyname; + public $department; + public $email1address; + public $email2address; + public $email3address; + public $fileas; + public $firstname; + public $home2phonenumber; + public $homecity; + public $homecountry; + public $homepostalcode; + public $homestate; + public $homestreet; + public $homefaxnumber; + public $homephonenumber; + public $jobtitle; + public $lastname; + public $middlename; + public $mobilephonenumber; + public $officelocation; + public $othercity; + public $othercountry; + public $otherpostalcode; + public $otherstate; + public $otherstreet; + public $pagernumber; + public $radiophonenumber; + public $spouse; + public $suffix; + public $title; + public $webpage; + public $yomicompanyname; + public $yomifirstname; + public $yomilastname; + public $rtf; + public $picture; + public $categories; + + // AS 2.5 props + public $customerid; + public $governmentid; + public $imaddress; + public $imaddress2; + public $imaddress3; + public $managername; + public $companymainphone; + public $accountname; + public $nickname; + public $mms; + + function SyncContact() { + $mapping = array ( + SYNC_POOMCONTACTS_ANNIVERSARY => array ( self::STREAMER_VAR => "anniversary", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES ), + + SYNC_POOMCONTACTS_ASSISTANTNAME => array ( self::STREAMER_VAR => "assistantname"), + SYNC_POOMCONTACTS_ASSISTNAMEPHONENUMBER => array ( self::STREAMER_VAR => "assistnamephonenumber"), + SYNC_POOMCONTACTS_BIRTHDAY => array ( self::STREAMER_VAR => "birthday", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES ), + + SYNC_POOMCONTACTS_BODY => array ( self::STREAMER_VAR => "body"), + SYNC_POOMCONTACTS_BODYSIZE => array ( self::STREAMER_VAR => "bodysize"), + SYNC_POOMCONTACTS_BODYTRUNCATED => array ( self::STREAMER_VAR => "bodytruncated"), + SYNC_POOMCONTACTS_BUSINESS2PHONENUMBER => array ( self::STREAMER_VAR => "business2phonenumber"), + SYNC_POOMCONTACTS_BUSINESSCITY => array ( self::STREAMER_VAR => "businesscity"), + SYNC_POOMCONTACTS_BUSINESSCOUNTRY => array ( self::STREAMER_VAR => "businesscountry"), + SYNC_POOMCONTACTS_BUSINESSPOSTALCODE => array ( self::STREAMER_VAR => "businesspostalcode"), + SYNC_POOMCONTACTS_BUSINESSSTATE => array ( self::STREAMER_VAR => "businessstate"), + SYNC_POOMCONTACTS_BUSINESSSTREET => array ( self::STREAMER_VAR => "businessstreet"), + SYNC_POOMCONTACTS_BUSINESSFAXNUMBER => array ( self::STREAMER_VAR => "businessfaxnumber"), + SYNC_POOMCONTACTS_BUSINESSPHONENUMBER => array ( self::STREAMER_VAR => "businessphonenumber"), + SYNC_POOMCONTACTS_CARPHONENUMBER => array ( self::STREAMER_VAR => "carphonenumber"), + SYNC_POOMCONTACTS_CHILDREN => array ( self::STREAMER_VAR => "children", + self::STREAMER_ARRAY => SYNC_POOMCONTACTS_CHILD ), + + SYNC_POOMCONTACTS_COMPANYNAME => array ( self::STREAMER_VAR => "companyname"), + SYNC_POOMCONTACTS_DEPARTMENT => array ( self::STREAMER_VAR => "department"), + SYNC_POOMCONTACTS_EMAIL1ADDRESS => array ( self::STREAMER_VAR => "email1address"), + SYNC_POOMCONTACTS_EMAIL2ADDRESS => array ( self::STREAMER_VAR => "email2address"), + SYNC_POOMCONTACTS_EMAIL3ADDRESS => array ( self::STREAMER_VAR => "email3address"), + SYNC_POOMCONTACTS_FILEAS => array ( self::STREAMER_VAR => "fileas"), + SYNC_POOMCONTACTS_FIRSTNAME => array ( self::STREAMER_VAR => "firstname"), + SYNC_POOMCONTACTS_HOME2PHONENUMBER => array ( self::STREAMER_VAR => "home2phonenumber"), + SYNC_POOMCONTACTS_HOMECITY => array ( self::STREAMER_VAR => "homecity"), + SYNC_POOMCONTACTS_HOMECOUNTRY => array ( self::STREAMER_VAR => "homecountry"), + SYNC_POOMCONTACTS_HOMEPOSTALCODE => array ( self::STREAMER_VAR => "homepostalcode"), + SYNC_POOMCONTACTS_HOMESTATE => array ( self::STREAMER_VAR => "homestate"), + SYNC_POOMCONTACTS_HOMESTREET => array ( self::STREAMER_VAR => "homestreet"), + SYNC_POOMCONTACTS_HOMEFAXNUMBER => array ( self::STREAMER_VAR => "homefaxnumber"), + SYNC_POOMCONTACTS_HOMEPHONENUMBER => array ( self::STREAMER_VAR => "homephonenumber"), + SYNC_POOMCONTACTS_JOBTITLE => array ( self::STREAMER_VAR => "jobtitle"), + SYNC_POOMCONTACTS_LASTNAME => array ( self::STREAMER_VAR => "lastname"), + SYNC_POOMCONTACTS_MIDDLENAME => array ( self::STREAMER_VAR => "middlename"), + SYNC_POOMCONTACTS_MOBILEPHONENUMBER => array ( self::STREAMER_VAR => "mobilephonenumber"), + SYNC_POOMCONTACTS_OFFICELOCATION => array ( self::STREAMER_VAR => "officelocation"), + SYNC_POOMCONTACTS_OTHERCITY => array ( self::STREAMER_VAR => "othercity"), + SYNC_POOMCONTACTS_OTHERCOUNTRY => array ( self::STREAMER_VAR => "othercountry"), + SYNC_POOMCONTACTS_OTHERPOSTALCODE => array ( self::STREAMER_VAR => "otherpostalcode"), + SYNC_POOMCONTACTS_OTHERSTATE => array ( self::STREAMER_VAR => "otherstate"), + SYNC_POOMCONTACTS_OTHERSTREET => array ( self::STREAMER_VAR => "otherstreet"), + SYNC_POOMCONTACTS_PAGERNUMBER => array ( self::STREAMER_VAR => "pagernumber"), + SYNC_POOMCONTACTS_RADIOPHONENUMBER => array ( self::STREAMER_VAR => "radiophonenumber"), + SYNC_POOMCONTACTS_SPOUSE => array ( self::STREAMER_VAR => "spouse"), + SYNC_POOMCONTACTS_SUFFIX => array ( self::STREAMER_VAR => "suffix"), + SYNC_POOMCONTACTS_TITLE => array ( self::STREAMER_VAR => "title"), + SYNC_POOMCONTACTS_WEBPAGE => array ( self::STREAMER_VAR => "webpage"), + SYNC_POOMCONTACTS_YOMICOMPANYNAME => array ( self::STREAMER_VAR => "yomicompanyname"), + SYNC_POOMCONTACTS_YOMIFIRSTNAME => array ( self::STREAMER_VAR => "yomifirstname"), + SYNC_POOMCONTACTS_YOMILASTNAME => array ( self::STREAMER_VAR => "yomilastname"), + SYNC_POOMCONTACTS_RTF => array ( self::STREAMER_VAR => "rtf"), + SYNC_POOMCONTACTS_PICTURE => array ( self::STREAMER_VAR => "picture", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_LENGTHMAX => SYNC_CONTACTS_MAXPICTURESIZE )), + + SYNC_POOMCONTACTS_CATEGORIES => array ( self::STREAMER_VAR => "categories", + self::STREAMER_ARRAY => SYNC_POOMCONTACTS_CATEGORY ), + ); + + if (Request::GetProtocolVersion() >= 2.5) { + $mapping[SYNC_POOMCONTACTS2_CUSTOMERID] = array ( self::STREAMER_VAR => "customerid"); + $mapping[SYNC_POOMCONTACTS2_GOVERNMENTID] = array ( self::STREAMER_VAR => "governmentid"); + $mapping[SYNC_POOMCONTACTS2_IMADDRESS] = array ( self::STREAMER_VAR => "imaddress"); + $mapping[SYNC_POOMCONTACTS2_IMADDRESS2] = array ( self::STREAMER_VAR => "imaddress2"); + $mapping[SYNC_POOMCONTACTS2_IMADDRESS3] = array ( self::STREAMER_VAR => "imaddress3"); + $mapping[SYNC_POOMCONTACTS2_MANAGERNAME] = array ( self::STREAMER_VAR => "managername"); + $mapping[SYNC_POOMCONTACTS2_COMPANYMAINPHONE] = array ( self::STREAMER_VAR => "companymainphone"); + $mapping[SYNC_POOMCONTACTS2_ACCOUNTNAME] = array ( self::STREAMER_VAR => "accountname"); + $mapping[SYNC_POOMCONTACTS2_NICKNAME] = array ( self::STREAMER_VAR => "nickname"); + $mapping[SYNC_POOMCONTACTS2_MMS] = array ( self::STREAMER_VAR => "mms"); + } + + if (Request::GetProtocolVersion() >= 12.0) { + $mapping[SYNC_AIRSYNCBASE_BODY] = array ( self::STREAMER_VAR => "asbody", + self::STREAMER_TYPE => "SyncBaseBody"); + + //unset these properties because airsyncbase body and attachments will be used instead + unset($mapping[SYNC_POOMCONTACTS_BODY], $mapping[SYNC_POOMCONTACTS_BODYTRUNCATED]); + } + + parent::SyncObject($mapping); + } +} + +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncdeviceinformation.php b/sources/lib/syncobjects/syncdeviceinformation.php new file mode 100644 index 0000000..9e2b412 --- /dev/null +++ b/sources/lib/syncobjects/syncdeviceinformation.php @@ -0,0 +1,85 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncDeviceInformation extends SyncObject { + public $model; + public $imei; + public $friendlyname; + public $os; + public $oslanguage; + public $phonenumber; + public $useragent; //12.1 &14.0 + public $mobileoperator; //14.0 + public $enableoutboundsms; //14.0 + public $Status; + + public function SyncDeviceInformation() { + $mapping = array ( + SYNC_SETTINGS_MODEL => array ( self::STREAMER_VAR => "model"), + SYNC_SETTINGS_IMEI => array ( self::STREAMER_VAR => "imei"), + SYNC_SETTINGS_FRIENDLYNAME => array ( self::STREAMER_VAR => "friendlyname"), + SYNC_SETTINGS_OS => array ( self::STREAMER_VAR => "os"), + SYNC_SETTINGS_OSLANGUAGE => array ( self::STREAMER_VAR => "oslanguage"), + SYNC_SETTINGS_PHONENUMBER => array ( self::STREAMER_VAR => "phonenumber"), + + SYNC_SETTINGS_PROP_STATUS => array ( self::STREAMER_VAR => "Status", + self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE) + ); + + if (Request::GetProtocolVersion() >= 12.1) { + $mapping[SYNC_SETTINGS_USERAGENT] = array ( self::STREAMER_VAR => "useragent"); + } + + if (Request::GetProtocolVersion() >= 14.0) { + $mapping[SYNC_SETTINGS_MOBILEOPERATOR] = array ( self::STREAMER_VAR => "mobileoperator"); + $mapping[SYNC_SETTINGS_ENABLEOUTBOUNDSMS] = array ( self::STREAMER_VAR => "enableoutboundsms"); + } + + parent::SyncObject($mapping); + } +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncdevicepassword.php b/sources/lib/syncobjects/syncdevicepassword.php new file mode 100644 index 0000000..4ac1b88 --- /dev/null +++ b/sources/lib/syncobjects/syncdevicepassword.php @@ -0,0 +1,63 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncDevicePassword extends SyncObject { + public $password; + public $Status; + + public function SyncDevicePassword() { + $mapping = array ( + SYNC_SETTINGS_DEVICEPW => array ( self::STREAMER_VAR => "password"), + + SYNC_SETTINGS_PROP_STATUS => array ( self::STREAMER_VAR => "Status", + self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE) + ); + + parent::SyncObject($mapping); + } +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncfolder.php b/sources/lib/syncobjects/syncfolder.php new file mode 100644 index 0000000..ae71c12 --- /dev/null +++ b/sources/lib/syncobjects/syncfolder.php @@ -0,0 +1,79 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncFolder extends SyncObject { + public $serverid; + public $parentid; + public $displayname; + public $type; + public $Store; + + function SyncFolder() { + $mapping = array ( + SYNC_FOLDERHIERARCHY_SERVERENTRYID => array ( self::STREAMER_VAR => "serverid", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => false)), + + SYNC_FOLDERHIERARCHY_PARENTID => array ( self::STREAMER_VAR => "parentid", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO)), + + SYNC_FOLDERHIERARCHY_DISPLAYNAME => array ( self::STREAMER_VAR => "displayname", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETEMPTY)), + + SYNC_FOLDERHIERARCHY_TYPE => array ( self::STREAMER_VAR => "type", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => 18, + self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 20 )), + + SYNC_FOLDERHIERARCHY_IGNORE_STORE => array ( self::STREAMER_VAR => "Store", + self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE), + ); + + parent::SyncObject($mapping); + } +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncitemoperationsattachment.php b/sources/lib/syncobjects/syncitemoperationsattachment.php new file mode 100644 index 0000000..6f2e8da --- /dev/null +++ b/sources/lib/syncobjects/syncitemoperationsattachment.php @@ -0,0 +1,62 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncItemOperationsAttachment extends SyncObject { + public $contenttype; + public $data; + + function SyncItemOperationsAttachment() { + $mapping = array( + SYNC_AIRSYNCBASE_CONTENTTYPE => array ( self::STREAMER_VAR => "contenttype"), + SYNC_ITEMOPERATIONS_DATA => array ( self::STREAMER_VAR => "data", + self::STREAMER_TYPE => self::STREAMER_TYPE_STREAM, + self::STREAMER_PROP => self::STREAMER_TYPE_MULTIPART), + ); + + parent::SyncObject($mapping); + } +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncmail.php b/sources/lib/syncobjects/syncmail.php new file mode 100644 index 0000000..7918a93 --- /dev/null +++ b/sources/lib/syncobjects/syncmail.php @@ -0,0 +1,200 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncMail extends SyncObject { + public $to; + public $cc; + public $from; + public $subject; + public $threadtopic; + public $datereceived; + public $displayto; + public $importance; + public $read; + public $attachments; + public $mimetruncated; + public $mimedata; + public $mimesize; + public $bodytruncated; + public $bodysize; + public $body; + public $messageclass; + public $meetingrequest; + public $reply_to; + + // AS 2.5 prop + public $internetcpid; + + // AS 12.0 props + public $asbody; + public $asattachments; + public $flag; + public $contentclass; + public $nativebodytype; + + // AS 14.0 props + public $umcallerid; + public $umusernotes; + public $conversationid; + public $conversationindex; + public $lastverbexecuted; //possible values unknown, reply to sender, reply to all, forward + public $lastverbexectime; + public $receivedasbcc; + public $sender; + + function SyncMail() { + $mapping = array ( + SYNC_POOMMAIL_TO => array ( self::STREAMER_VAR => "to", + self::STREAMER_TYPE => self::STREAMER_TYPE_COMMA_SEPARATED, + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_LENGTHMAX => 32768, + self::STREAMER_CHECK_EMAIL => "" )), + + SYNC_POOMMAIL_CC => array ( self::STREAMER_VAR => "cc", + self::STREAMER_TYPE => self::STREAMER_TYPE_COMMA_SEPARATED, + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_LENGTHMAX => 32768, + self::STREAMER_CHECK_EMAIL => "" )), + + SYNC_POOMMAIL_FROM => array ( self::STREAMER_VAR => "from", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_LENGTHMAX => 32768, + self::STREAMER_CHECK_EMAIL => "" )), + + SYNC_POOMMAIL_SUBJECT => array ( self::STREAMER_VAR => "subject"), + SYNC_POOMMAIL_THREADTOPIC => array ( self::STREAMER_VAR => "threadtopic"), + SYNC_POOMMAIL_DATERECEIVED => array ( self::STREAMER_VAR => "datereceived", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMMAIL_DISPLAYTO => array ( self::STREAMER_VAR => "displayto"), + + // Importance values + // 0 = Low + // 1 = Normal + // 2 = High + // even the default value 1 is optional, the native android client 2.2 interprets a non-existing value as 0 (low) + SYNC_POOMMAIL_IMPORTANCE => array ( self::STREAMER_VAR => "importance", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETONE, + self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2) )), + + SYNC_POOMMAIL_READ => array ( self::STREAMER_VAR => "read", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_POOMMAIL_ATTACHMENTS => array ( self::STREAMER_VAR => "attachments", + self::STREAMER_TYPE => "SyncAttachment", + self::STREAMER_ARRAY => SYNC_POOMMAIL_ATTACHMENT), + + SYNC_POOMMAIL_MIMETRUNCATED => array ( self::STREAMER_VAR => "mimetruncated", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ZEROORONE => self::STREAMER_CHECK_SETZERO)), + + SYNC_POOMMAIL_MIMEDATA => array ( self::STREAMER_VAR => "mimedata"), //TODO mimedata should be of a type stream + + SYNC_POOMMAIL_MIMESIZE => array ( self::STREAMER_VAR => "mimesize", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => -1)), + + SYNC_POOMMAIL_BODYTRUNCATED => array ( self::STREAMER_VAR => "bodytruncated", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ZEROORONE => self::STREAMER_CHECK_SETZERO)), + + SYNC_POOMMAIL_BODYSIZE => array ( self::STREAMER_VAR => "bodysize", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => -1)), + + SYNC_POOMMAIL_BODY => array ( self::STREAMER_VAR => "body"), + SYNC_POOMMAIL_MESSAGECLASS => array ( self::STREAMER_VAR => "messageclass"), + SYNC_POOMMAIL_MEETINGREQUEST => array ( self::STREAMER_VAR => "meetingrequest", + self::STREAMER_TYPE => "SyncMeetingRequest"), + + SYNC_POOMMAIL_REPLY_TO => array ( self::STREAMER_VAR => "reply_to", + self::STREAMER_TYPE => self::STREAMER_TYPE_SEMICOLON_SEPARATED, + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_EMAIL => "" )), + + ); + + if (Request::GetProtocolVersion() >= 2.5) { + $mapping[SYNC_POOMMAIL_INTERNETCPID] = array ( self::STREAMER_VAR => "internetcpid"); + } + + if (Request::GetProtocolVersion() >= 12.0) { + $mapping[SYNC_AIRSYNCBASE_BODY] = array ( self::STREAMER_VAR => "asbody", + self::STREAMER_TYPE => "SyncBaseBody"); + + $mapping[SYNC_AIRSYNCBASE_ATTACHMENTS] = array ( self::STREAMER_VAR => "asattachments", + self::STREAMER_TYPE => "SyncBaseAttachment", + 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) )); + + $mapping[SYNC_POOMMAIL_FLAG] = array ( self::STREAMER_VAR => "flag", + self::STREAMER_TYPE => "SyncMailFlags", + self::STREAMER_PROP => self::STREAMER_TYPE_SEND_EMPTY); + + $mapping[SYNC_AIRSYNCBASE_NATIVEBODYTYPE] = array ( self::STREAMER_VAR => "nativebodytype"); + + //unset these properties because airsyncbase body and attachments will be used instead + unset($mapping[SYNC_POOMMAIL_BODY], $mapping[SYNC_POOMMAIL_BODYTRUNCATED], $mapping[SYNC_POOMMAIL_ATTACHMENTS]); + } + + if (Request::GetProtocolVersion() >= 14.0) { + $mapping[SYNC_POOMMAIL2_UMCALLERID] = array ( self::STREAMER_VAR => "umcallerid"); + $mapping[SYNC_POOMMAIL2_UMUSERNOTES] = array ( self::STREAMER_VAR => "umusernotes"); + $mapping[SYNC_POOMMAIL2_CONVERSATIONID] = array ( self::STREAMER_VAR => "conversationid"); + $mapping[SYNC_POOMMAIL2_CONVERSATIONINDEX] = array ( self::STREAMER_VAR => "conversationindex"); + $mapping[SYNC_POOMMAIL2_LASTVERBEXECUTED] = array ( self::STREAMER_VAR => "lastverbexecuted"); + + $mapping[SYNC_POOMMAIL2_LASTVERBEXECUTIONTIME] = array ( self::STREAMER_VAR => "lastverbexectime", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES); + + $mapping[SYNC_POOMMAIL2_RECEIVEDASBCC] = array ( self::STREAMER_VAR => "receivedasbcc"); + $mapping[SYNC_POOMMAIL2_SENDER] = array ( self::STREAMER_VAR => "sender"); + $mapping[SYNC_POOMMAIL_CATEGORIES] = array ( self::STREAMER_VAR => "categories", + self::STREAMER_ARRAY => SYNC_POOMMAIL_CATEGORY); + //TODO bodypart, accountid, rightsmanagementlicense + } + + parent::SyncObject($mapping); + } +} + +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncmailflags.php b/sources/lib/syncobjects/syncmailflags.php new file mode 100644 index 0000000..fa08a84 --- /dev/null +++ b/sources/lib/syncobjects/syncmailflags.php @@ -0,0 +1,99 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncMailFlags extends SyncObject { + public $subject; + public $flagstatus; + public $flagtype; //Possible types are clear, complete, active + public $datecompleted; + public $completetime; + public $startdate; + public $duedate; + public $utcstartdate; + public $utcduedate; + public $reminderset; + public $remindertime; + public $ordinaldate; + public $subordinaldate; + + + function SyncMailFlags() { + $mapping = array( + SYNC_POOMTASKS_SUBJECT => array ( self::STREAMER_VAR => "subject"), + SYNC_POOMMAIL_FLAGSTATUS => array ( self::STREAMER_VAR => "flagstatus"), + SYNC_POOMMAIL_FLAGTYPE => array ( self::STREAMER_VAR => "flagtype"), + SYNC_POOMTASKS_DATECOMPLETED => array ( self::STREAMER_VAR => "datecompleted", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMMAIL_COMPLETETIME => array ( self::STREAMER_VAR => "completetime", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMTASKS_STARTDATE => array ( self::STREAMER_VAR => "startdate", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMTASKS_DUEDATE => array ( self::STREAMER_VAR => "duedate", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMTASKS_UTCSTARTDATE => array ( self::STREAMER_VAR => "utcstartdate", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMTASKS_UTCDUEDATE => array ( self::STREAMER_VAR => "utcduedate", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMTASKS_REMINDERSET => array ( self::STREAMER_VAR => "reminderset"), + SYNC_POOMTASKS_REMINDERTIME => array ( self::STREAMER_VAR => "remindertime", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMTASKS_ORDINALDATE => array ( self::STREAMER_VAR => "ordinaldate", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMTASKS_SUBORDINALDATE => array ( self::STREAMER_VAR => "subordinaldate"), + ); + + parent::SyncObject($mapping); + } +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncmeetingrequest.php b/sources/lib/syncobjects/syncmeetingrequest.php new file mode 100644 index 0000000..a293cbd --- /dev/null +++ b/sources/lib/syncobjects/syncmeetingrequest.php @@ -0,0 +1,134 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncMeetingRequest extends SyncObject { + public $alldayevent; + public $starttime; + public $dtstamp; + public $endtime; + public $instancetype; + public $location; + public $organizer; + public $recurrenceid; + public $reminder; + public $responserequested; + public $recurrences; + public $sensitivity; + public $busystatus; + public $timezone; + public $globalobjid; + + function SyncMeetingRequest() { + $mapping = array ( + SYNC_POOMMAIL_ALLDAYEVENT => array ( self::STREAMER_VAR => "alldayevent", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ZEROORONE => self::STREAMER_CHECK_SETZERO)), + + SYNC_POOMMAIL_STARTTIME => array ( self::STREAMER_VAR => "starttime", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES, + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO, + self::STREAMER_CHECK_CMPLOWER => SYNC_POOMMAIL_ENDTIME ) ), + + SYNC_POOMMAIL_DTSTAMP => array ( self::STREAMER_VAR => "dtstamp", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES, + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO) ), + + SYNC_POOMMAIL_ENDTIME => array ( self::STREAMER_VAR => "endtime", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES, + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETONE, + self::STREAMER_CHECK_CMPHIGHER => SYNC_POOMMAIL_STARTTIME ) ), + // Instancetype values + // 0 = single appointment + // 1 = master recurring appointment + // 2 = single instance of recurring appointment + // 3 = exception of recurring appointment + SYNC_POOMMAIL_INSTANCETYPE => array ( self::STREAMER_VAR => "instancetype", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO, + self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2,3) )), + + SYNC_POOMMAIL_LOCATION => array ( self::STREAMER_VAR => "location"), + SYNC_POOMMAIL_ORGANIZER => array ( self::STREAMER_VAR => "organizer", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETEMPTY ) ), + + SYNC_POOMMAIL_RECURRENCEID => array ( self::STREAMER_VAR => "recurrenceid", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMMAIL_REMINDER => array ( self::STREAMER_VAR => "reminder", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => -1)), + + SYNC_POOMMAIL_RESPONSEREQUESTED => array ( self::STREAMER_VAR => "responserequested"), + SYNC_POOMMAIL_RECURRENCES => array ( self::STREAMER_VAR => "recurrences", + self::STREAMER_TYPE => "SyncMeetingRequestRecurrence", + self::STREAMER_ARRAY => SYNC_POOMMAIL_RECURRENCE), + // Sensitivity values + // 0 = Normal + // 1 = Personal + // 2 = Private + // 3 = Confident + SYNC_POOMMAIL_SENSITIVITY => array ( self::STREAMER_VAR => "sensitivity", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO, + self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2,3) )), + + // Busystatus values + // 0 = Free + // 1 = Tentative + // 2 = Busy + // 3 = Out of office + SYNC_POOMMAIL_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) )), + + SYNC_POOMMAIL_TIMEZONE => array ( self::STREAMER_VAR => "timezone", + 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"), + ); + + parent::SyncObject($mapping); + } +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncmeetingrequestrecurrence.php b/sources/lib/syncobjects/syncmeetingrequestrecurrence.php new file mode 100644 index 0000000..25ba05d --- /dev/null +++ b/sources/lib/syncobjects/syncmeetingrequestrecurrence.php @@ -0,0 +1,121 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncMeetingRequestRecurrence extends SyncObject { + public $type; + public $until; + public $occurrences; + public $interval; + public $dayofweek; + public $dayofmonth; + public $weekofmonth; + public $monthofyear; + + function SyncMeetingRequestRecurrence() { + $mapping = array ( + // Recurrence type + // 0 = Recurs daily + // 1 = Recurs weekly + // 2 = Recurs monthly + // 3 = Recurs monthly on the nth day + // 5 = Recurs yearly + // 6 = Recurs yearly on the nth day + SYNC_POOMMAIL_TYPE => array ( self::STREAMER_VAR => "type", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO, + self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2,3,5,6) )), + + SYNC_POOMMAIL_UNTIL => array ( self::STREAMER_VAR => "until", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE), + + SYNC_POOMMAIL_OCCURRENCES => array ( self::STREAMER_VAR => "occurrences", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 1000 )), + + SYNC_POOMMAIL_INTERVAL => array ( self::STREAMER_VAR => "interval", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 1000 )), + + // DayOfWeek values + // 1 = Sunday + // 2 = Monday + // 4 = Tuesday + // 8 = Wednesday + // 16 = Thursday + // 32 = Friday + // 62 = Weekdays // not in spec: daily weekday recurrence + // 64 = Saturday + // 127 = The last day of the month. Value valid only in monthly or yearly recurrences. + // As this is a bitmask, actually all values 0 > x < 128 are allowed + SYNC_POOMMAIL_DAYOFWEEK => array ( self::STREAMER_VAR => "dayofweek", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 128 )), + + // DayOfMonth values + // 1-31 representing the day + SYNC_POOMMAIL_DAYOFMONTH => array ( self::STREAMER_VAR => "dayofmonth", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 32 )), + + // WeekOfMonth + // 1-4 = Y st/nd/rd/th week of month + // 5 = last week of month + SYNC_POOMMAIL_WEEKOFMONTH => array ( self::STREAMER_VAR => "weekofmonth", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(1,2,3,4,5) )), + + // MonthOfYear + // 1-12 representing the month + SYNC_POOMMAIL_MONTHOFYEAR => array ( self::STREAMER_VAR => "monthofyear", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(1,2,3,4,5,6,7,8,9,10,11,12) )), + ); + + parent::SyncObject($mapping); + } +} + +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncnote.php b/sources/lib/syncobjects/syncnote.php new file mode 100644 index 0000000..66db246 --- /dev/null +++ b/sources/lib/syncobjects/syncnote.php @@ -0,0 +1,75 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncNote extends SyncObject { + public $asbody; + public $categories; + public $lastmodified; + public $messageclass; + public $subject; + + function SyncNote() { + $mapping = array( + SYNC_AIRSYNCBASE_BODY => array ( self::STREAMER_VAR => "asbody", + self::STREAMER_TYPE => "SyncBaseBody"), + + SYNC_NOTES_CATEGORIES => array ( self::STREAMER_VAR => "categories", + self::STREAMER_ARRAY => SYNC_NOTES_CATEGORY), + + SYNC_NOTES_LASTMODIFIEDDATE => array ( self::STREAMER_VAR => "lastmodified", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE), + + SYNC_NOTES_MESSAGECLASS => array ( self::STREAMER_VAR => "messageclass"), + + SYNC_NOTES_SUBJECT => array ( self::STREAMER_VAR => "subject"), + ); + + parent::SyncObject($mapping); + } +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncobject.php b/sources/lib/syncobjects/syncobject.php new file mode 100644 index 0000000..4a571e9 --- /dev/null +++ b/sources/lib/syncobjects/syncobject.php @@ -0,0 +1,423 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +abstract class SyncObject extends Streamer { + const STREAMER_CHECKS = 6; + const STREAMER_CHECK_REQUIRED = 7; + const STREAMER_CHECK_ZEROORONE = 8; + const STREAMER_CHECK_NOTALLOWED = 9; + const STREAMER_CHECK_ONEVALUEOF = 10; + const STREAMER_CHECK_SETZERO = "setToValue0"; + const STREAMER_CHECK_SETONE = "setToValue1"; + const STREAMER_CHECK_SETTWO = "setToValue2"; + const STREAMER_CHECK_SETEMPTY = "setToValueEmpty"; + const STREAMER_CHECK_CMPLOWER = 13; + const STREAMER_CHECK_CMPHIGHER = 14; + const STREAMER_CHECK_LENGTHMAX = 15; + const STREAMER_CHECK_EMAIL = 16; + + protected $unsetVars; + + + public function SyncObject($mapping) { + $this->unsetVars = array(); + parent::Streamer($mapping); + } + + /** + * Sets all supported but not transmitted variables + * of this SyncObject to an "empty" value, so they are deleted when being saved + * + * @param array $supportedFields array with all supported fields, if available + * + * @access public + * @return boolean + */ + public function emptySupported($supportedFields) { + // Some devices do not send supported tag. In such a case remove all not set properties. + if (($supportedFields === false || !is_array($supportedFields) || (empty($supportedFields)))) { + if (defined('UNSET_UNDEFINED_PROPERTIES') && UNSET_UNDEFINED_PROPERTIES && ($this instanceOf SyncContact || $this instanceOf SyncAppointment)) { + ZLog::Write(LOGLEVEL_INFO, sprintf("%s->emptySupported(): no supported list available, emptying all not set parameters", get_class($this))); + $supportedFields = array_keys($this->mapping); + } + else { + return false; + } + } + + foreach ($supportedFields as $field) { + if (!isset($this->mapping[$field])) { + ZLog::Write(LOGLEVEL_WARN, sprintf("Field '%s' is supposed to be emptied but is not defined for '%s'", $field, get_class($this))); + continue; + } + $var = $this->mapping[$field][self::STREAMER_VAR]; + // add var to $this->unsetVars if $var is not set + if (!isset($this->$var)) + $this->unsetVars[] = $var; + } + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Supported variables to be unset: %s", implode(',', $this->unsetVars))); + return true; + } + + + /** + * Compares this a SyncObject to another. + * In case that all available mapped fields are exactly EQUAL, it returns true + * + * @see SyncObject + * @param SyncObject $odo other SyncObject + * @return boolean + */ + public function equals($odo, $log = false) { + if ($odo === false) + return false; + + // check objecttype + if (! ($odo instanceof SyncObject)) { + ZLog::Write(LOGLEVEL_DEBUG, "SyncObject->equals() the target object is not a SyncObject"); + return false; + } + + // check for mapped fields + foreach ($this->mapping as $v) { + $val = $v[self::STREAMER_VAR]; + // array of values? + if (isset($v[self::STREAMER_ARRAY])) { + // seek for differences in the arrays + if (is_array($this->$val) && is_array($odo->$val)) { + if (count(array_diff($this->$val, $odo->$val)) + count(array_diff($odo->$val, $this->$val)) > 0) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncObject->equals() items in array '%s' differ", $val)); + return false; + } + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncObject->equals() array '%s' is set in one but not the other object", $val)); + return false; + } + } + else { + if (isset($this->$val) && isset($odo->$val)) { + if ($this->$val != $odo->$val){ + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncObject->equals() false on field '%s': '%s' != '%s'", $val, Utils::PrintAsString($this->$val), Utils::PrintAsString($odo->$val))); + return false; + } + } + else if (!isset($this->$val) && !isset($odo->$val)) { + continue; + } + else { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncObject->equals() false because field '%s' is only defined at one obj: '%s' != '%s'", $val, Utils::PrintAsString(isset($this->$val)), Utils::PrintAsString(isset($odo->$val)))); + return false; + } + } + } + + return true; + } + + /** + * String representation of the object + * + * @return String + */ + public function __toString() { + $str = get_class($this) . " (\n"; + + $streamerVars = array(); + foreach ($this->mapping as $k=>$v) + $streamerVars[$v[self::STREAMER_VAR]] = (isset($v[self::STREAMER_TYPE]))?$v[self::STREAMER_TYPE]:false; + + foreach (get_object_vars($this) as $k=>$v) { + if ($k == "mapping") continue; + + if (array_key_exists($k, $streamerVars)) + $strV = "(S) "; + else + $strV = ""; + + // self::STREAMER_ARRAY ? + if (is_array($v)) { + $str .= "\t". $strV . $k ."(Array) size: " . count($v) ."\n"; + foreach ($v as $value) $str .= "\t\t". Utils::PrintAsString($value) ."\n"; + } + else if ($v instanceof SyncObject) { + $str .= "\t". $strV .$k ." => ". str_replace("\n", "\n\t\t\t", $v->__toString()) . "\n"; + } + else + $str .= "\t". $strV .$k ." => " . (isset($this->$k)? Utils::PrintAsString($this->$k) :"null") . "\n"; + } + $str .= ")"; + + return $str; + } + + /** + * Returns the properties which have to be unset on the server + * + * @access public + * @return array + */ + public function getUnsetVars() { + return $this->unsetVars; + } + + /** + * Method checks if the object has the minimum of required parameters + * and fullfills semantic dependencies + * + * General checks: + * STREAMER_CHECK_REQUIRED may have as value false (do not fix, ignore object!) or set-to-values: STREAMER_CHECK_SETZERO/ONE/TWO, STREAMER_CHECK_SETEMPTY + * STREAMER_CHECK_ZEROORONE may be 0 or 1, if none of these, set-to-values: STREAMER_CHECK_SETZERO or STREAMER_CHECK_SETONE + * STREAMER_CHECK_NOTALLOWED fails if is set + * STREAMER_CHECK_ONEVALUEOF expects an array with accepted values, fails if value is not in array + * + * Comparison: + * STREAMER_CHECK_CMPLOWER compares if the current parameter is lower as a literal or another parameter of the same object + * STREAMER_CHECK_CMPHIGHER compares if the current parameter is higher as a literal or another parameter of the same object + * + * @param boolean $logAsDebug (opt) default is false, so messages are logged in WARN log level + * + * @access public + * @return boolean + */ + public function Check($logAsDebug = false) { + // semantic checks general "turn off switch" + if (defined("DO_SEMANTIC_CHECKS") && DO_SEMANTIC_CHECKS === false) { + ZLog::Write(LOGLEVEL_DEBUG, "SyncObject->Check(): semantic checks disabled. Check your config for 'DO_SEMANTIC_CHECKS'."); + return true; + } + + $defaultLogLevel = LOGLEVEL_WARN; + + // in some cases non-false checks should not provoke a WARN log but only a DEBUG log + if ($logAsDebug) + $defaultLogLevel = LOGLEVEL_DEBUG; + + $objClass = get_class($this); + foreach ($this->mapping as $k=>$v) { + + // check sub-objects recursively + if (isset($v[self::STREAMER_TYPE]) && isset($this->$v[self::STREAMER_VAR])) { + if ($this->$v[self::STREAMER_VAR] instanceof SyncObject) { + if (! $this->$v[self::STREAMER_VAR]->Check($logAsDebug)) + return false; + } + else if (is_array($this->$v[self::STREAMER_VAR])) { + foreach ($this->$v[self::STREAMER_VAR] as $subobj) + if ($subobj instanceof SyncObject && !$subobj->Check($logAsDebug)) + return false; + } + } + + if (isset($v[self::STREAMER_CHECKS])) { + foreach ($v[self::STREAMER_CHECKS] as $rule => $condition) { + // check REQUIRED settings + if ($rule === self::STREAMER_CHECK_REQUIRED && (!isset($this->$v[self::STREAMER_VAR]) || $this->$v[self::STREAMER_VAR] === '' ) ) { + // parameter is not set but .. + // requested to set to 0 + if ($condition === self::STREAMER_CHECK_SETZERO) { + $this->$v[self::STREAMER_VAR] = 0; + ZLog::Write($defaultLogLevel, sprintf("SyncObject->Check(): Fixed object from type %s: parameter '%s' is set to 0", $objClass, $v[self::STREAMER_VAR])); + } + // requested to be set to 1 + else if ($condition === self::STREAMER_CHECK_SETONE) { + $this->$v[self::STREAMER_VAR] = 1; + ZLog::Write($defaultLogLevel, sprintf("SyncObject->Check(): Fixed object from type %s: parameter '%s' is set to 1", $objClass, $v[self::STREAMER_VAR])); + } + // requested to be set to 2 + else if ($condition === self::STREAMER_CHECK_SETTWO) { + $this->$v[self::STREAMER_VAR] = 2; + ZLog::Write($defaultLogLevel, sprintf("SyncObject->Check(): Fixed object from type %s: parameter '%s' is set to 2", $objClass, $v[self::STREAMER_VAR])); + } + // requested to be set to '' + else if ($condition === self::STREAMER_CHECK_SETEMPTY) { + if (!isset($this->$v[self::STREAMER_VAR])) { + $this->$v[self::STREAMER_VAR] = ''; + ZLog::Write($defaultLogLevel, sprintf("SyncObject->Check(): Fixed object from type %s: parameter '%s' is set to ''", $objClass, $v[self::STREAMER_VAR])); + } + } + // there is another value !== false + else if ($condition !== false) { + $this->$v[self::STREAMER_VAR] = $condition; + ZLog::Write($defaultLogLevel, sprintf("SyncObject->Check(): Fixed object from type %s: parameter '%s' is set to '%s'", $objClass, $v[self::STREAMER_VAR], $condition)); + + } + // no fix available! + else { + ZLog::Write($defaultLogLevel, sprintf("SyncObject->Check(): Unmet condition in object from type %s: parameter '%s' is required but not set. Check failed!", $objClass, $v[self::STREAMER_VAR])); + return false; + } + } // end STREAMER_CHECK_REQUIRED + + + // check STREAMER_CHECK_ZEROORONE + if ($rule === self::STREAMER_CHECK_ZEROORONE && isset($this->$v[self::STREAMER_VAR])) { + if ($this->$v[self::STREAMER_VAR] != 0 && $this->$v[self::STREAMER_VAR] != 1) { + $newval = $condition === self::STREAMER_CHECK_SETZERO ? 0:1; + $this->$v[self::STREAMER_VAR] = $newval; + ZLog::Write($defaultLogLevel, sprintf("SyncObject->Check(): Fixed object from type %s: parameter '%s' is set to '%s' as it was not 0 or 1", $objClass, $v[self::STREAMER_VAR], $newval)); + } + }// end STREAMER_CHECK_ZEROORONE + + + // check STREAMER_CHECK_ONEVALUEOF + if ($rule === self::STREAMER_CHECK_ONEVALUEOF && isset($this->$v[self::STREAMER_VAR])) { + if (!in_array($this->$v[self::STREAMER_VAR], $condition)) { + ZLog::Write($defaultLogLevel, sprintf("SyncObject->Check(): object from type %s: parameter '%s'->'%s' is not in the range of allowed values.", $objClass, $v[self::STREAMER_VAR], $this->$v[self::STREAMER_VAR])); + return false; + } + }// end STREAMER_CHECK_ONEVALUEOF + + + // Check value compared to other value or literal + if ($rule === self::STREAMER_CHECK_CMPHIGHER || $rule === self::STREAMER_CHECK_CMPLOWER) { + if (isset($this->$v[self::STREAMER_VAR])) { + $cmp = false; + // directly compare against literals + if (is_int($condition)) { + $cmp = $condition; + } + // check for invalid compare-to + else if (!isset($this->mapping[$condition])) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("SyncObject->Check(): Can not compare parameter '%s' against the other value '%s' as it is not defined object from type %s. Please report this! Check skipped!", $objClass, $v[self::STREAMER_VAR], $condition)); + continue; + } + else { + $cmpPar = $this->mapping[$condition][self::STREAMER_VAR]; + if (isset($this->$cmpPar)) + $cmp = $this->$cmpPar; + } + + if ($cmp === false) { + ZLog::Write(LOGLEVEL_WARN, sprintf("SyncObject->Check(): Unmet condition in object from type %s: parameter '%s' can not be compared, as the comparable is not set. Check failed!", $objClass, $v[self::STREAMER_VAR])); + return false; + } + if ( ($rule == self::STREAMER_CHECK_CMPHIGHER && $this->$v[self::STREAMER_VAR] < $cmp) || + ($rule == self::STREAMER_CHECK_CMPLOWER && $this->$v[self::STREAMER_VAR] > $cmp) + ) { + + ZLog::Write(LOGLEVEL_WARN, sprintf("SyncObject->Check(): Unmet condition in object from type %s: parameter '%s' is %s than '%s'. Check failed!", + $objClass, + $v[self::STREAMER_VAR], + (($rule === self::STREAMER_CHECK_CMPHIGHER)?'LOWER':'HIGHER'), + ((isset($cmpPar)?$cmpPar:$condition)) )); + return false; + } + } + } // STREAMER_CHECK_CMP* + + + // check STREAMER_CHECK_LENGTHMAX + if ($rule === self::STREAMER_CHECK_LENGTHMAX && isset($this->$v[self::STREAMER_VAR])) { + + if (is_array($this->$v[self::STREAMER_VAR])) { + // implosion takes 2bytes, so we just assume ", " here + $chkstr = implode(", ", $this->$v[self::STREAMER_VAR]); + } + else + $chkstr = $this->$v[self::STREAMER_VAR]; + + if (strlen($chkstr) > $condition) { + ZLog::Write(LOGLEVEL_WARN, sprintf("SyncObject->Check(): object from type %s: parameter '%s' is longer than %d. Check failed", $objClass, $v[self::STREAMER_VAR], $condition)); + return false; + } + }// end STREAMER_CHECK_LENGTHMAX + + + // check STREAMER_CHECK_EMAIL + // if $condition is false then the check really fails. Otherwise invalid emails are removed. + // if nothing is left (all emails were false), the parameter is set to condition + if ($rule === self::STREAMER_CHECK_EMAIL && isset($this->$v[self::STREAMER_VAR])) { + if ($condition === false && ( (is_array($this->$v[self::STREAMER_VAR]) && empty($this->$v[self::STREAMER_VAR])) || strlen($this->$v[self::STREAMER_VAR]) == 0) ) + continue; + + $as_array = false; + + if (is_array($this->$v[self::STREAMER_VAR])) { + $mails = $this->$v[self::STREAMER_VAR]; + $as_array = true; + } + else { + $mails = array( $this->$v[self::STREAMER_VAR] ); + } + + $output = array(); + foreach ($mails as $mail) { + if (! Utils::CheckEmail($mail)) { + ZLog::Write(LOGLEVEL_WARN, sprintf("SyncObject->Check(): object from type %s: parameter '%s' contains an invalid email address '%s'. Address is removed.", $objClass, $v[self::STREAMER_VAR], $mail)); + } + else + $output[] = $mail; + } + if (count($mails) != count($output)) { + if ($condition === false) + return false; + + // nothing left, use $condition as new value + if (count($output) == 0) + $output[] = $condition; + + // if we are allowed to rewrite the attribute, we do that + if ($as_array) + $this->$v[self::STREAMER_VAR] = $output; + else + $this->$v[self::STREAMER_VAR] = $output[0]; + } + }// end STREAMER_CHECK_EMAIL + + + } // foreach CHECKS + } // isset CHECKS + } // foreach mapping + + return true; + } +} + +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncoof.php b/sources/lib/syncobjects/syncoof.php new file mode 100644 index 0000000..163a2b7 --- /dev/null +++ b/sources/lib/syncobjects/syncoof.php @@ -0,0 +1,83 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncOOF extends SyncObject { + public $oofstate; + public $starttime; + public $endtime; + public $oofmessage = array(); + public $bodytype; + public $Status; + + public function SyncOOF() { + $mapping = array ( + SYNC_SETTINGS_OOFSTATE => array ( self::STREAMER_VAR => "oofstate", + self::STREAMER_CHECKS => array( array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2) ))), + + SYNC_SETTINGS_STARTTIME => array ( self::STREAMER_VAR => "starttime", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_SETTINGS_ENDTIME => array ( self::STREAMER_VAR => "endtime", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_SETTINGS_OOFMESSAGE => array ( self::STREAMER_VAR => "oofmessage", + self::STREAMER_TYPE => "SyncOOFMessage", + self::STREAMER_PROP => self::STREAMER_TYPE_NO_CONTAINER, + self::STREAMER_ARRAY => SYNC_SETTINGS_OOFMESSAGE), + + SYNC_SETTINGS_BODYTYPE => array ( self::STREAMER_VAR => "bodytype", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(SYNC_SETTINGSOOF_BODYTYPE_HTML, ucfirst(strtolower(SYNC_SETTINGSOOF_BODYTYPE_TEXT))) )), + + SYNC_SETTINGS_PROP_STATUS => array ( self::STREAMER_VAR => "Status", + self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE) + ); + + parent::SyncObject($mapping); + } + +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncoofmessage.php b/sources/lib/syncobjects/syncoofmessage.php new file mode 100644 index 0000000..c0ec349 --- /dev/null +++ b/sources/lib/syncobjects/syncoofmessage.php @@ -0,0 +1,81 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncOOFMessage extends SyncObject { + public $appliesToInternal; + public $appliesToExternal; + public $appliesToExternalUnknown; + public $enabled; + public $replymessage; + public $bodytype; + + public function SyncOOFMessage() { + $mapping = array ( + //only one of the following 3 apply types will be available + SYNC_SETTINGS_APPLIESTOINTERVAL => array ( self::STREAMER_VAR => "appliesToInternal", + self::STREAMER_PROP => self::STREAMER_TYPE_SEND_EMPTY), + + SYNC_SETTINGS_APPLIESTOEXTERNALKNOWN => array ( self::STREAMER_VAR => "appliesToExternal", + self::STREAMER_PROP => self::STREAMER_TYPE_SEND_EMPTY), + + SYNC_SETTINGS_APPLIESTOEXTERNALUNKNOWN => array ( self::STREAMER_VAR => "appliesToExternalUnknown", + self::STREAMER_PROP => self::STREAMER_TYPE_SEND_EMPTY), + + SYNC_SETTINGS_ENABLED => array ( self::STREAMER_VAR => "enabled"), + + SYNC_SETTINGS_REPLYMESSAGE => array ( self::STREAMER_VAR => "replymessage"), + + SYNC_SETTINGS_BODYTYPE => array ( self::STREAMER_VAR => "bodytype", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(SYNC_SETTINGSOOF_BODYTYPE_HTML, ucfirst(strtolower(SYNC_SETTINGSOOF_BODYTYPE_TEXT))) )), + + ); + + parent::SyncObject($mapping); + } + +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncprovisioning.php b/sources/lib/syncobjects/syncprovisioning.php new file mode 100644 index 0000000..7d05150 --- /dev/null +++ b/sources/lib/syncobjects/syncprovisioning.php @@ -0,0 +1,304 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncProvisioning extends SyncObject { + //AS 12.0, 12.1 and 14.0 props + public $devpwenabled; + public $alphanumpwreq; + public $devencenabled; + public $pwrecoveryenabled; + public $docbrowseenabled; + public $attenabled; + public $mindevpwlenngth; + public $maxinacttimedevlock; + public $maxdevpwfailedattempts; + public $maxattsize; + public $allowsimpledevpw; + public $devpwexpiration; + public $devpwhistory; + + //AS 12.1 and 14.0 props + public $allostoragecard; + public $allowcam; + public $reqdevenc; + public $allowunsignedapps; + public $allowunsigninstallpacks; + public $mindevcomplexchars; + public $allowwifi; + public $allowtextmessaging; + public $allowpopimapemail; + public $allowbluetooth; + public $allowirda; + public $reqmansyncroam; + public $allowdesktopsync; + public $maxcalagefilter; + public $allowhtmlemail; + public $maxemailagefilter; + public $maxemailbodytruncsize; + public $maxemailhtmlbodytruncsize; + public $reqsignedsmimemessages; + public $reqencsmimemessages; + public $reqsignedsmimealgorithm; + public $reqencsmimealgorithm; + public $allowsmimeencalgneg; + public $allowsmimesoftcerts; + public $allowbrowser; + public $allowconsumeremail; + public $allowremotedesk; + public $allowinternetsharing; + public $unapprovedinromapplist; + public $approvedapplist; + + function SyncProvisioning() { + $mapping = array ( + SYNC_PROVISION_DEVPWENABLED => array ( self::STREAMER_VAR => "devpwenabled", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_ALPHANUMPWREQ => array ( self::STREAMER_VAR => "alphanumpwreq", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_PWRECOVERYENABLED => array ( self::STREAMER_VAR => "pwrecoveryenabled", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_DEVENCENABLED => array ( self::STREAMER_VAR => "devencenabled", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_DOCBROWSEENABLED => array ( self::STREAMER_VAR => "docbrowseenabled"), // depricated + SYNC_PROVISION_ATTENABLED => array ( self::STREAMER_VAR => "attenabled", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_MINDEVPWLENGTH => array ( self::STREAMER_VAR => "mindevpwlenngth", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 17 )), + + SYNC_PROVISION_MAXINACTTIMEDEVLOCK => array ( self::STREAMER_VAR => "maxinacttimedevlock"), + SYNC_PROVISION_MAXDEVPWFAILEDATTEMPTS => array ( self::STREAMER_VAR => "maxdevpwfailedattempts", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 3, + self::STREAMER_CHECK_CMPLOWER => 17 )), + + SYNC_PROVISION_MAXATTSIZE => array ( self::STREAMER_VAR => "maxattsize", + self::STREAMER_PROP => self::STREAMER_TYPE_SEND_EMPTY, + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => -1 )), + + SYNC_PROVISION_ALLOWSIMPLEDEVPW => array ( self::STREAMER_VAR => "allowsimpledevpw", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_DEVPWEXPIRATION => array ( self::STREAMER_VAR => "devpwexpiration", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => -1 )), + + SYNC_PROVISION_DEVPWHISTORY => array ( self::STREAMER_VAR => "devpwhistory", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => -1 )), + ); + + if(Request::GetProtocolVersion() >= 12.1) { + $mapping += array ( + SYNC_PROVISION_ALLOWSTORAGECARD => array ( self::STREAMER_VAR => "allostoragecard", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_ALLOWCAM => array ( self::STREAMER_VAR => "allowcam", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_REQDEVENC => array ( self::STREAMER_VAR => "reqdevenc", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_ALLOWUNSIGNEDAPPS => array ( self::STREAMER_VAR => "allowunsignedapps", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_ALLOWUNSIGNEDINSTALLATIONPACKAGES => array ( self::STREAMER_VAR => "allowunsigninstallpacks", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_MINDEVPWCOMPLEXCHARS => array ( self::STREAMER_VAR => "mindevcomplexchars", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(1,2,3,4) )), + + SYNC_PROVISION_ALLOWWIFI => array ( self::STREAMER_VAR => "allowwifi", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_ALLOWTEXTMESSAGING => array ( self::STREAMER_VAR => "allowtextmessaging", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_ALLOWPOPIMAPEMAIL => array ( self::STREAMER_VAR => "allowpopimapemail", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_ALLOWBLUETOOTH => array ( self::STREAMER_VAR => "allowbluetooth", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2) )), + + SYNC_PROVISION_ALLOWIRDA => array ( self::STREAMER_VAR => "allowirda", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_REQMANUALSYNCWHENROAM => array ( self::STREAMER_VAR => "reqmansyncroam", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_ALLOWDESKTOPSYNC => array ( self::STREAMER_VAR => "allowdesktopsync", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_MAXCALAGEFILTER => array ( self::STREAMER_VAR => "maxcalagefilter", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,4,5,6,7) )), + + SYNC_PROVISION_ALLOWHTMLEMAIL => array ( self::STREAMER_VAR => "allowhtmlemail", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_MAXEMAILAGEFILTER => array ( self::STREAMER_VAR => "maxemailagefilter", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => -1, + self::STREAMER_CHECK_CMPLOWER => 6 )), + + SYNC_PROVISION_MAXEMAILBODYTRUNCSIZE => array ( self::STREAMER_VAR => "maxemailbodytruncsize", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => -2 )), + + SYNC_PROVISION_MAXEMAILHTMLBODYTRUNCSIZE => array ( self::STREAMER_VAR => "maxemailhtmlbodytruncsize", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => -2 )), + + SYNC_PROVISION_REQSIGNEDSMIMEMESSAGES => array ( self::STREAMER_VAR => "reqsignedsmimemessages", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_REQENCSMIMEMESSAGES => array ( self::STREAMER_VAR => "reqencsmimemessages", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_REQSIGNEDSMIMEALGORITHM => array ( self::STREAMER_VAR => "reqsignedsmimealgorithm", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_REQENCSMIMEALGORITHM => array ( self::STREAMER_VAR => "reqencsmimealgorithm", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2,3,4) )), + + SYNC_PROVISION_ALLOWSMIMEENCALGORITHNEG => array ( self::STREAMER_VAR => "allowsmimeencalgneg", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2) )), + + SYNC_PROVISION_ALLOWSMIMESOFTCERTS => array ( self::STREAMER_VAR => "allowsmimesoftcerts", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_ALLOWBROWSER => array ( self::STREAMER_VAR => "allowbrowser", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_ALLOWCONSUMEREMAIL => array ( self::STREAMER_VAR => "allowconsumeremail", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_ALLOWREMOTEDESKTOP => array ( self::STREAMER_VAR => "allowremotedesk", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_ALLOWINTERNETSHARING => array ( self::STREAMER_VAR => "allowinternetsharing", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1) )), + + SYNC_PROVISION_UNAPPROVEDINROMAPPLIST => array ( self::STREAMER_VAR => "unapprovedinromapplist", + self::STREAMER_PROP => self::STREAMER_TYPE_SEND_EMPTY, + self::STREAMER_ARRAY => SYNC_PROVISION_APPNAME), //TODO check + + SYNC_PROVISION_APPROVEDAPPLIST => array ( self::STREAMER_VAR => "approvedapplist", + self::STREAMER_PROP => self::STREAMER_TYPE_SEND_EMPTY, + self::STREAMER_ARRAY => SYNC_PROVISION_HASH), //TODO check + ); + } + + parent::SyncObject($mapping); + } + + public function Load($policies = array()) { + if (empty($policies)) { + $this->LoadDefaultPolicies(); + } + else foreach ($policies as $p=>$v) { + if (!isset($this->mapping[$p])) { + ZLog::Write(LOGLEVEL_INFO, sprintf("Policy '%s' not supported by the device, ignoring", substr($p, strpos($p,':')+1))); + continue; + } + ZLog::Write(LOGLEVEL_INFO, sprintf("Policy '%s' enforced with: %s", substr($p, strpos($p,':')+1), Utils::PrintAsString($v))); + + $var = $this->mapping[$p][self::STREAMER_VAR]; + $this->$var = $v; + } + } + + public function LoadDefaultPolicies() { + //AS 12.0, 12.1 and 14.0 props + $this->devpwenabled = 0; + $this->alphanumpwreq = 0; + $this->devencenabled = 0; + $this->pwrecoveryenabled = 0; + $this->docbrowseenabled; + $this->attenabled = 1; + $this->mindevpwlenngth = 4; + $this->maxinacttimedevlock = 900; + $this->maxdevpwfailedattempts = 8; + $this->maxattsize = ''; + $this->allowsimpledevpw = 1; + $this->devpwexpiration = 0; + $this->devpwhistory = 0; + + //AS 12.1 and 14.0 props + $this->allostoragecard = 1; + $this->allowcam = 1; + $this->reqdevenc = 0; + $this->allowunsignedapps = 1; + $this->allowunsigninstallpacks = 1; + $this->mindevcomplexchars = 3; + $this->allowwifi = 1; + $this->allowtextmessaging = 1; + $this->allowpopimapemail = 1; + $this->allowbluetooth = 2; + $this->allowirda = 1; + $this->reqmansyncroam = 0; + $this->allowdesktopsync = 1; + $this->maxcalagefilter = 0; + $this->allowhtmlemail = 1; + $this->maxemailagefilter = 0; + $this->maxemailbodytruncsize = -1; + $this->maxemailhtmlbodytruncsize = -1; + $this->reqsignedsmimemessages = 0; + $this->reqencsmimemessages = 0; + $this->reqsignedsmimealgorithm = 0; + $this->reqencsmimealgorithm = 0; + $this->allowsmimeencalgneg = 2; + $this->allowsmimesoftcerts = 1; + $this->allowbrowser = 1; + $this->allowconsumeremail = 1; + $this->allowremotedesk = 1; + $this->allowinternetsharing = 1; + $this->unapprovedinromapplist = array(); + $this->approvedapplist = array(); + } +} + +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncrecurrence.php b/sources/lib/syncobjects/syncrecurrence.php new file mode 100644 index 0000000..665abaf --- /dev/null +++ b/sources/lib/syncobjects/syncrecurrence.php @@ -0,0 +1,120 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncRecurrence extends SyncObject { + public $type; + public $until; + public $occurrences; + public $interval; + public $dayofweek; + public $dayofmonth; + public $weekofmonth; + public $monthofyear; + + function SyncRecurrence() { + $mapping = array ( + // Recurrence type + // 0 = Recurs daily + // 1 = Recurs weekly + // 2 = Recurs monthly + // 3 = Recurs monthly on the nth day + // 5 = Recurs yearly + // 6 = Recurs yearly on the nth day + SYNC_POOMCAL_TYPE => array ( self::STREAMER_VAR => "type", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO, + self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2,3,5,6) )), + + SYNC_POOMCAL_UNTIL => array ( self::STREAMER_VAR => "until", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE), + + SYNC_POOMCAL_OCCURRENCES => array ( self::STREAMER_VAR => "occurrences", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 1000 )), + + SYNC_POOMCAL_INTERVAL => array ( self::STREAMER_VAR => "interval", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 1000 )), + + // DayOfWeek values + // 1 = Sunday + // 2 = Monday + // 4 = Tuesday + // 8 = Wednesday + // 16 = Thursday + // 32 = Friday + // 62 = Weekdays // not in spec: daily weekday recurrence + // 64 = Saturday + // 127 = The last day of the month. Value valid only in monthly or yearly recurrences. + // As this is a bitmask, actually all values 0 > x < 128 are allowed + SYNC_POOMCAL_DAYOFWEEK => array ( self::STREAMER_VAR => "dayofweek", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 128 )), + + // DayOfMonth values + // 1-31 representing the day + SYNC_POOMCAL_DAYOFMONTH => array ( self::STREAMER_VAR => "dayofmonth", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 32 )), + + // WeekOfMonth + // 1-4 = Y st/nd/rd/th week of month + // 5 = last week of month + SYNC_POOMCAL_WEEKOFMONTH => array ( self::STREAMER_VAR => "weekofmonth", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(1,2,3,4,5) )), + + // MonthOfYear + // 1-12 representing the month + SYNC_POOMCAL_MONTHOFYEAR => array ( self::STREAMER_VAR => "monthofyear", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(1,2,3,4,5,6,7,8,9,10,11,12) )), + ); + + parent::SyncObject($mapping); + } +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncresolverecipient.php b/sources/lib/syncobjects/syncresolverecipient.php new file mode 100755 index 0000000..b8f6926 --- /dev/null +++ b/sources/lib/syncobjects/syncresolverecipient.php @@ -0,0 +1,77 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncResolveRecipient extends SyncObject { + public $type; + public $displayname; + public $emailaddress; + public $availability; + public $certificates; + public $pictures; + + public function SyncResolveRecipient() { + $mapping = array ( + SYNC_RESOLVERECIPIENTS_TYPE => array ( self::STREAMER_VAR => "type"), + SYNC_RESOLVERECIPIENTS_DISPLAYNAME => array ( self::STREAMER_VAR => "displayname"), + SYNC_RESOLVERECIPIENTS_EMAILADDRESS => array ( self::STREAMER_VAR => "emailaddress"), + + SYNC_RESOLVERECIPIENTS_AVAILABILITY => array ( self::STREAMER_VAR => "availability", + self::STREAMER_TYPE => "SyncRRAvailability"), + + SYNC_RESOLVERECIPIENTS_CERTIFICATES => array ( self::STREAMER_VAR => "certificates", + self::STREAMER_TYPE => "SyncRRCertificates"), + + SYNC_RESOLVERECIPIENTS_PICTURE => array ( self::STREAMER_VAR => "pictures", + self::STREAMER_TYPE => "SyncRRPicture", + self::STREAMER_ARRAY => SYNC_RESOLVERECIPIENTS_PICTURE), + ); + + parent::SyncObject($mapping); + } + +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncresolverecipients.php b/sources/lib/syncobjects/syncresolverecipients.php new file mode 100755 index 0000000..9339545 --- /dev/null +++ b/sources/lib/syncobjects/syncresolverecipients.php @@ -0,0 +1,75 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncResolveRecipients extends SyncObject { + public $to = array(); + public $options; + public $status; + public $recipientCount; + public $recipient; + + public function SyncResolveRecipients() { + $mapping = array ( + SYNC_RESOLVERECIPIENTS_TO => array ( self::STREAMER_VAR => "to", + self::STREAMER_ARRAY => SYNC_RESOLVERECIPIENTS_TO, + self::STREAMER_PROP => self::STREAMER_TYPE_NO_CONTAINER), + + SYNC_RESOLVERECIPIENTS_OPTIONS => array ( self::STREAMER_VAR => "options", + self::STREAMER_TYPE => "SyncRROptions"), + + SYNC_RESOLVERECIPIENTS_STATUS => array ( self::STREAMER_VAR => "status"), + SYNC_RESOLVERECIPIENTS_RECIPIENTCOUNT => array ( self::STREAMER_VAR => "recipientcount"), + + SYNC_RESOLVERECIPIENTS_RECIPIENT => array ( self::STREAMER_VAR => "recipient", + self::STREAMER_TYPE => "SyncResolveRecipient"), + ); + + parent::SyncObject($mapping); + } + +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncresolverecipientsavailability.php b/sources/lib/syncobjects/syncresolverecipientsavailability.php new file mode 100755 index 0000000..d4f3642 --- /dev/null +++ b/sources/lib/syncobjects/syncresolverecipientsavailability.php @@ -0,0 +1,66 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncRRAvailability extends SyncObject { + public $starttime; + public $endtime; + public $status; + public $mergedfreebusy; + + public function SyncRRAvailability() { + $mapping = array ( + SYNC_RESOLVERECIPIENTS_STARTTIME => array ( self::STREAMER_VAR => "starttime"), + SYNC_RESOLVERECIPIENTS_ENDTIME => array ( self::STREAMER_VAR => "endtime"), + SYNC_RESOLVERECIPIENTS_STATUS => array ( self::STREAMER_VAR => "status"), + SYNC_RESOLVERECIPIENTS_MERGEDFREEBUSY => array ( self::STREAMER_VAR => "mergedfreebusy"), + ); + + parent::SyncObject($mapping); + } + +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncresolverecipientscertificates.php b/sources/lib/syncobjects/syncresolverecipientscertificates.php new file mode 100755 index 0000000..52ce8f0 --- /dev/null +++ b/sources/lib/syncobjects/syncresolverecipientscertificates.php @@ -0,0 +1,74 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncRRCertificates extends SyncObject { + public $status; + public $certificatecount; + public $recipientcount; + public $certificate; + public $minicertificate; + + public function SyncRRCertificates() { + $mapping = array ( + SYNC_RESOLVERECIPIENTS_STATUS => array ( self::STREAMER_VAR => "status"), + SYNC_RESOLVERECIPIENTS_CERTIFICATECOUNT => array ( self::STREAMER_VAR => "certificatecount"), + SYNC_RESOLVERECIPIENTS_RECIPIENTCOUNT => array ( self::STREAMER_VAR => "recipientcount"), + + SYNC_RESOLVERECIPIENTS_CERTIFICATE => array ( self::STREAMER_VAR => "certificate", + self::STREAMER_ARRAY => SYNC_RESOLVERECIPIENTS_CERTIFICATE, + self::STREAMER_PROP => self::STREAMER_TYPE_NO_CONTAINER), + + SYNC_RESOLVERECIPIENTS_MINICERTIFICATE => array ( self::STREAMER_VAR => "minicertificate", + self::STREAMER_ARRAY => SYNC_RESOLVERECIPIENTS_MINICERTIFICATE, + self::STREAMER_PROP => self::STREAMER_TYPE_NO_CONTAINER) + ); + + parent::SyncObject($mapping); + } + +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncresolverecipientsoptions.php b/sources/lib/syncobjects/syncresolverecipientsoptions.php new file mode 100755 index 0000000..09b5393 --- /dev/null +++ b/sources/lib/syncobjects/syncresolverecipientsoptions.php @@ -0,0 +1,72 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncRROptions extends SyncObject { + public $certificateretrieval; + public $maxcertificates; + public $maxambiguousrecipients; + public $availability; + public $picture; + + public function SyncRROptions() { + $mapping = array ( + SYNC_RESOLVERECIPIENTS_CERTIFICATERETRIEVAL => array ( self::STREAMER_VAR => "certificateretrieval"), + SYNC_RESOLVERECIPIENTS_MAXCERTIFICATES => array ( self::STREAMER_VAR => "maxcertificates"), + SYNC_RESOLVERECIPIENTS_MAXAMBIGUOUSRECIPIENTS => array ( self::STREAMER_VAR => "maxambiguousrecipients"), + + SYNC_RESOLVERECIPIENTS_AVAILABILITY => array ( self::STREAMER_VAR => "availability", + self::STREAMER_TYPE => "SyncRRAvailability"), + + SYNC_RESOLVERECIPIENTS_PICTURE => array ( self::STREAMER_VAR => "picture", + self::STREAMER_TYPE => "SyncRRPicture"), + ); + + parent::SyncObject($mapping); + } + +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncresolverecipientspicture.php b/sources/lib/syncobjects/syncresolverecipientspicture.php new file mode 100755 index 0000000..d241184 --- /dev/null +++ b/sources/lib/syncobjects/syncresolverecipientspicture.php @@ -0,0 +1,66 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncRRPicture extends SyncObject { + public $maxsize; + public $maxpictures; + public $status; + public $data; + + public function SyncRRPicture() { + $mapping = array ( + SYNC_RESOLVERECIPIENTS_MAXSIZE => array ( self::STREAMER_VAR => "maxsize"), + SYNC_RESOLVERECIPIENTS_MAXPICTURES => array ( self::STREAMER_VAR => "maxpictures"), + SYNC_RESOLVERECIPIENTS_STATUS => array ( self::STREAMER_VAR => "status"), + SYNC_RESOLVERECIPIENTS_DATA => array ( self::STREAMER_VAR => "data"), + ); + + parent::SyncObject($mapping); + } + +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncsendmail.php b/sources/lib/syncobjects/syncsendmail.php new file mode 100644 index 0000000..cfe109c --- /dev/null +++ b/sources/lib/syncobjects/syncsendmail.php @@ -0,0 +1,86 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncSendMail extends SyncObject { + public $clientid; + public $saveinsent; + public $replacemime; + public $accountid; + public $source; + public $mime; + public $replyflag; + public $forwardflag; + + function SyncSendMail() { + $mapping = array ( + SYNC_COMPOSEMAIL_CLIENTID => array ( self::STREAMER_VAR => "clientid"), + + SYNC_COMPOSEMAIL_SAVEINSENTITEMS => array ( self::STREAMER_VAR => "saveinsent", + self::STREAMER_PROP => self::STREAMER_TYPE_SEND_EMPTY), + + SYNC_COMPOSEMAIL_REPLACEMIME => array ( self::STREAMER_VAR => "replacemime", + self::STREAMER_PROP => self::STREAMER_TYPE_SEND_EMPTY), + + SYNC_COMPOSEMAIL_ACCOUNTID => array ( self::STREAMER_VAR => "accountid"), + + SYNC_COMPOSEMAIL_SOURCE => array ( self::STREAMER_VAR => "source", + self::STREAMER_TYPE => "SyncSendMailSource"), + + SYNC_COMPOSEMAIL_MIME => array ( self::STREAMER_VAR => "mime"), + + SYNC_COMPOSEMAIL_REPLYFLAG => array ( self::STREAMER_VAR => "replyflag", + self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE), + + SYNC_COMPOSEMAIL_FORWARDFLAG => array ( self::STREAMER_VAR => "forwardflag", + self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE), + ); + + parent::SyncObject($mapping); + } +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncsendmailsource.php b/sources/lib/syncobjects/syncsendmailsource.php new file mode 100644 index 0000000..b75f2b5 --- /dev/null +++ b/sources/lib/syncobjects/syncsendmailsource.php @@ -0,0 +1,67 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncSendMailSource extends SyncObject { + public $folderid; + public $itemid; + public $longid; + public $instanceid; + + function SyncSendMailSource() { + $mapping = array ( + SYNC_COMPOSEMAIL_FOLDERID => array ( self::STREAMER_VAR => "folderid"), + SYNC_COMPOSEMAIL_ITEMID => array ( self::STREAMER_VAR => "itemid"), + SYNC_COMPOSEMAIL_LONGID => array ( self::STREAMER_VAR => "longid"), + SYNC_COMPOSEMAIL_INSTANCEID => array ( self::STREAMER_VAR => "instanceid"), + ); + + parent::SyncObject($mapping); + } + +} +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/synctask.php b/sources/lib/syncobjects/synctask.php new file mode 100644 index 0000000..daf3e69 --- /dev/null +++ b/sources/lib/syncobjects/synctask.php @@ -0,0 +1,180 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class SyncTask extends SyncObject { + public $body; + public $complete; + public $datecompleted; + public $duedate; + public $utcduedate; + public $importance; + public $recurrence; + public $regenerate; + public $deadoccur; + public $reminderset; + public $remindertime; + public $sensitivity; + public $startdate; + public $utcstartdate; + public $subject; + public $rtf; + public $categories; + + function SyncTask() { + $mapping = array ( + SYNC_POOMTASKS_BODY => array ( self::STREAMER_VAR => "body"), + SYNC_POOMTASKS_COMPLETE => array ( self::STREAMER_VAR => "complete", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO, + self::STREAMER_CHECK_ZEROORONE => self::STREAMER_CHECK_SETZERO )), + + SYNC_POOMTASKS_DATECOMPLETED => array ( self::STREAMER_VAR => "datecompleted", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMTASKS_DUEDATE => array ( self::STREAMER_VAR => "duedate", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMTASKS_UTCDUEDATE => array ( self::STREAMER_VAR => "utcduedate", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + // Importance values + // 0 = Low + // 1 = Normal + // 2 = High + // even the default value 1 is optional, the native android client 2.2 interprets a non-existing value as 0 (low) + SYNC_POOMTASKS_IMPORTANCE => array ( self::STREAMER_VAR => "importance", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETONE, + self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2) )), + + SYNC_POOMTASKS_RECURRENCE => array ( self::STREAMER_VAR => "recurrence", + self::STREAMER_TYPE => "SyncTaskRecurrence"), + + SYNC_POOMTASKS_REGENERATE => array ( self::STREAMER_VAR => "regenerate"), + SYNC_POOMTASKS_DEADOCCUR => array ( self::STREAMER_VAR => "deadoccur"), + SYNC_POOMTASKS_REMINDERSET => array ( self::STREAMER_VAR => "reminderset", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO, + self::STREAMER_CHECK_ZEROORONE => self::STREAMER_CHECK_SETZERO )), + + SYNC_POOMTASKS_REMINDERTIME => array ( self::STREAMER_VAR => "remindertime", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + // Sensitivity values + // 0 = Normal + // 1 = Personal + // 2 = Private + // 3 = Confident + SYNC_POOMTASKS_SENSITIVITY => array ( self::STREAMER_VAR => "sensitivity", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2,3) )), + + SYNC_POOMTASKS_STARTDATE => array ( self::STREAMER_VAR => "startdate", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMTASKS_UTCSTARTDATE => array ( self::STREAMER_VAR => "utcstartdate", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE_DASHES), + + SYNC_POOMTASKS_SUBJECT => array ( self::STREAMER_VAR => "subject"), + SYNC_POOMTASKS_RTF => array ( self::STREAMER_VAR => "rtf"), + SYNC_POOMTASKS_CATEGORIES => array ( self::STREAMER_VAR => "categories", + self::STREAMER_ARRAY => SYNC_POOMTASKS_CATEGORY), + ); + + if (Request::GetProtocolVersion() >= 12.0) { + $mapping[SYNC_AIRSYNCBASE_BODY] = array ( self::STREAMER_VAR => "asbody", + self::STREAMER_TYPE => "SyncBaseBody"); + + //unset these properties because airsyncbase body and attachments will be used instead + unset($mapping[SYNC_POOMTASKS_BODY]); + } + + parent::SyncObject($mapping); + } + + /** + * Method checks if the object has the minimum of required parameters + * and fullfills semantic dependencies + * + * This overloads the general check() with special checks to be executed + * + * @param boolean $logAsDebug (opt) default is false, so messages are logged in WARN log level + * + * @access public + * @return boolean + */ + public function Check($logAsDebug = false) { + $ret = parent::Check($logAsDebug); + + // semantic checks general "turn off switch" + if (defined("DO_SEMANTIC_CHECKS") && DO_SEMANTIC_CHECKS === false) + return $ret; + + if (!$ret) + return false; + + if (isset($this->startdate) && isset($this->duedate) && $this->duedate < $this->startdate) { + ZLog::Write(LOGLEVEL_WARN, sprintf("SyncObject->Check(): Unmet condition in object from type %s: parameter 'startdate' is HIGHER than 'duedate'. Check failed!", get_class($this) )); + return false; + } + + if (isset($this->utcstartdate) && isset($this->utcduedate) && $this->utcduedate < $this->utcstartdate) { + ZLog::Write(LOGLEVEL_WARN, sprintf("SyncObject->Check(): Unmet condition in object from type %s: parameter 'utcstartdate' is HIGHER than 'utcduedate'. Check failed!", get_class($this) )); + return false; + } + + if (isset($this->duedate) && $this->duedate != Utils::getDayStartOfTimestamp($this->duedate)) { + $this->duedate = Utils::getDayStartOfTimestamp($this->duedate); + ZLog::Write(LOGLEVEL_DEBUG, "Set the due time to the start of the day"); + if (isset($this->startdate) && $this->duedate < $this->startdate) { + $this->startdate = Utils::getDayStartOfTimestamp($this->startdate); + ZLog::Write(LOGLEVEL_DEBUG, "Set the start date to the start of the day"); + } + } + + return true; + } +} + +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/synctaskrecurrence.php b/sources/lib/syncobjects/synctaskrecurrence.php new file mode 100644 index 0000000..e4a0cd7 --- /dev/null +++ b/sources/lib/syncobjects/synctaskrecurrence.php @@ -0,0 +1,157 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +// Exactly the same as SyncRecurrence, but then with SYNC_POOMTASKS_* +class SyncTaskRecurrence extends SyncObject { + public $start; + public $type; + public $until; + public $occurrences; + public $interval; + public $dayofweek; + public $dayofmonth; + public $weekofmonth; + public $monthofyear; + public $deadoccur; + + function SyncTaskRecurrence() { + $mapping = array ( + SYNC_POOMTASKS_START => array ( self::STREAMER_VAR => "start", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE), + + // Recurrence type + // 0 = Recurs daily + // 1 = Recurs weekly + // 2 = Recurs monthly + // 3 = Recurs monthly on the nth day + // 5 = Recurs yearly + // 6 = Recurs yearly on the nth day + SYNC_POOMTASKS_TYPE => array ( self::STREAMER_VAR => "type", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO, + self::STREAMER_CHECK_ONEVALUEOF => array(0,1,2,3,5,6) )), + + SYNC_POOMTASKS_UNTIL => array ( self::STREAMER_VAR => "until", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE ), + + SYNC_POOMTASKS_OCCURRENCES => array ( self::STREAMER_VAR => "occurrences", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 1000 )), + + SYNC_POOMTASKS_INTERVAL => array ( self::STREAMER_VAR => "interval", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 1000 )), + + //TODO: check iOS5 sends deadoccur inside of the recurrence + SYNC_POOMTASKS_DEADOCCUR => array ( self::STREAMER_VAR => "deadoccur"), + + // DayOfWeek values + // 1 = Sunday + // 2 = Monday + // 4 = Tuesday + // 8 = Wednesday + // 16 = Thursday + // 32 = Friday + // 62 = Weekdays // TODO check: value set by WA with daily weekday recurrence + // 64 = Saturday + // 127 = The last day of the month. Value valid only in monthly or yearly recurrences. + SYNC_POOMTASKS_DAYOFWEEK => array ( self::STREAMER_VAR => "dayofweek", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 128 )), + + // DayOfMonth values + // 1-31 representing the day + SYNC_POOMTASKS_DAYOFMONTH => array ( self::STREAMER_VAR => "dayofmonth", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_CMPHIGHER => 0, + self::STREAMER_CHECK_CMPLOWER => 32 )), + + // WeekOfMonth + // 1-4 = Y st/nd/rd/th week of month + // 5 = last week of month + SYNC_POOMTASKS_WEEKOFMONTH => array ( self::STREAMER_VAR => "weekofmonth", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(1,2,3,4,5) )), + + // MonthOfYear + // 1-12 representing the month + SYNC_POOMTASKS_MONTHOFYEAR => array ( self::STREAMER_VAR => "monthofyear", + self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(1,2,3,4,5,6,7,8,9,10,11,12) )), + + ); + parent::SyncObject($mapping); + } + + /** + * Method checks if the object has the minimum of required parameters + * and fullfills semantic dependencies + * + * This overloads the general check() with special checks to be executed + * + * @param boolean $logAsDebug (opt) default is false, so messages are logged in WARN log level + * + * @access public + * @return boolean + */ + public function Check($logAsDebug = false) { + $ret = parent::Check($logAsDebug); + + // semantic checks general "turn off switch" + if (defined("DO_SEMANTIC_CHECKS") && DO_SEMANTIC_CHECKS === false) + return $ret; + + if (!$ret) + return false; + + if (isset($this->start) && isset($this->until) && $this->until < $this->start) { + ZLog::Write(LOGLEVEL_WARN, sprintf("SyncObject->Check(): Unmet condition in object from type %s: parameter 'start' is HIGHER than 'until'. Check failed!", get_class($this) )); + return false; + } + + return true; + } +} + +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncuserinformation.php b/sources/lib/syncobjects/syncuserinformation.php new file mode 100644 index 0000000..a9fae8c --- /dev/null +++ b/sources/lib/syncobjects/syncuserinformation.php @@ -0,0 +1,79 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncUserInformation extends SyncObject { + public $accountid; + public $accountname; + public $userdisplayname; + public $senddisabled; + public $emailaddresses; + public $Status; + + public function SyncUserInformation() { + $mapping = array ( + SYNC_SETTINGS_ACCOUNTID => array ( self::STREAMER_VAR => "accountid"), + SYNC_SETTINGS_ACCOUNTNAME => array ( self::STREAMER_VAR => "accountname"), + SYNC_SETTINGS_EMAILADDRESSES => array ( self::STREAMER_VAR => "emailaddresses", + self::STREAMER_ARRAY => SYNC_SETTINGS_SMPTADDRESS), + + SYNC_SETTINGS_PROP_STATUS => array ( self::STREAMER_VAR => "Status", + self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE) + ); + + if (Request::GetProtocolVersion() >= 12.1) { + $mapping[SYNC_SETTINGS_USERDISPLAYNAME] = array ( self::STREAMER_VAR => "userdisplayname"); + } + + if (Request::GetProtocolVersion() >= 14.0) { + $mapping[SYNC_SETTINGS_SENDDISABLED] = array ( self::STREAMER_VAR => "senddisabled"); + } + + parent::SyncObject($mapping); + } +} + +?> \ No newline at end of file diff --git a/sources/lib/syncobjects/syncvalidatecert.php b/sources/lib/syncobjects/syncvalidatecert.php new file mode 100755 index 0000000..e23c4a3 --- /dev/null +++ b/sources/lib/syncobjects/syncvalidatecert.php @@ -0,0 +1,72 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class SyncValidateCert extends SyncObject { + public $certificatechain; + public $certificates; + public $checkCRL; + public $Status; + + public function SyncValidateCert() { + $mapping = array ( + SYNC_VALIDATECERT_CERTIFICATECHAIN => array ( self::STREAMER_VAR => "certificatechain", + self::STREAMER_ARRAY => SYNC_VALIDATECERT_CERTIFICATE), + + SYNC_VALIDATECERT_CERTIFICATES => array ( self::STREAMER_VAR => "certificates", + self::STREAMER_ARRAY => SYNC_VALIDATECERT_CERTIFICATE), + + SYNC_VALIDATECERT_CHECKCRL => array ( self::STREAMER_VAR => "checkCRL"), + + SYNC_SETTINGS_PROP_STATUS => array ( self::STREAMER_VAR => "Status", + self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE) + ); + + parent::SyncObject($mapping); + } + +} +?> \ No newline at end of file diff --git a/sources/lib/utils/compat.php b/sources/lib/utils/compat.php new file mode 100644 index 0000000..1c3bbef --- /dev/null +++ b/sources/lib/utils/compat.php @@ -0,0 +1,89 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +if (!function_exists("quoted_printable_encode")) { + /** + * Process a string to fit the requirements of RFC2045 section 6.7. Note that + * this works, but replaces more characters than the minimum set. For readability + * the spaces and CRLF pairs aren't encoded though. + * + * @param string $string string to be encoded + * + * @see http://www.php.net/manual/en/function.quoted-printable-decode.php#89417 + */ + 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))); + } +} + +if (!function_exists("apache_request_headers")) { + /** + * When using other webservers or using php as cgi in apache + * the function apache_request_headers() is not available. + * This function parses the environment variables to extract + * the necessary headers for Z-Push + */ + function apache_request_headers() { + $headers = array(); + foreach ($_SERVER as $key => $value) + if (substr($key, 0, 5) == 'HTTP_') + $headers[strtr(substr($key, 5), '_', '-')] = $value; + + return $headers; + } +} + +if (!function_exists("hex2bin")) { + /** + * Complementary function to bin2hex() which converts a hex entryid to a binary entryid. + * Since PHP 5.4 an internal hex2bin() implementation is available. + * + * @param string $data the hexadecimal string + * + * @returns string + */ + function hex2bin($data) { + return pack("H*", $data); + } +} +?> \ No newline at end of file diff --git a/sources/lib/utils/timezoneutil.php b/sources/lib/utils/timezoneutil.php new file mode 100644 index 0000000..910b3fc --- /dev/null +++ b/sources/lib/utils/timezoneutil.php @@ -0,0 +1,1263 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class TimezoneUtil { + + /** + * list of MS and AS compatible timezones + * + * origin: http://msdn.microsoft.com/en-us/library/ms912391%28v=winembedded.11%29.aspx + * dots of tz identifiers were removed + * + * Updated at: 01.06.2012 + */ + private static $mstzones = array( + "000" => array("Dateline Standard Time", "(GMT-12:00) International Date Line West"), + "001" => array("Samoa Standard Time", "(GMT-11:00) Midway Island, Samoa"), + "002" => array("Hawaiian Standard Time", "(GMT-10:00) Hawaii"), + "003" => array("Alaskan Standard Time", "(GMT-09:00) Alaska"), + "004" => array("Pacific Standard Time", "(GMT-08:00) Pacific Time (US and Canada); Tijuana"), + "010" => array("Mountain Standard Time", "(GMT-07:00) Mountain Time (US and Canada)"), + "013" => array("Mexico Standard Time 2", "(GMT-07:00) Chihuahua, La Paz, Mazatlan"), + "015" => array("US Mountain Standard Time", "(GMT-07:00) Arizona"), + "020" => array("Central Standard Time", "(GMT-06:00) Central Time (US and Canada"), + "025" => array("Canada Central Standard Time", "(GMT-06:00) Saskatchewan"), + "030" => array("Mexico Standard Time", "(GMT-06:00) Guadalajara, Mexico City, Monterrey"), + "033" => array("Central America Standard Time", "(GMT-06:00) Central America"), + "035" => array("Eastern Standard Time", "(GMT-05:00) Eastern Time (US and Canada)"), + "040" => array("US Eastern Standard Time", "(GMT-05:00) Indiana (East)"), + "045" => array("SA Pacific Standard Time", "(GMT-05:00) Bogota, Lima, Quito"), + "uk1" => array("Venezuela Standard Time", "(GMT-04:30) Caracas"), // added + "050" => array("Atlantic Standard Time", "(GMT-04:00) Atlantic Time (Canada)"), + "055" => array("SA Western Standard Time", "(GMT-04:00) Caracas, La Paz"), + "056" => array("Pacific SA Standard Time", "(GMT-04:00) Santiago"), + "060" => array("Newfoundland and Labrador Standard Time", "(GMT-03:30) Newfoundland and Labrador"), + "065" => array("E South America Standard Time" , "(GMT-03:00) Brasilia"), + "070" => array("SA Eastern Standard Time", "(GMT-03:00) Buenos Aires, Georgetown"), + "073" => array("Greenland Standard Time", "(GMT-03:00) Greenland"), + "075" => array("Mid-Atlantic Standard Time", "(GMT-02:00) Mid-Atlantic"), + "080" => array("Azores Standard Time", "(GMT-01:00) Azores"), + "083" => array("Cape Verde Standard Time", "(GMT-01:00) Cape Verde Islands"), + "085" => array("GMT Standard Time", "(GMT) Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London"), + "090" => array("Greenwich Standard Time", "(GMT) Casablanca, Monrovia"), + "095" => array("Central Europe Standard Time", "(GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague"), + "100" => array("Central European Standard Time", "(GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb"), + "105" => array("Romance Standard Time", "(GMT+01:00) Brussels, Copenhagen, Madrid, Paris"), + "110" => array("W Europe Standard Time", "(GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"), + "113" => array("W Central Africa Standard Time", "(GMT+01:00) West Central Africa"), + "115" => array("E Europe Standard Time", "(GMT+02:00) Bucharest"), + "120" => array("Egypt Standard Time", "(GMT+02:00) Cairo"), + "125" => array("FLE Standard Time", "(GMT+02:00) Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius"), + "130" => array("GTB Standard Time", "(GMT+02:00) Athens, Istanbul, Minsk"), + "135" => array("Israel Standard Time", "(GMT+02:00) Jerusalem"), + "140" => array("South Africa Standard Time", "(GMT+02:00) Harare, Pretoria"), + "145" => array("Russian Standard Time", "(GMT+03:00) Moscow, St. Petersburg, Volgograd"), + "150" => array("Arab Standard Time", "(GMT+03:00) Kuwait, Riyadh"), + "155" => array("E Africa Standard Time", "(GMT+03:00) Nairobi"), + "158" => array("Arabic Standard Time", "(GMT+03:00) Baghdad"), + "160" => array("Iran Standard Time", "(GMT+03:30) Tehran"), + "165" => array("Arabian Standard Time", "(GMT+04:00) Abu Dhabi, Muscat"), + "170" => array("Caucasus Standard Time", "(GMT+04:00) Baku, Tbilisi, Yerevan"), + "175" => array("Transitional Islamic State of Afghanistan Standard Time","(GMT+04:30) Kabul"), + "180" => array("Ekaterinburg Standard Time", "(GMT+05:00) Ekaterinburg"), + "185" => array("West Asia Standard Time", "(GMT+05:00) Islamabad, Karachi, Tashkent"), + "190" => array("India Standard Time", "(GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi"), + "193" => array("Nepal Standard Time", "(GMT+05:45) Kathmandu"), + "195" => array("Central Asia Standard Time", "(GMT+06:00) Astana, Dhaka"), + "200" => array("Sri Lanka Standard Time", "(GMT+06:00) Sri Jayawardenepura"), + "201" => array("N Central Asia Standard Time", "(GMT+06:00) Almaty, Novosibirsk"), + "203" => array("Myanmar Standard Time", "(GMT+06:30) Yangon Rangoon"), + "205" => array("SE Asia Standard Time", "(GMT+07:00) Bangkok, Hanoi, Jakarta"), + "207" => array("North Asia Standard Time", "(GMT+07:00) Krasnoyarsk"), + "210" => array("China Standard Time", "(GMT+08:00) Beijing, Chongqing, Hong Kong SAR, Urumqi"), + "215" => array("Singapore Standard Time", "(GMT+08:00) Kuala Lumpur, Singapore"), + "220" => array("Taipei Standard Time", "(GMT+08:00) Taipei"), + "225" => array("W Australia Standard Time", "(GMT+08:00) Perth"), + "227" => array("North Asia East Standard Time", "(GMT+08:00) Irkutsk, Ulaanbaatar"), + "230" => array("Korea Standard Time", "(GMT+09:00) Seoul"), + "235" => array("Tokyo Standard Time", "(GMT+09:00) Osaka, Sapporo, Tokyo"), + "240" => array("Yakutsk Standard Time", "(GMT+09:00) Yakutsk"), + "245" => array("AUS Central Standard Time", "(GMT+09:30) Darwin"), + "250" => array("Cen Australia Standard Time", "(GMT+09:30) Adelaide"), + "255" => array("AUS Eastern Standard Time", "(GMT+10:00) Canberra, Melbourne, Sydney"), + "260" => array("E Australia Standard Time", "(GMT+10:00) Brisbane"), + "265" => array("Tasmania Standard Time", "(GMT+10:00) Hobart"), + "270" => array("Vladivostok Standard Time", "(GMT+10:00) Vladivostok"), + "275" => array("West Pacific Standard Time", "(GMT+10:00) Guam, Port Moresby"), + "280" => array("Central Pacific Standard Time", "(GMT+11:00) Magadan, Solomon Islands, New Caledonia"), + "285" => array("Fiji Islands Standard Time", "(GMT+12:00) Fiji Islands, Kamchatka, Marshall Islands"), + "290" => array("New Zealand Standard Time", "(GMT+12:00) Auckland, Wellington"), + "300" => array("Tonga Standard Time", "(GMT+13:00) Nuku'alofa"), + ); + + /** + * Python generated offset list + * dots in keys were removed + * + * Array indices + * 0 = lBias + * 1 = lStandardBias + * 2 = lDSTBias + * 3 = wStartYear + * 4 = wStartMonth + * 5 = wStartDOW + * 6 = wStartDay + * 7 = wStartHour + * 8 = wStartMinute + * 9 = wStartSecond + * 10 = wStartMilliseconds + * 11 = wEndYear + * 12 = wEndMonth + * 13 = wEndDOW + * 14 = wEndDay + * 15 = wEndHour + * 16 = wEndMinute + * 17 = wEndSecond + * 18 = wEndMilloseconds + * + * As the $tzoneoffsets and the $mstzones need to be resolved in both directions, + * some offsets are commented as they are not available in the $mstzones. + * + * Created at: 01.06.2012 + */ + private static $tzonesoffsets = array( + "Transitional Islamic State of Afghanistan Standard Time" + => array(-270, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Alaskan Standard Time" => array(540, 0, -60, 0, 11, 0, 1, 2, 0, 0, 0, 0, 3, 0, 2, 2, 0, 0, 0), + "Arab Standard Time" => array(-180, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Arabian Standard Time" => array(-240, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Arabic Standard Time" => array(-180, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + //"Argentina Standard Time" => array(180, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Atlantic Standard Time" => array(240, 0, -60, 0, 11, 0, 1, 2, 0, 0, 0, 0, 3, 0, 2, 2, 0, 0, 0), + "AUS Central Standard Time" => array(-570, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "AUS Eastern Standard Time" => array(-600, 0, -60, 0, 4, 0, 1, 3, 0, 0, 0, 0, 10, 0, 1, 2, 0, 0, 0), + //"Azerbaijan Standard Time" => array(-240, 0, -60, 0, 10, 0, 5, 5, 0, 0, 0, 0, 3, 0, 5, 4, 0, 0, 0), + "Azores Standard Time" => array(60, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + //"Bangladesh Standard Time" => array(-360, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Canada Central Standard Time" => array(360, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Cape Verde Standard Time" => array(60, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Caucasus Standard Time" => array(-240, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + "Cen Australia Standard Time" => array(-570, 0, -60, 0, 4, 0, 1, 3, 0, 0, 0, 0, 10, 0, 1, 2, 0, 0, 0), + "Central America Standard Time" => array(360, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Central Asia Standard Time" => array(-360, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + //"Central Brazilian Standard Time" => array(240, 0, -60, 0, 2, 6, 4, 23, 59, 59, 999, 0, 10, 6, 3, 23, 59, 59, 999), + "Central Europe Standard Time" => array(-60, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + "Central European Standard Time" => array(-60, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + "Central Pacific Standard Time" => array(-660, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Central Standard Time" => array(360, 0, -60, 0, 11, 0, 1, 2, 0, 0, 0, 0, 3, 0, 2, 2, 0, 0, 0), + "Mexico Standard Time" => array(360, 0, -60, 0, 10, 0, 5, 2, 0, 0, 0, 0, 4, 0, 1, 2, 0, 0, 0), + "China Standard Time" => array(-480, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Dateline Standard Time" => array(720, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "E Africa Standard Time" => array(-180, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "E Australia Standard Time" => array(-600, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "E Europe Standard Time" => array(-120, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + "E South America Standard Time" => array(180, 0, -60, 0, 2, 6, 4, 23, 59, 59, 999, 0, 10, 6, 3, 23, 59, 59, 999), + "Eastern Standard Time" => array(300, 0, -60, 0, 11, 0, 1, 2, 0, 0, 0, 0, 3, 0, 2, 2, 0, 0, 0), + "Egypt Standard Time" => array(-120, 0, -60, 0, 9, 4, 5, 23, 59, 59, 999, 0, 4, 4, 5, 23, 59, 59, 999), + "Ekaterinburg Standard Time" => array(-300, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + "Fiji Islands Standard Time" => array(-720, 0, -60, 0, 3, 0, 5, 3, 0, 0, 0, 0, 10, 0, 4, 2, 0, 0, 0), + "FLE Standard Time" => array(-120, 0, -60, 0, 10, 0, 5, 4, 0, 0, 0, 0, 3, 0, 5, 3, 0, 0, 0), + //"Georgian Standard Time" => array(-240, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "GMT Standard Time" => array(0, 0, -60, 0, 10, 0, 5, 2, 0, 0, 0, 0, 3, 0, 5, 1, 0, 0, 0), + "Greenland Standard Time" => array(180, 0, -60, 0, 10, 6, 5, 23, 0, 0, 0, 0, 3, 6, 4, 22, 0, 0, 0), + "Greenwich Standard Time" => array(0, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "GTB Standard Time" => array(-120, 0, -60, 0, 10, 0, 5, 4, 0, 0, 0, 0, 3, 0, 5, 3, 0, 0, 0), + "Hawaiian Standard Time" => array(600, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "India Standard Time" => array(-330, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Iran Standard Time" => array(-210, 0, -60, 0, 9, 1, 3, 23, 59, 59, 999, 0, 3, 6, 3, 23, 59, 59, 999), + "Israel Standard Time" => array(-120, 0, -60, 0, 9, 0, 4, 2, 0, 0, 0, 0, 3, 5, 5, 2, 0, 0, 0), + //"Jordan Standard Time" => array(-120, 0, -60, 0, 10, 5, 5, 1, 0, 0, 0, 0, 3, 4, 5, 23, 59, 59, 999), + //"Kamchatka Standard Time" => array(-720, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + "Korea Standard Time" => array(-540, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + //"Magadan Standard Time" => array(-660, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + //"Mauritius Standard Time" => array(-240, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Mid-Atlantic Standard Time" => array(120, 0, -60, 0, 9, 0, 5, 2, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + //"Middle East Standard Time" => array(-120, 0, -60, 0, 10, 6, 5, 23, 59, 59, 999, 0, 3, 6, 4, 23, 59, 59, 999), + //"Montevideo Standard Time" => array(180, 0, -60, 0, 3, 0, 2, 2, 0, 0, 0, 0, 10, 0, 1, 2, 0, 0, 0), + //"Morocco Standard Time" => array(0, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Mountain Standard Time" => array(420, 0, -60, 0, 11, 0, 1, 2, 0, 0, 0, 0, 3, 0, 2, 2, 0, 0, 0), + "Mexico Standard Time 2" => array(420, 0, -60, 0, 10, 0, 5, 2, 0, 0, 0, 0, 4, 0, 1, 2, 0, 0, 0), + "Myanmar Standard Time" => array(-390, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "N Central Asia Standard Time" => array(-360, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + //"Namibia Standard Time" => array(-60, 0, -60, 0, 4, 0, 1, 2, 0, 0, 0, 0, 9, 0, 1, 2, 0, 0, 0), + "Nepal Standard Time" => array(-345, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "New Zealand Standard Time" => array(-720, 0, -60, 0, 4, 0, 1, 3, 0, 0, 0, 0, 9, 0, 5, 2, 0, 0, 0), + "Newfoundland and Labrador Standard Time" => array(210, 0, -60, 0, 11, 0, 1, 0, 1, 0, 0, 0, 3, 0, 2, 0, 1, 0, 0), + "North Asia East Standard Time" => array(-480, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + "North Asia Standard Time" => array(-420, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + "Pacific SA Standard Time" => array(240, 0, -60, 0, 3, 6, 2, 23, 59, 59, 999, 0, 10, 6, 2, 23, 59, 59, 999), + "Pacific Standard Time" => array(480, 0, -60, 0, 11, 0, 1, 2, 0, 0, 0, 0, 3, 0, 2, 2, 0, 0, 0), + //"Pacific Standard Time (Mexico)" => array(480, 0, -60, 0, 10, 0, 5, 2, 0, 0, 0, 0, 4, 0, 1, 2, 0, 0, 0), + //"Pakistan Standard Time" => array(-300, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + //"Paraguay Standard Time" => array(240, 0, -60, 0, 4, 6, 1, 23, 59, 59, 999, 0, 10, 6, 1, 23, 59, 59, 999), + "Romance Standard Time" => array(-60, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + "Russian Standard Time" => array(-180, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + "SA Eastern Standard Time" => array(180, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "SA Pacific Standard Time" => array(300, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "SA Western Standard Time" => array(240, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Samoa Standard Time" => array(660, 0, -60, 0, 3, 6, 5, 23, 59, 59, 999, 0, 9, 6, 5, 23, 59, 59, 999), + "SE Asia Standard Time" => array(-420, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Singapore Standard Time" => array(-480, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "South Africa Standard Time" => array(-120, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Sri Lanka Standard Time" => array(-330, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + //"Syria Standard Time" => array(-120, 0, -60, 0, 10, 4, 5, 23, 59, 59, 999, 0, 4, 4, 1, 23, 59, 59, 999), + "Taipei Standard Time" => array(-480, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Tasmania Standard Time" => array(-600, 0, -60, 0, 4, 0, 1, 3, 0, 0, 0, 0, 10, 0, 1, 2, 0, 0, 0), + "Tokyo Standard Time" => array(-540, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Tonga Standard Time" => array(-780, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + //"Ulaanbaatar Standard Time" => array(-480, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "US Eastern Standard Time" => array(300, 0, -60, 0, 11, 0, 1, 2, 0, 0, 0, 0, 3, 0, 2, 2, 0, 0, 0), + "US Mountain Standard Time" => array(420, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + //"UTC" => array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + //"UTC+12" => array(-720, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + //"UTC-02" => array(120, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + //"UTC-11" => array(660, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Venezuela Standard Time" => array(270, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Vladivostok Standard Time" => array(-600, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + "W Australia Standard Time" => array(-480, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "W Central Africa Standard Time" => array(-60, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "W Europe Standard Time" => array(-60, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + "West Asia Standard Time" => array(-300, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "West Pacific Standard Time" => array(-600, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "Yakutsk Standard Time" => array(-540, 0, -60, 0, 10, 0, 5, 3, 0, 0, 0, 0, 3, 0, 5, 2, 0, 0, 0), + ); + + /** + * Generated list of PHP timezones in GMT timezones + * + * Created at: 01.06.2012 + */ + private static $phptimezones = array( + // -720 min + "Dateline Standard Time" => array( + "Etc/GMT+12", + ), + + // -660 min + "Samoa Standard Time" => array( + "Etc/GMT+11", + "Pacific/Midway", + "Pacific/Niue", + "Pacific/Pago_Pago", + "Pacific/Samoa", + "US/Samoa", + ), + + // -600 min + "Hawaiian Standard Time" => array( + "America/Adak", + "America/Atka", + "Etc/GMT+10", + "HST", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Rarotonga", + "Pacific/Tahiti", + "US/Aleutian", + "US/Hawaii", + ), + + // -570 min + "-570" => array( + "Pacific/Marquesas", + ), + + // -540 min + "Alaskan Standard Time" => array( + "America/Anchorage", + "America/Juneau", + "America/Nome", + "America/Sitka", + "America/Yakutat", + "Etc/GMT+9", + "Pacific/Gambier", + "US/Alaska", + ), + + // -480 min + "Pacific Standard Time" => array( + "America/Dawson", + "America/Ensenada", + "America/Los_Angeles", + "America/Metlakatla", + "America/Santa_Isabel", + "America/Tijuana", + "America/Vancouver", + "America/Whitehorse", + "Canada/Pacific", + "Canada/Yukon", + "Etc/GMT+8", + "Mexico/BajaNorte", + "Pacific/Pitcairn", + "PST8PDT", + "US/Pacific", + "US/Pacific-New", + ), + + // -420 min + "US Mountain Standard Time" => array( + "America/Boise", + "America/Cambridge_Bay", + "America/Chihuahua", + "America/Creston", + "America/Dawson_Creek", + "America/Denver", + "America/Edmonton", + "America/Hermosillo", + "America/Inuvik", + "America/Mazatlan", + "America/Ojinaga", + "America/Phoenix", + "America/Shiprock", + "America/Yellowknife", + "Canada/Mountain", + "Etc/GMT+7", + "Mexico/BajaSur", + "MST", + "MST7MDT", + "Navajo", + "US/Arizona", + "US/Mountain", + ), + + // -360 min + "Central Standard Time" => array( + "America/Chicago", + "America/Indiana/Knox", + "America/Indiana/Tell_City", + "America/Knox_IN", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Regina", + "America/Resolute", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Winnipeg", + "US/Central", + "US/Indiana-Starke", + "CST6CDT", + "Etc/GMT+6", + ), + "Canada Central Standard Time" => array( + "Canada/Central", + "Canada/East-Saskatchewan", + "Canada/Saskatchewan", + ), + "Mexico Standard Time" => array( + "America/Mexico_City", + "America/Monterrey", + "Mexico/General", + ), + "Central America Standard Time" => array( + "America/Bahia_Banderas", + "America/Belize", + "America/Cancun", + "America/Costa_Rica", + "America/El_Salvador", + "America/Guatemala", + "America/Managua", + "America/Matamoros", + "America/Menominee", + "America/Merida", + "Chile/EasterIsland", + "Pacific/Easter", + "Pacific/Galapagos", + ), + + // -300 min + "US Eastern Standard Time" => array( + "America/Detroit", + "America/Fort_Wayne", + "America/Grand_Turk", + "America/Indiana/Indianapolis", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Jamaica", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Louisville", + "America/Montreal", + "America/New_York", + "America/Thunder_Bay", + "America/Toronto", + "Canada/Eastern", + "Cuba", + "EST", + "EST5EDT", + "Etc/GMT+5", + "Jamaica", + "US/East-Indiana", + "US/Eastern", + "US/Michigan", + ), + "SA Pacific Standard Time" => array( + "America/Atikokan", + "America/Bogota", + "America/Cayman", + "America/Coral_Harbour", + "America/Guayaquil", + "America/Havana", + "America/Iqaluit", + "America/Lima", + "America/Nassau", + "America/Nipigon", + "America/Panama", + "America/Pangnirtung", + "America/Port-au-Prince", + ), + + // -270 min + "Venezuela Standard Time" => array( + "America/Caracas", + ), + // -240 min + "Atlantic Standard Time" => array( + "America/Barbados", + "America/Blanc-Sablon", + "America/Glace_Bay", + "America/Goose_Bay", + "America/Halifax", + "America/Lower_Princes", + "America/St_Barthelemy", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Thule", + "America/Tortola", + "America/Virgin", + "Atlantic/Bermuda", + "Canada/Atlantic", + "Etc/GMT+4", + ), + "SA Western Standard Time" => array( + "America/Anguilla", + "America/Antigua", + "America/Aruba", + "America/Asuncion", + "America/Boa_Vista", + "America/Campo_Grande", + "America/Cuiaba", + "America/Curacao", + "America/Dominica", + "America/Eirunepe", + "America/Grenada", + "America/Guadeloupe", + "America/Guyana", + "America/Kralendijk", + "America/La_Paz", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Moncton", + "America/Montserrat", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Rio_Branco", + "Brazil/Acre", + "Brazil/West", + ), + "Pacific SA Standard Time" => array( + "America/Santiago", + "America/Santo_Domingo", + "Antarctica/Palmer", + "Chile/Continental", + ), + + // -210 min + "Newfoundland and Labrador Standard Time" => array( + "America/St_Johns", + "Canada/Newfoundland", + ), + + // -180 min + "E South America Standard Time" => array( + "America/Araguaina", + "America/Bahia", + "America/Belem", + "America/Fortaleza", + "America/Maceio", + "America/Recife", + "America/Sao_Paulo", + "Brazil/East", + "Etc/GMT+3", + ), + "SA Eastern Standard Time" => array( + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Buenos_Aires", + "America/Catamarca", + "America/Cayenne", + "America/Cordoba", + "America/Godthab", + "America/Jujuy", + "America/Mendoza", + "America/Miquelon", + "America/Montevideo", + "America/Paramaribo", + "America/Rosario", + "America/Santarem", + ), + "Greenland Standard Time" => array( + "Antarctica/Rothera", + "Atlantic/Stanley", + ), + + // -120 min + "Mid-Atlantic Standard Time" => array( + "America/Noronha", + "Atlantic/South_Georgia", + "Brazil/DeNoronha", + "Etc/GMT+2", + ), + + // -60 min + "Azores Standard Time" => array( + "Atlantic/Azores", + "Etc/GMT+1", + ), + "Cape Verde Standard Time" => array( + "America/Scoresbysund", + "Atlantic/Cape_Verde", + ), + + // 0 min + "GMT Standard Time" => array( + "Eire", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT-0", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/Universal", + "Etc/UTC", + "Etc/Zulu", + "Europe/Belfast", + "Europe/Dublin", + "Europe/Guernsey", + "Europe/Isle_of_Man", + "Europe/Jersey", + "Europe/Lisbon", + "Europe/London", + "Factory", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "Iceland", + "Portugal", + "UCT", + "Universal", + "UTC", + ), + "Greenwich Standard Time" => array( + "Africa/Abidjan", + "Africa/Accra", + "Africa/Bamako", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Casablanca", + "Africa/Conakry", + "Africa/Dakar", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Lome", + "Africa/Monrovia", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "America/Danmarkshavn", + "Atlantic/Canary", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/St_Helena", + "Zulu", + ), + + // +60 min + "Central Europe Standard Time" => array( + "Europe/Belgrade", + "Europe/Bratislava", + "Europe/Budapest", + "Europe/Ljubljana", + "Europe/Prague", + "Europe/Vaduz", + ), + "Central European Standard Time" => array( + "Europe/Sarajevo", + "Europe/Skopje", + "Europe/Warsaw", + "Europe/Zagreb", + "MET", + "Poland", + ), + "Romance Standard Time" => array( + "Europe/Andorra", + "Europe/Brussels", + "Europe/Copenhagen", + "Europe/Gibraltar", + "Europe/Madrid", + "Europe/Malta", + "Europe/Monaco", + "Europe/Paris", + "Europe/Podgorica", + "Europe/San_Marino", + "Europe/Tirane", + ), + "W Europe Standard Time" => array( + "Europe/Amsterdam", + "Europe/Berlin", + "Europe/Luxembourg", + "Europe/Vatican", + "Europe/Rome", + "Europe/Stockholm", + "Arctic/Longyearbyen", + "Europe/Vienna", + "Europe/Zurich", + "Europe/Oslo", + "WET", + "CET", + "Etc/GMT-1", + ), + "W Central Africa Standard Time" => array( + "Africa/Algiers", + "Africa/Bangui", + "Africa/Brazzaville", + "Africa/Ceuta", + "Africa/Douala", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Luanda", + "Africa/Malabo", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Porto-Novo", + "Africa/Tunis", + "Africa/Windhoek", + "Atlantic/Jan_Mayen", + ), + + // +120 min + "E Europe Standard Time" => array( + "Europe/Bucharest", + "EET", + "Etc/GMT-2", + "Europe/Chisinau", + "Europe/Mariehamn", + "Europe/Nicosia", + "Europe/Simferopol", + "Europe/Tiraspol", + "Europe/Uzhgorod", + "Europe/Zaporozhye", + ), + "Egypt Standard Time" => array( + "Africa/Cairo", + "Africa/Tripoli", + "Egypt", + "Libya", + ), + "FLE Standard Time" => array( + "Europe/Helsinki", + "Europe/Kiev", + "Europe/Riga", + "Europe/Sofia", + "Europe/Tallinn", + "Europe/Vilnius", + ), + "GTB Standard Time" => array( + "Asia/Istanbul", + "Europe/Athens", + "Europe/Istanbul", + "Turkey", + ), + "Israel Standard Time" => array( + "Asia/Amman", + "Asia/Beirut", + "Asia/Damascus", + "Asia/Gaza", + "Asia/Hebron", + "Asia/Nicosia", + "Asia/Tel_Aviv", + "Asia/Jerusalem", + "Israel", + ), + "South Africa Standard Time" => array( + "Africa/Blantyre", + "Africa/Bujumbura", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Kigali", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + ), + + // +180 min + "Russian Standard Time" => array( + "Antarctica/Syowa", + "Europe/Kaliningrad", + "Europe/Minsk", + "Etc/GMT-3", + ), + "Arab Standard Time" => array( + "Asia/Qatar", + "Asia/Kuwait", + "Asia/Riyadh", + ), + "E Africa Standard Time" => array( + "Africa/Addis_Ababa", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Mogadishu", + "Africa/Nairobi", + ), + "Arabic Standard Time" => array( + "Asia/Aden", + "Asia/Baghdad", + "Asia/Bahrain", + "Indian/Antananarivo", + "Indian/Comoro", + "Indian/Mayotte", + ), + + // +210 min + "Iran Standard Time" => array( + "Asia/Tehran", + "Iran", + ), + + // +240 min + "Arabian Standard Time" => array( + "Asia/Dubai", + "Asia/Muscat", + "Indian/Mahe", + "Indian/Mauritius", + "Indian/Reunion", + ), + "Caucasus Standard Time" => array( + "Asia/Baku", + "Asia/Tbilisi", + "Asia/Yerevan", + "Etc/GMT-4", + "Europe/Moscow", + "Europe/Samara", + "Europe/Volgograd", + "W-SU", + ), + + // +270 min + "Transitional Islamic State of Afghanistan Standard Time" => array( + "Asia/Kabul", + ), + + // +300 min + "Ekaterinburg Standard Time" => array( + "Antarctica/Mawson", + ), + "West Asia Standard Time" => array( + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Dushanbe", + "Asia/Karachi", + "Asia/Oral", + "Asia/Samarkand", + "Asia/Tashkent", + "Etc/GMT-5", + "Indian/Kerguelen", + "Indian/Maldives", + ), + + // +330 min + "India Standard Time" => array( + "Asia/Calcutta", + "Asia/Colombo", + "Asia/Kolkata", + ), + + // +345 min + "Nepal Standard Time" => array( + "Asia/Kathmandu", + "Asia/Katmandu", + ), + + // +360 min + "Central Asia Standard Time" => array( + "Asia/Dacca", + "Asia/Dhaka", + ), + "Sri Lanka Standard Time" => array( + "Indian/Chagos", + ), + "N Central Asia Standard Time" => array( + "Antarctica/Vostok", + "Asia/Almaty", + "Asia/Bishkek", + "Asia/Qyzylorda", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Yekaterinburg", + "Etc/GMT-6", + ), + + // +390 min + "Myanmar Standard Time" => array( + "Asia/Rangoon", + "Indian/Cocos", + ), + + // +420 min + "SE Asia Standard Time" => array( + "Asia/Bangkok", + "Asia/Ho_Chi_Minh", + "Asia/Hovd", + "Asia/Jakarta", + "Asia/Phnom_Penh", + "Asia/Saigon", + "Indian/Christmas", + ), + "North Asia Standard Time" => array( + "Antarctica/Davis", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Pontianak", + "Asia/Vientiane", + "Etc/GMT-7", + ), + + // +480 min + "China Standard Time" => array( + "Asia/Brunei", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Harbin", + "Asia/Hong_Kong", + "Asia/Shanghai", + "Asia/Ujung_Pandang", + "Asia/Urumqi", + "Hongkong", + "PRC", + "ROC", + ), + "Singapore Standard Time" => array( + "Singapore", + "Asia/Singapore", + "Asia/Kuala_Lumpur", + ), + "Taipei Standard Time" => array( + "Asia/Taipei", + ), + "W Australia Standard Time" => array( + "Australia/Perth", + "Australia/West", + ), + "North Asia East Standard Time" => array( + "Antarctica/Casey", + "Asia/Kashgar", + "Asia/Krasnoyarsk", + "Asia/Kuching", + "Asia/Macao", + "Asia/Macau", + "Asia/Makassar", + "Asia/Manila", + "Etc/GMT-8", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + ), + + // +525 min + "525" => array( + "Australia/Eucla", + ), + + // +540 min + "Korea Standard Time" => array( + "Asia/Seoul", + "Asia/Pyongyang", + "ROK", + ), + "Tokyo Standard Time" => array( + "Asia/Tokyo", + "Japan", + "Etc/GMT-9", + ), + "Yakutsk Standard Time" => array( + "Asia/Dili", + "Asia/Irkutsk", + "Asia/Jayapura", + "Pacific/Palau", + ), + + // +570 min + "AUS Central Standard Time" => array( + "Australia/Darwin", + "Australia/North", + ), + // DST + "Cen Australia Standard Time" => array( + "Australia/Adelaide", + "Australia/Broken_Hill", + "Australia/South", + "Australia/Yancowinna", + ), + + // +600 min + "AUS Eastern Standard Time" => array( + "Australia/Canberra", + "Australia/Melbourne", + "Australia/Sydney", + "Australia/Currie", + "Australia/ACT", + "Australia/NSW", + "Australia/Victoria", + ), + "E Australia Standard Time" => array( + "Etc/GMT-10", + "Australia/Brisbane", + "Australia/Queensland", + "Australia/Lindeman", + ), + "Tasmania Standard Time" => array( + "Australia/Hobart", + "Australia/Tasmania", + ), + "Vladivostok Standard Time" => array( + "Antarctica/DumontDUrville", + ), + "West Pacific Standard Time" => array( + "Asia/Yakutsk", + "Pacific/Chuuk", + "Pacific/Guam", + "Pacific/Port_Moresby", + "Pacific/Saipan", + "Pacific/Truk", + "Pacific/Yap", + ), + + // +630 min + "630" => array( + "Australia/LHI", + "Australia/Lord_Howe", + ), + + // +660 min + "Central Pacific Standard Time" => array( + "Antarctica/Macquarie", + "Asia/Sakhalin", + "Asia/Vladivostok", + "Etc/GMT-11", + "Pacific/Efate", + "Pacific/Guadalcanal", + "Pacific/Kosrae", + "Pacific/Noumea", + "Pacific/Pohnpei", + "Pacific/Ponape", + ), + + // 690 min + "690" => array( + "Pacific/Norfolk", + ), + + // +720 min + "Fiji Islands Standard Time" => array( + "Asia/Anadyr", + "Asia/Kamchatka", + "Asia/Magadan", + "Kwajalein", + ), + "New Zealand Standard Time" => array( + "Antarctica/McMurdo", + "Antarctica/South_Pole", + "Etc/GMT-12", + "NZ", + "Pacific/Auckland", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Nauru", + "Pacific/Tarawa", + "Pacific/Wake", + "Pacific/Wallis", + ), + + // +765 min + "765" => array( + "NZ-CHAT", + "Pacific/Chatham", + ), + + // +780 min + "Tonga Standard Time" => array( + "Etc/GMT-13", + "Pacific/Apia", + "Pacific/Enderbury", + "Pacific/Tongatapu", + ), + + // +840 min + "840" => array( + "Etc/GMT-14", + "Pacific/Fakaofo", + "Pacific/Kiritimati", + ), + ); + + /** + * Returns a full timezone array + * + * @param string $phptimezone (opt) a php timezone string. + * If omitted the env. default timezone is used. + * + * @access public + * @return array + */ + static public function GetFullTZ($phptimezone = false) { + if ($phptimezone === false) + $phptimezone = date_default_timezone_get(); + + ZLog::Write(LOGLEVEL_DEBUG, "TimezoneUtil::GetFullTZ() for ". $phptimezone); + + $servertzname = self::guessTZNameFromPHPName($phptimezone); + $offset = self::$tzonesoffsets[$servertzname]; + + $tz = array( + "bias" => $offset[0], + "tzname" => self::encodeTZName(self::getMSTZnameFromTZName($servertzname)), + "dstendyear" => $offset[3], + "dstendmonth" => $offset[4], + "dstendday" => $offset[6], + "dstendweek" => $offset[5], + "dstendhour" => $offset[7], + "dstendminute" => $offset[8], + "dstendsecond" => $offset[9], + "dstendmillis" => $offset[10], + "stdbias" => $offset[1], + "tznamedst" => self::encodeTZName(self::getMSTZnameFromTZName($servertzname)), + "dststartyear" => $offset[11], + "dststartmonth" => $offset[12], + "dststartday" => $offset[14], + "dststartweek" => $offset[13], + "dststarthour" => $offset[15], + "dststartminute" => $offset[16], + "dststartsecond" => $offset[17], + "dststartmillis" => $offset[18], + "dstbias" => $offset[2] + ); + + return $tz; + } + + /** + * Sets the timezone name by matching data from the offset (bias etc) + * + * @param array $offset a z-push timezone array + * + * @access public + * @return array + */ + static public function FillTZNames($tz) { + ZLog::Write(LOGLEVEL_DEBUG, "TimezoneUtil::FillTZNames() filling up bias ". $tz["bias"]); + if (!isset($tz["bias"])) + ZLog::Write(LOGLEVEL_WARN, "TimezoneUtil::FillTZNames() submitted TZ array does not have a bias"); + else { + $tzname = self::guessTZNameFromOffset($tz); + $tz['tzname'] = $tz['tznamedst'] = self::encodeTZName(self::getMSTZnameFromTZName($tzname)); + } + return $tz; + } + + /** + * Tries to find a timezone using the Bias and other offset parameters + * + * @param array $offset a z-push timezone array + * + * @access public + * @return string + */ + static private function guessTZNameFromOffset($offset) { + // try to find a quite exact match + foreach (self::$tzonesoffsets as $tzname => $tzoffset) { + if ($offset["bias"] == $tzoffset[0] && + isset($offset["dstendmonth"]) && $offset["dstendmonth"] == $tzoffset[4] && + isset($offset["dstendday"]) && $offset["dstendday"] == $tzoffset[6] && + isset($offset["dststartmonth"]) && $offset["dststartmonth"] == $tzoffset[12] && + isset($offset["dststartday"]) && $offset["dststartday"] == $tzoffset[14]) + return $tzname; + } + + // try to find a bias match + foreach (self::$tzonesoffsets as $tzname => $tzoffset) { + if ($offset["bias"] == $tzoffset[0]) + return $tzname; + } + + // nothing found? return gmt + ZLog::Write(LOGLEVEL_WARN, "TimezoneUtil::guessTZNameFromOffset() no timezone found for the data submitted. Returning 'GMT Standard Time'."); + return "GMT Standard Time"; + } + + /** + * Tries to find a AS timezone for a php timezone + * + * @param string $phpname a php timezone name + * + * @access public + * @return string + */ + static private function guessTZNameFromPHPName($phpname) { + foreach (self::$phptimezones as $tzn => $phptzs) { + if (in_array($phpname, $phptzs)) { + $tzname = $tzn; + break; + } + } + + if (!isset($tzname) || is_int($tzname)) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("TimezoneUtil::guessTZNameFromPHPName() no compatible timezone found for '%s'. Returning 'GMT Standard Time'. Please contact the Z-Push dev team.", $phpname)); + return self::$mstzones["085"][0]; + } + + return $tzname; + } + + /** + * Returns an AS compatible tz name + * + * @param string $name internal timezone name + * + * @access public + * @return string + */ + static private function getMSTZnameFromTZName($name) { + foreach (self::$mstzones as $mskey => $msdefs) { + if ($name == $msdefs[0]) + return $msdefs[1]; + } + + ZLog::Write(LOGLEVEL_WARN, sprintf("TimezoneUtil::getMSTZnameFromTZName() no MS name found for '%s'. Returning '(GMT) Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London'", $name)); + return self::$mstzones["085"][1]; + } + + /** + * Encodes the tz name to UTF-16 compatible with a syncblob + * + * @param string $name timezone name + * + * @access public + * @return string + */ + static private function encodeTZName($name) { + return substr(iconv('UTF-8', 'UTF-16', $name),2,-1); + } + + /** + * Test to check if $mstzones and $tzonesoffsets can be resolved + * in both directions. + * + * @access public + * @return + */ + static public function TZtest() { + foreach (self::$mstzones as $mskey => $msdefs) { + if (!array_key_exists($msdefs[0], self::$tzonesoffsets)) + echo "key '". $msdefs[0]. "' not found in tzonesoffsets\n"; + } + + foreach (self::$tzonesoffsets as $tzname => $offset) { + $found = false; + foreach (self::$mstzones as $mskey => $msdefs) { + if ($tzname == $msdefs[0]) { + $found = true; + break; + } + } + if (!$found) + echo "key '$tzname' NOT FOUND\n"; + } + } + +} + +?> \ No newline at end of file diff --git a/sources/lib/utils/utils.php b/sources/lib/utils/utils.php new file mode 100644 index 0000000..aadcec6 --- /dev/null +++ b/sources/lib/utils/utils.php @@ -0,0 +1,957 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class Utils { + /** + * Prints a variable as string + * If a boolean is sent, 'true' or 'false' is displayed + * + * @param string $var + * @access public + * @return string + */ + static public function PrintAsString($var) { + return ($var)?(($var===true)?'true':$var):(($var===false)?'false':(($var==='')?'empty':$var)); +//return ($var)?(($var===true)?'true':$var):'false'; + } + + /** + * Splits a "domain\user" string into two values + * If the string cotains only the user, domain is returned empty + * + * @param string $domainuser + * + * @access public + * @return array index 0: user 1: domain + */ + static public function SplitDomainUser($domainuser) { + $pos = strrpos($domainuser, '\\'); + if($pos === false){ + $user = $domainuser; + $domain = ''; + } + else{ + $domain = substr($domainuser,0,$pos); + $user = substr($domainuser,$pos+1); + } + return array($user, $domain); + } + + /** + * Build an address string from the components + * + * @param string $street the street + * @param string $zip the zip code + * @param string $city the city + * @param string $state the state + * @param string $country the country + * + * @access public + * @return string the address string or null + */ + static public function BuildAddressString($street, $zip, $city, $state, $country) { + $out = ""; + + if (isset($country) && $street != "") $out = $country; + + $zcs = ""; + if (isset($zip) && $zip != "") $zcs = $zip; + if (isset($city) && $city != "") $zcs .= (($zcs)?" ":"") . $city; + if (isset($state) && $state != "") $zcs .= (($zcs)?" ":"") . $state; + if ($zcs) $out = $zcs . "\r\n" . $out; + + if (isset($street) && $street != "") $out = $street . (($out)?"\r\n\r\n". $out: "") ; + + return ($out)?$out:null; + } + + /** + * Build the fileas string from the components according to the configuration. + * + * @param string $lastname + * @param string $firstname + * @param string $middlename + * @param string $company + * + * @access public + * @return string fileas + */ + static public function BuildFileAs($lastname = "", $firstname = "", $middlename = "", $company = "") { + if (defined('FILEAS_ORDER')) { + $fileas = $lastfirst = $firstlast = ""; + $names = trim ($firstname . " " . $middlename); + $lastname = trim($lastname); + $company = trim($company); + + // lastfirst is "lastname, firstname middlename" + // firstlast is "firstname middlename lastname" + if (strlen($lastname) > 0) { + $lastfirst = $lastname; + if (strlen($names) > 0){ + $lastfirst .= ", $names"; + $firstlast = "$names $lastname"; + } + else { + $firstlast = $lastname; + } + } + elseif (strlen($names) > 0) { + $lastfirst = $firstlast = $names; + } + + // if fileas with a company is selected + // but company is emtpy then it will + // fallback to firstlast or lastfirst + // (depending on which is selected for company) + switch (FILEAS_ORDER) { + case SYNC_FILEAS_COMPANYONLY: + if (strlen($company) > 0) { + $fileas = $company; + } + elseif (strlen($firstlast) > 0) + $fileas = $lastfirst; + break; + case SYNC_FILEAS_COMPANYLAST: + if (strlen($company) > 0) { + $fileas = $company; + if (strlen($lastfirst) > 0) + $fileas .= "($lastfirst)"; + } + elseif (strlen($lastfirst) > 0) + $fileas = $lastfirst; + break; + case SYNC_FILEAS_COMPANYFIRST: + if (strlen($company) > 0) { + $fileas = $company; + if (strlen($firstlast) > 0) { + $fileas .= " ($firstlast)"; + } + } + elseif (strlen($firstlast) > 0) { + $fileas = $firstlast; + } + break; + case SYNC_FILEAS_FIRSTCOMPANY: + if (strlen($firstlast) > 0) { + $fileas = $firstlast; + if (strlen($company) > 0) { + $fileas .= " ($company)"; + } + } + elseif (strlen($company) > 0) { + $fileas = $company; + } + break; + case SYNC_FILEAS_LASTCOMPANY: + if (strlen($lastfirst) > 0) { + $fileas = $lastfirst; + if (strlen($company) > 0) { + $fileas .= " ($company)"; + } + } + elseif (strlen($company) > 0) { + $fileas = $company; + } + break; + case SYNC_FILEAS_LASTFIRST: + if (strlen($lastfirst) > 0) { + $fileas = $lastfirst; + } + break; + default: + $fileas = $firstlast; + break; + } + if (strlen($fileas) == 0) + ZLog::Write(LOGLEVEL_DEBUG, "Fileas is empty."); + return $fileas; + } + ZLog::Write(LOGLEVEL_DEBUG, "FILEAS_ORDER not defined. Add it to your config.php."); + return null; + } + /** + * Checks if the PHP-MAPI extension is available and in a requested version + * + * @param string $version the version to be checked ("6.30.10-18495", parts or build number) + * + * @access public + * @return boolean installed version is superior to the checked strin + */ + static public function CheckMapiExtVersion($version = "") { + // compare build number if requested + if (preg_match('/^\d+$/', $version) && strlen($version) > 3) { + $vs = preg_split('/-/', phpversion("mapi")); + return ($version <= $vs[1]); + } + + if (extension_loaded("mapi")){ + if (version_compare(phpversion("mapi"), $version) == -1){ + return false; + } + } + else + return false; + + return true; + } + + /** + * Parses and returns an ecoded vCal-Uid from an + * OL compatible GlobalObjectID + * + * @param string $olUid an OL compatible GlobalObjectID + * + * @access public + * @return string the vCal-Uid if available in the olUid, else the original olUid as HEX + */ + static public function GetICalUidFromOLUid($olUid){ + //check if "vCal-Uid" is somewhere in outlookid case-insensitive + $icalUid = stristr($olUid, "vCal-Uid"); + if ($icalUid !== false) { + //get the length of the ical id - go back 4 position from where "vCal-Uid" was found + $begin = unpack("V", substr($olUid, strlen($icalUid) * (-1) - 4, 4)); + //remove "vCal-Uid" and packed "1" and use the ical id length + return substr($icalUid, 12, ($begin[1] - 13)); + } + return strtoupper(bin2hex($olUid)); + } + + /** + * Checks the given UID if it is an OL compatible GlobalObjectID + * If not, the given UID is encoded inside the GlobalObjectID + * + * @param string $icalUid an appointment uid as HEX + * + * @access public + * @return string an OL compatible GlobalObjectID + * + */ + static public function GetOLUidFromICalUid($icalUid) { + if (strlen($icalUid) <= 64) { + $len = 13 + strlen($icalUid); + $OLUid = pack("V", $len); + $OLUid .= "vCal-Uid"; + $OLUid .= pack("V", 1); + $OLUid .= $icalUid; + return hex2bin("040000008200E00074C5B7101A82E0080000000000000000000000000000000000000000". bin2hex($OLUid). "00"); + } + else + return hex2bin($icalUid); + } + + /** + * Extracts the basedate of the GlobalObjectID and the RecurStartTime + * + * @param string $goid OL compatible GlobalObjectID + * @param long $recurStartTime + * + * @access public + * @return long basedate + */ + static public function ExtractBaseDate($goid, $recurStartTime) { + $hexbase = substr(bin2hex($goid), 32, 8); + $day = hexdec(substr($hexbase, 6, 2)); + $month = hexdec(substr($hexbase, 4, 2)); + $year = hexdec(substr($hexbase, 0, 4)); + + if ($day && $month && $year) { + $h = $recurStartTime >> 12; + $m = ($recurStartTime - $h * 4096) >> 6; + $s = $recurStartTime - $h * 4096 - $m * 64; + + return gmmktime($h, $m, $s, $month, $day, $year); + } + else + return false; + } + + /** + * Converts SYNC_FILTERTYPE into a timestamp + * + * @param int Filtertype + * + * @access public + * @return long + */ + static public function GetCutOffDate($restrict) { + switch($restrict) { + case SYNC_FILTERTYPE_1DAY: + $back = 60 * 60 * 24; + break; + case SYNC_FILTERTYPE_3DAYS: + $back = 60 * 60 * 24 * 3; + break; + case SYNC_FILTERTYPE_1WEEK: + $back = 60 * 60 * 24 * 7; + break; + case SYNC_FILTERTYPE_2WEEKS: + $back = 60 * 60 * 24 * 14; + break; + case SYNC_FILTERTYPE_1MONTH: + $back = 60 * 60 * 24 * 31; + break; + case SYNC_FILTERTYPE_3MONTHS: + $back = 60 * 60 * 24 * 31 * 3; + break; + case SYNC_FILTERTYPE_6MONTHS: + $back = 60 * 60 * 24 * 31 * 6; + break; + default: + break; + } + + if(isset($back)) { + $date = time() - $back; + return $date; + } else + return 0; // unlimited + } + + /** + * Converts SYNC_TRUNCATION into bytes + * + * @param int SYNC_TRUNCATION + * + * @return long + */ + static public function GetTruncSize($truncation) { + switch($truncation) { + case SYNC_TRUNCATION_HEADERS: + return 0; + case SYNC_TRUNCATION_512B: + return 512; + case SYNC_TRUNCATION_1K: + return 1024; + case SYNC_TRUNCATION_2K: + return 2*1024; + case SYNC_TRUNCATION_5K: + return 5*1024; + case SYNC_TRUNCATION_10K: + return 10*1024; + case SYNC_TRUNCATION_20K: + return 20*1024; + case SYNC_TRUNCATION_50K: + return 50*1024; + case SYNC_TRUNCATION_100K: + return 100*1024; + case SYNC_TRUNCATION_ALL: + return 1024*1024; // We'll limit to 1MB anyway + default: + return 1024; // Default to 1Kb + } + } + + /** + * Truncate an UTF-8 encoded sting correctly + * + * If it's not possible to truncate properly, an empty string is returned + * + * @param string $string - the string + * @param string $length - position where string should be cut + * @return string truncated string + */ + static public function Utf8_truncate($string, $length) { + // make sure length is always an interger + $length = (int)$length; + + if (strlen($string) <= $length) + return $string; + + while($length >= 0) { + if ((ord($string[$length]) < 0x80) || (ord($string[$length]) >= 0xC0)) + return substr($string, 0, $length); + + $length--; + } + return ""; + } + + /** + * Indicates if the specified folder type is a system folder + * + * @param int $foldertype + * + * @access public + * @return boolean + */ + static public function IsSystemFolder($foldertype) { + return ($foldertype == SYNC_FOLDER_TYPE_INBOX || $foldertype == SYNC_FOLDER_TYPE_DRAFTS || $foldertype == SYNC_FOLDER_TYPE_WASTEBASKET || $foldertype == SYNC_FOLDER_TYPE_SENTMAIL || + $foldertype == SYNC_FOLDER_TYPE_OUTBOX || $foldertype == SYNC_FOLDER_TYPE_TASK || $foldertype == SYNC_FOLDER_TYPE_APPOINTMENT || $foldertype == SYNC_FOLDER_TYPE_CONTACT || + $foldertype == SYNC_FOLDER_TYPE_NOTE || $foldertype == SYNC_FOLDER_TYPE_JOURNAL) ? true:false; + } + + /** + * Our own utf7_decode function because imap_utf7_decode converts a string + * into ISO-8859-1 encoding which doesn't have euro sign (it will be converted + * into two chars: [space](ascii 32) and "¬" ("not sign", ascii 172)). Also + * php iconv function expects '+' as delimiter instead of '&' like in IMAP. + * + * @param string $string IMAP folder name + * + * @access public + * @return string + */ + static public function Utf7_iconv_decode($string) { + //do not alter string if there aren't any '&' or '+' chars because + //it won't have any utf7-encoded chars and nothing has to be escaped. + if (strpos($string, '&') === false && strpos($string, '+') === false ) return $string; + + //Get the string length and go back through it making the replacements + //necessary + $len = strlen($string) - 1; + while ($len > 0) { + //look for '&-' sequence and replace it with '&' + if ($len > 0 && $string{($len-1)} == '&' && $string{$len} == '-') { + $string = substr_replace($string, '&', $len - 1, 2); + $len--; //decrease $len as this char has alreasy been processed + } + //search for '&' which weren't found in if clause above and + //replace them with '+' as they mark an utf7-encoded char + if ($len > 0 && $string{($len-1)} == '&') { + $string = substr_replace($string, '+', $len - 1, 1); + $len--; //decrease $len as this char has alreasy been processed + } + //finally "escape" all remaining '+' chars + if ($len > 0 && $string{($len-1)} == '+') { + $string = substr_replace($string, '+-', $len - 1, 1); + } + $len--; + } + return $string; + } + + /** + * Our own utf7_encode function because the string has to be converted from + * standard UTF7 into modified UTF7 (aka UTF7-IMAP). + * + * @param string $str IMAP folder name + * + * @access public + * @return string + */ + static public function Utf7_iconv_encode($string) { + //do not alter string if there aren't any '&' or '+' chars because + //it won't have any utf7-encoded chars and nothing has to be escaped. + if (strpos($string, '&') === false && strpos($string, '+') === false ) return $string; + + //Get the string length and go back through it making the replacements + //necessary + $len = strlen($string) - 1; + while ($len > 0) { + //look for '&-' sequence and replace it with '&' + if ($len > 0 && $string{($len-1)} == '+' && $string{$len} == '-') { + $string = substr_replace($string, '+', $len - 1, 2); + $len--; //decrease $len as this char has alreasy been processed + } + //search for '&' which weren't found in if clause above and + //replace them with '+' as they mark an utf7-encoded char + if ($len > 0 && $string{($len-1)} == '+') { + $string = substr_replace($string, '&', $len - 1, 1); + $len--; //decrease $len as this char has alreasy been processed + } + //finally "escape" all remaining '+' chars + if ($len > 0 && $string{($len-1)} == '&') { + $string = substr_replace($string, '&-', $len - 1, 1); + } + $len--; + } + return $string; + } + + /** + * Converts an UTF-7 encoded string into an UTF-8 string. + * + * @param string $string to convert + * + * @access public + * @return string + */ + static public function Utf7_to_utf8($string) { + if (function_exists("iconv")){ + return @iconv("UTF-7", "UTF-8", $string); + } + else + ZLog::Write(LOGLEVEL_WARN, "Utils::Utf7_to_utf8() 'iconv' is not available. Charset conversion skipped."); + + return $string; + } + + /** + * Converts an UTF-8 encoded string into an UTF-7 string. + * + * @param string $string to convert + * + * @access public + * @return string + */ + static public function Utf8_to_utf7($string) { + if (function_exists("iconv")){ + return @iconv("UTF-8", "UTF-7", $string); + } + else + ZLog::Write(LOGLEVEL_WARN, "Utils::Utf8_to_utf7() 'iconv' is not available. Charset conversion skipped."); + + return $string; + } + + /** + * Checks for valid email addresses + * The used regex actually only checks if a valid email address is part of the submitted string + * it also returns true for the mailbox format, but this is not checked explicitly + * + * @param string $email address to be checked + * + * @access public + * @return boolean + */ + static public function CheckEmail($email) { + return (bool) preg_match('#([a-zA-Z0-9_\-])+(\.([a-zA-Z0-9_\-])+)*@((\[(((([0-1])?([0-9])?[0-9])|(2[0-4][0-9])|(2[0-5][0-5])))\.(((([0-1])?([0-9])?[0-9])|(2[0-4][0-9])|(2[0-5][0-5])))\.(((([0-1])?([0-9])?[0-9])|(2[0-4][0-9])|(2[0-5][0-5])))\.(((([0-1])?([0-9])?[0-9])|(2[0-4][0-9])|(2[0-5][0-5]))\]))|((([a-zA-Z0-9])+(([\-])+([a-zA-Z0-9])+)*\.)+([a-zA-Z])+(([\-])+([a-zA-Z0-9])+)*)|localhost)#', $email); + } + + /** + * Checks if a string is base64 encoded + * + * @param string $string the string to be checked + * + * @access public + * @return boolean + */ + static public function IsBase64String($string) { + return (bool) preg_match("#^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+/]{4})?$#", $string); + } + + /** + * Decodes base64 encoded query parameters. Based on dw2412 contribution. + * + * @param string $query the query to decode + * + * @access public + * @return array + */ + static public function DecodeBase64URI($query) { + /* + * The query string has a following structure. Number in () is position: + * 1 byte - protocoll version (0) + * 1 byte - command code (1) + * 2 bytes - locale (2) + * 1 byte - device ID length (4) + * variable - device ID (4+device ID length) + * 1 byte - policy key length (5+device ID length) + * 0 or 4 bytes - policy key (5+device ID length + policy key length) + * 1 byte - device type length (6+device ID length + policy key length) + * variable - device type (6+device ID length + policy key length + device type length) + * variable - command parameters, array which consists of: + * 1 byte - tag + * 1 byte - length + * variable - value of the parameter + * + */ + $decoded = base64_decode($query); + $devIdLength = ord($decoded[4]); //device ID length + $polKeyLength = ord($decoded[5+$devIdLength]); //policy key length + $devTypeLength = ord($decoded[6+$devIdLength+$polKeyLength]); //device type length + //unpack the decoded query string values + $unpackedQuery = unpack("CProtVer/CCommand/vLocale/CDevIDLen/H".($devIdLength*2)."DevID/CPolKeyLen".($polKeyLength == 4 ? "/VPolKey" : "")."/CDevTypeLen/A".($devTypeLength)."DevType", $decoded); + + //get the command parameters + $pos = 7 + $devIdLength + $polKeyLength + $devTypeLength; + $decoded = substr($decoded, $pos); + while (strlen($decoded) > 0) { + $paramLength = ord($decoded[1]); + $unpackedParam = unpack("CParamTag/CParamLength/A".$paramLength."ParamValue", $decoded); + $unpackedQuery[ord($decoded[0])] = $unpackedParam['ParamValue']; + //remove parameter from decoded query string + $decoded = substr($decoded, 2 + $paramLength); + } + return $unpackedQuery; + } + + /** + * Returns a command string for a given command code. + * + * @param int $code + * + * @access public + * @return string or false if code is unknown + */ + public static function GetCommandFromCode($code) { + switch ($code) { + case ZPush::COMMAND_SYNC: return 'Sync'; + case ZPush::COMMAND_SENDMAIL: return 'SendMail'; + case ZPush::COMMAND_SMARTFORWARD: return 'SmartForward'; + case ZPush::COMMAND_SMARTREPLY: return 'SmartReply'; + case ZPush::COMMAND_GETATTACHMENT: return 'GetAttachment'; + case ZPush::COMMAND_FOLDERSYNC: return 'FolderSync'; + case ZPush::COMMAND_FOLDERCREATE: return 'FolderCreate'; + case ZPush::COMMAND_FOLDERDELETE: return 'FolderDelete'; + case ZPush::COMMAND_FOLDERUPDATE: return 'FolderUpdate'; + case ZPush::COMMAND_MOVEITEMS: return 'MoveItems'; + case ZPush::COMMAND_GETITEMESTIMATE: return 'GetItemEstimate'; + case ZPush::COMMAND_MEETINGRESPONSE: return 'MeetingResponse'; + case ZPush::COMMAND_SEARCH: return 'Search'; + case ZPush::COMMAND_SETTINGS: return 'Settings'; + case ZPush::COMMAND_PING: return 'Ping'; + case ZPush::COMMAND_ITEMOPERATIONS: return 'ItemOperations'; + case ZPush::COMMAND_PROVISION: return 'Provision'; + case ZPush::COMMAND_RESOLVERECIPIENTS: return 'ResolveRecipients'; + case ZPush::COMMAND_VALIDATECERT: return 'ValidateCert'; + + // Deprecated commands + case ZPush::COMMAND_GETHIERARCHY: return 'GetHierarchy'; + case ZPush::COMMAND_CREATECOLLECTION: return 'CreateCollection'; + case ZPush::COMMAND_DELETECOLLECTION: return 'DeleteCollection'; + case ZPush::COMMAND_MOVECOLLECTION: return 'MoveCollection'; + case ZPush::COMMAND_NOTIFY: return 'Notify'; + + // Webservice commands + case ZPush::COMMAND_WEBSERVICE_DEVICE: return 'WebserviceDevice'; + case ZPush::COMMAND_WEBSERVICE_USERS: return 'WebserviceUsers'; + } + return false; + } + + /** + * Returns a command code for a given command. + * + * @param string $command + * + * @access public + * @return int or false if command is unknown + */ + public static function GetCodeFromCommand($command) { + switch ($command) { + case 'Sync': return ZPush::COMMAND_SYNC; + case 'SendMail': return ZPush::COMMAND_SENDMAIL; + case 'SmartForward': return ZPush::COMMAND_SMARTFORWARD; + case 'SmartReply': return ZPush::COMMAND_SMARTREPLY; + case 'GetAttachment': return ZPush::COMMAND_GETATTACHMENT; + case 'FolderSync': return ZPush::COMMAND_FOLDERSYNC; + case 'FolderCreate': return ZPush::COMMAND_FOLDERCREATE; + case 'FolderDelete': return ZPush::COMMAND_FOLDERDELETE; + case 'FolderUpdate': return ZPush::COMMAND_FOLDERUPDATE; + case 'MoveItems': return ZPush::COMMAND_MOVEITEMS; + case 'GetItemEstimate': return ZPush::COMMAND_GETITEMESTIMATE; + case 'MeetingResponse': return ZPush::COMMAND_MEETINGRESPONSE; + case 'Search': return ZPush::COMMAND_SEARCH; + case 'Settings': return ZPush::COMMAND_SETTINGS; + case 'Ping': return ZPush::COMMAND_PING; + case 'ItemOperations': return ZPush::COMMAND_ITEMOPERATIONS; + case 'Provision': return ZPush::COMMAND_PROVISION; + case 'ResolveRecipients': return ZPush::COMMAND_RESOLVERECIPIENTS; + case 'ValidateCert': return ZPush::COMMAND_VALIDATECERT; + + // Deprecated commands + case 'GetHierarchy': return ZPush::COMMAND_GETHIERARCHY; + case 'CreateCollection': return ZPush::COMMAND_CREATECOLLECTION; + case 'DeleteCollection': return ZPush::COMMAND_DELETECOLLECTION; + case 'MoveCollection': return ZPush::COMMAND_MOVECOLLECTION; + case 'Notify': return ZPush::COMMAND_NOTIFY; + + // Webservice commands + case 'WebserviceDevice': return ZPush::COMMAND_WEBSERVICE_DEVICE; + case 'WebserviceUsers': return ZPush::COMMAND_WEBSERVICE_USERS; + } + return false; + } + + /** + * Normalize the given timestamp to the start of the day + * + * @param long $timestamp + * + * @access private + * @return long + */ + public static function getDayStartOfTimestamp($timestamp) { + return $timestamp - ($timestamp % (60 * 60 * 24)); + } + + /** + * Returns a formatted string output from an optional timestamp. + * If no timestamp is sent, NOW is used. + * + * @param long $timestamp + * + * @access public + * @return string + */ + public static function GetFormattedTime($timestamp = false) { + if (!$timestamp) + return @strftime("%d/%m/%Y %H:%M:%S"); + else + return @strftime("%d/%m/%Y %H:%M:%S", $timestamp); + } + + + /** + * Get charset name from a codepage + * + * @see http://msdn.microsoft.com/en-us/library/dd317756(VS.85).aspx + * + * Table taken from common/codepage.cpp + * + * @param integer codepage Codepage + * + * @access public + * @return string iconv-compatible charset name + */ + public static function GetCodepageCharset($codepage) { + $codepages = array( + 20106 => "DIN_66003", + 20108 => "NS_4551-1", + 20107 => "SEN_850200_B", + 950 => "big5", + 50221 => "csISO2022JP", + 51932 => "euc-jp", + 51936 => "euc-cn", + 51949 => "euc-kr", + 949 => "euc-kr", + 936 => "gb18030", + 52936 => "csgb2312", + 852 => "ibm852", + 866 => "ibm866", + 50220 => "iso-2022-jp", + 50222 => "iso-2022-jp", + 50225 => "iso-2022-kr", + 1252 => "windows-1252", + 28591 => "iso-8859-1", + 28592 => "iso-8859-2", + 28593 => "iso-8859-3", + 28594 => "iso-8859-4", + 28595 => "iso-8859-5", + 28596 => "iso-8859-6", + 28597 => "iso-8859-7", + 28598 => "iso-8859-8", + 28599 => "iso-8859-9", + 28603 => "iso-8859-13", + 28605 => "iso-8859-15", + 20866 => "koi8-r", + 21866 => "koi8-u", + 932 => "shift-jis", + 1200 => "unicode", + 1201 => "unicodebig", + 65000 => "utf-7", + 65001 => "utf-8", + 1250 => "windows-1250", + 1251 => "windows-1251", + 1253 => "windows-1253", + 1254 => "windows-1254", + 1255 => "windows-1255", + 1256 => "windows-1256", + 1257 => "windows-1257", + 1258 => "windows-1258", + 874 => "windows-874", + 20127 => "us-ascii" + ); + + if(isset($codepages[$codepage])) { + return $codepages[$codepage]; + } else { + // Defaulting to iso-8859-15 since it is more likely for someone to make a mistake in the codepage + // when using west-european charsets then when using other charsets since utf-8 is binary compatible + // with the bottom 7 bits of west-european + return "iso-8859-15"; + } + } + + /** + * Converts a string encoded with codepage into an UTF-8 string + * + * @param int $codepage + * @param string $string + * + * @access public + * @return string + */ + public static function ConvertCodepageStringToUtf8($codepage, $string) { + if (function_exists("iconv")) { + $charset = self::GetCodepageCharset($codepage); + return iconv($charset, "utf-8", $string); + } + else + ZLog::Write(LOGLEVEL_WARN, "Utils::ConvertCodepageStringToUtf8() 'iconv' is not available. Charset conversion skipped."); + + return $string; + } + + /** + * Converts a string to another charset. + * + * @param int $in + * @param int $out + * @param string $string + * + * @access public + * @return string + */ + public static function ConvertCodepage($in, $out, $string) { + // do nothing if both charsets are the same + if ($in == $out) + return $string; + + if (function_exists("iconv")) { + $inCharset = self::GetCodepageCharset($in); + $outCharset = self::GetCodepageCharset($out); + return iconv($inCharset, $outCharset, $string); + } + else + ZLog::Write(LOGLEVEL_WARN, "Utils::ConvertCodepage() 'iconv' is not available. Charset conversion skipped."); + + return $string; + } + + /** + * Returns the best match of preferred body preference types. + * + * @param array $bpTypes + * + * @access public + * @return int + */ + public static function GetBodyPreferenceBestMatch($bpTypes) { + // The best choice is RTF, then HTML and then MIME in order to save bandwidth + // because MIME is a complete message including the headers and attachments + if (in_array(SYNC_BODYPREFERENCE_RTF, $bpTypes)) return SYNC_BODYPREFERENCE_RTF; + if (in_array(SYNC_BODYPREFERENCE_HTML, $bpTypes)) return SYNC_BODYPREFERENCE_HTML; + if (in_array(SYNC_BODYPREFERENCE_MIME, $bpTypes)) return SYNC_BODYPREFERENCE_MIME; + return SYNC_BODYPREFERENCE_PLAIN; + } + + /* BEGIN fmbiete's contribution r1516, ZP-318 */ + /** + * Converts a html string into a plain text string + * + * @param string $html + * + * @access public + * @return string + */ + public static function ConvertHtmlToText($html) { + // remove css-style tags + $plaintext = preg_replace("//is", "", $html); + // remove all other html + $plaintext = strip_tags($plaintext); + + return $plaintext; + } + /* END fmbiete's contribution r1516, ZP-318 */ + + /** + * Checks if a file has the same owner and group as the parent directory. + * If not, owner and group are fixed (being updated to the owner/group of the directory). + * Function code contributed by Robert Scheck aka rsc. + * + * @param string $file + * + * @access public + * @return boolean + */ + public static function FixFileOwner($file) { + if(posix_getuid() == 0 && file_exists($file)) { + $dir = dirname($file); + $perm_dir = stat($dir); + $perm_log = stat($file); + + if($perm_dir[4] !== $perm_log[4] || $perm_dir[5] !== $perm_log[5]) { + chown($file, $perm_dir[4]); + chgrp($file, $perm_dir[5]); + } + } + return true; + } + + /** + * Returns AS-style LastVerbExecuted value from the server value. + * + * @param int $verb + * + * @access public + * @return int + */ + public static function GetLastVerbExecuted($verb) { + switch ($verb) { + case NOTEIVERB_REPLYTOSENDER: return AS_REPLYTOSENDER; + case NOTEIVERB_REPLYTOALL: return AS_REPLYTOALL; + case NOTEIVERB_FORWARD: return AS_FORWARD; + } + + return 0; + } +} + + + +// TODO Win1252/UTF8 functions are deprecated and will be removed sometime +//if the ICS backend is loaded in CombinedBackend and Zarafa > 7 +//STORE_SUPPORTS_UNICODE is true and the convertion will not be done +//for other backends. +function utf8_to_windows1252($string, $option = "", $force_convert = false) { + //if the store supports unicode return the string without converting it + if (!$force_convert && defined('STORE_SUPPORTS_UNICODE') && STORE_SUPPORTS_UNICODE == true) return $string; + + if (function_exists("iconv")){ + return @iconv("UTF-8", "Windows-1252" . $option, $string); + }else{ + return utf8_decode($string); // no euro support here + } +} + +function windows1252_to_utf8($string, $option = "", $force_convert = false) { + //if the store supports unicode return the string without converting it + if (!$force_convert && defined('STORE_SUPPORTS_UNICODE') && STORE_SUPPORTS_UNICODE == true) return $string; + + if (function_exists("iconv")){ + return @iconv("Windows-1252", "UTF-8" . $option, $string); + }else{ + return utf8_encode($string); // no euro support here + } +} + +function w2u($string) { return windows1252_to_utf8($string); } +function u2w($string) { return utf8_to_windows1252($string); } + +function w2ui($string) { return windows1252_to_utf8($string, "//TRANSLIT"); } +function u2wi($string) { return utf8_to_windows1252($string, "//TRANSLIT"); } + + +?> \ No newline at end of file diff --git a/sources/lib/utils/zpushadmin.php b/sources/lib/utils/zpushadmin.php new file mode 100644 index 0000000..17ba954 --- /dev/null +++ b/sources/lib/utils/zpushadmin.php @@ -0,0 +1,622 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class ZPushAdmin { + /** + * //TODO resync of a foldertype for all users (e.g. Appointment) + */ + + /** + * List devices known to Z-Push. + * If no user is given, all devices are listed + * + * @param string $user devices of that user, if false all devices of all users + * + * @return array + * @access public + */ + static public function ListDevices($user = false) { + return ZPush::GetStateMachine()->GetAllDevices($user); + } + + /** + * List users of a device known to Z-Push. + * + * @param string $devid users of that device + * + * @return array + * @access public + */ + static public function ListUsers($devid) { + try { + $devState = ZPush::GetStateMachine()->GetState($devid, IStateMachine::DEVICEDATA); + + if ($devState instanceof StateObject && isset($devState->devices) && is_array($devState->devices)) + return array_keys($devState->devices); + else + return array(); + } + catch (StateNotFoundException $stnf) { + return array(); + } + } + + /** + * Returns details of a device like synctimes, + * policy and wipe status, synched folders etc + * + * @param string $devid device id + * @param string $user user to be looked up + * + * @return ASDevice object + * @access public + */ + static public function GetDeviceDetails($devid, $user) { + + try { + $device = new ASDevice($devid, ASDevice::UNDEFINED, $user, ASDevice::UNDEFINED); + $device->SetData(ZPush::GetStateMachine()->GetState($devid, IStateMachine::DEVICEDATA), false); + $device->StripData(); + + try { + // we need a StateManager for this operation + $stateManager = new StateManager(); + $stateManager->SetDevice($device); + + $sc = new SyncCollections(); + $sc->SetStateManager($stateManager); + + // load all collections of device without loading states or checking permissions + $sc->LoadAllCollections(true, false, false); + + if ($sc->GetLastSyncTime()) + $device->SetLastSyncTime($sc->GetLastSyncTime()); + + // get information about the folder synchronization status from SyncCollections + $folders = $device->GetAllFolderIds(); + + foreach ($folders as $folderid) { + $fstatus = $device->GetFolderSyncStatus($folderid); + + if ($fstatus !== false && isset($fstatus[ASDevice::FOLDERSYNCSTATUS])) { + $spa = $sc->GetCollection($folderid); + $total = $spa->GetFolderSyncTotal(); + $todo = $spa->GetFolderSyncRemaining(); + $fstatus['status'] = ($fstatus[ASDevice::FOLDERSYNCSTATUS] == 1)?'Initialized':'Synchronizing'; + $fstatus['total'] = $total; + $fstatus['done'] = $total - $todo; + $fstatus['todo'] = $todo; + + $device->SetFolderSyncStatus($folderid, $fstatus); + } + } + } + catch (StateInvalidException $sive) { + ZLog::Write(LOGLEVEL_WARN, sprintf("ZPushAdmin::GetDeviceDetails(): device '%s' of user '%s' has invalid states. Please sync to solve this issue.", $devid, $user)); + $device->SetDeviceError("Invalid states. Please force synchronization!"); + } + + return $device; + } + catch (StateNotFoundException $e) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::GetDeviceDetails(): device '%s' of user '%s' can not be found", $devid, $user)); + return false; + } + } + + /** + * Wipes 'a' or all devices of a user. + * If no user is set, the device is generally wiped. + * If no device id is set, all devices of the user will be wiped. + * Device id or user must be set! + * + * @param string $requestedBy user which requested this operation + * @param string $user (opt)user of the device + * @param string $devid (opt) device id which should be wiped + * + * @return boolean + * @access public + */ + static public function WipeDevice($requestedBy, $user, $devid = false) { + if ($user === false && $devid === false) + return false; + + if ($devid === false) { + $devicesIds = ZPush::GetStateMachine()->GetAllDevices($user); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::WipeDevice(): all '%d' devices for user '%s' found to be wiped", count($devicesIds), $user)); + foreach ($devicesIds as $deviceid) { + if (!self::WipeDevice($requestedBy, $user, $deviceid)) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::WipeDevice(): wipe devices failed for device '%s' of user '%s'. Aborting.", $deviceid, $user)); + return false; + } + } + } + + // wipe a device completely (for connected users to this device) + else if ($devid !== false && $user === false) { + $users = self::ListUsers($devid); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::WipeDevice(): device '%d' is used by '%d' users and will be wiped", $devid, count($users))); + if (count($users) == 0) + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::WipeDevice(): no user found on device '%s'. Aborting.", $devid)); + + return self::WipeDevice($requestedBy, $users[0], $devid); + } + + else { + // load device data + $device = new ASDevice($devid, ASDevice::UNDEFINED, $user, ASDevice::UNDEFINED); + try { + $device->SetData(ZPush::GetStateMachine()->GetState($devid, IStateMachine::DEVICEDATA), false); + } + catch (StateNotFoundException $e) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::WipeDevice(): device '%s' of user '%s' can not be found", $devid, $user)); + return false; + } + + // set wipe status + if ($device->GetWipeStatus() == SYNC_PROVISION_RWSTATUS_WIPED) + ZLog::Write(LOGLEVEL_INFO, sprintf("ZPushAdmin::WipeDevice(): device '%s' of user '%s' was alread sucessfully remote wiped on %s", $devid , $user, strftime("%Y-%m-%d %H:%M", $device->GetWipeActionOn()))); + else + $device->SetWipeStatus(SYNC_PROVISION_RWSTATUS_PENDING, $requestedBy); + + // save device data + try { + if ($device->IsNewDevice()) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::WipeDevice(): data of user '%s' not synchronized on device '%s'. Aborting.", $user, $devid)); + return false; + } + + ZPush::GetStateMachine()->SetState($device->GetData(), $devid, IStateMachine::DEVICEDATA); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::WipeDevice(): device '%s' of user '%s' marked to be wiped", $devid, $user)); + } + catch (StateNotFoundException $e) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::WipeDevice(): state for device '%s' of user '%s' can not be saved", $devid, $user)); + return false; + } + } + return true; + } + + + /** + * Removes device details from the z-push directory. + * If device id is not set, all devices of a user are removed. + * If the user is not set, the details of the device (independently if used by several users) is removed. + * Device id or user must be set! + * + * @param string $user (opt) user of the device + * @param string $devid (opt) device id which should be wiped + * + * @return boolean + * @access public + */ + static public function RemoveDevice($user = false, $devid = false) { + if ($user === false && $devid === false) + return false; + + // remove all devices for user + if ($devid === false && $user !== false) { + $devicesIds = ZPush::GetStateMachine()->GetAllDevices($user); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::RemoveDevice(): all '%d' devices for user '%s' found to be removed", count($devicesIds), $user)); + foreach ($devicesIds as $deviceid) { + if (!self::RemoveDevice($user, $deviceid)) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::RemoveDevice(): removing devices failed for device '%s' of user '%s'. Aborting", $deviceid, $user)); + return false; + } + } + } + // remove a device completely (for connected users to this device) + else if ($devid !== false && $user === false) { + $users = self::ListUsers($devid); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::RemoveDevice(): device '%d' is used by '%d' users and will be removed", $devid, count($users))); + foreach ($users as $aUser) { + if (!self::RemoveDevice($aUser, $devid)) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::RemoveDevice(): removing user '%s' from device '%s' failed. Aborting", $aUser, $devid)); + return false; + } + } + } + + // user and deviceid set + else { + // load device data + $device = new ASDevice($devid, ASDevice::UNDEFINED, $user, ASDevice::UNDEFINED); + $devices = array(); + try { + $devicedata = ZPush::GetStateMachine()->GetState($devid, IStateMachine::DEVICEDATA); + $device->SetData($devicedata, false); + if (!isset($devicedata->devices)) + throw new StateInvalidException("No devicedata stored in ASDevice"); + $devices = $devicedata->devices; + } + catch (StateNotFoundException $e) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::RemoveDevice(): device '%s' of user '%s' can not be found", $devid, $user)); + return false; + } + + // remove all related states + foreach ($device->GetAllFolderIds() as $folderid) + StateManager::UnLinkState($device, $folderid); + + // remove hierarchcache + StateManager::UnLinkState($device, false); + + // remove backend storage permanent data + ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, false, 99999999999); + + // remove devicedata and unlink user from device + unset($devices[$user]); + if (isset($devicedata->devices)) + $devicedata->devices = $devices; + ZPush::GetStateMachine()->UnLinkUserDevice($user, $devid); + + // no more users linked for device - remove device data + if (count($devices) == 0) + ZPush::GetStateMachine()->CleanStates($devid, IStateMachine::DEVICEDATA, false); + + // save data if something left + else + ZPush::GetStateMachine()->SetState($devicedata, $devid, IStateMachine::DEVICEDATA); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::RemoveDevice(): data of device '%s' of user '%s' removed", $devid, $user)); + } + return true; + } + + + /** + * Marks a folder of a device of a user for re-synchronization + * + * @param string $user user of the device + * @param string $devid device id which should be wiped + * @param mixed $folderid a single folder id or an array of folder ids + * + * @return boolean + * @access public + */ + static public function ResyncFolder($user, $devid, $folderid) { + // load device data + $device = new ASDevice($devid, ASDevice::UNDEFINED, $user, ASDevice::UNDEFINED); + try { + $device->SetData(ZPush::GetStateMachine()->GetState($devid, IStateMachine::DEVICEDATA), false); + + if ($device->IsNewDevice()) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::ResyncFolder(): data of user '%s' not synchronized on device '%s'. Aborting.",$user, $devid)); + return false; + } + + if (!$folderid || (is_array($folderid) && empty($folderid))) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::ResyncFolder(): no folders synchronized for user '%s' on device '%s'. Aborting.",$user, $devid)); + return false; + } + + // remove folder state + if (is_array($folderid)) { + foreach ($folderid as $fid) { + StateManager::UnLinkState($device, $fid); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::ResyncFolder(): folder '%s' on device '%s' of user '%s' marked to be re-synchronized.", $fid, $devid, $user)); + } + } + else { + StateManager::UnLinkState($device, $folderid); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::ResyncFolder(): folder '%s' on device '%s' of user '%s' marked to be re-synchronized.", $folderid, $devid, $user)); + } + + ZPush::GetStateMachine()->SetState($device->GetData(), $devid, IStateMachine::DEVICEDATA); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::ResyncFolder(): saved updated device data of device '%s' of user '%s'", $devid, $user)); + } + catch (StateNotFoundException $e) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::ResyncFolder(): state for device '%s' of user '%s' can not be found or saved", $devid, $user)); + return false; + } + return true; + } + + + /** + * Marks a all folders synchronized to a device for re-synchronization + * If no user is set all user which are synchronized for a device are marked for re-synchronization. + * If no device id is set all devices of that user are marked for re-synchronization. + * If no user and no device are set then ALL DEVICES are marked for resynchronization (use with care!). + * + * @param string $user (opt) user of the device + * @param string $devid (opt)device id which should be wiped + * + * @return boolean + * @access public + */ + static public function ResyncDevice($user, $devid = false) { + + // search for target devices + if ($devid === false) { + $devicesIds = ZPush::GetStateMachine()->GetAllDevices($user); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::ResyncDevice(): all '%d' devices for user '%s' found to be re-synchronized", count($devicesIds), $user)); + foreach ($devicesIds as $deviceid) { + if (!self::ResyncDevice($user, $deviceid)) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::ResyncDevice(): wipe devices failed for device '%s' of user '%s'. Aborting", $deviceid, $user)); + return false; + } + } + } + else { + // get devicedata + try { + $devicedata = ZPush::GetStateMachine()->GetState($devid, IStateMachine::DEVICEDATA); + } + catch (StateNotFoundException $e) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::ResyncDevice(): state for device '%s' can not be found", $devid)); + return false; + + } + + // loop through all users which currently use this device + if ($user === false && $devicedata instanceof StateObject && isset($devicedata->devices) && + is_array($devicedata->devices) && count($devicedata->devices) > 1) { + foreach (array_keys($devicedata) as $aUser) { + if (!self::ResyncDevice($aUser, $devid)) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::ResyncDevice(): re-synchronization failed for device '%s' of user '%s'. Aborting", $devid, $aUser)); + return false; + } + } + } + + // load device data + $device = new ASDevice($devid, ASDevice::UNDEFINED, $user, ASDevice::UNDEFINED); + try { + $device->SetData($devicedata, false); + + if ($device->IsNewDevice()) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::ResyncDevice(): data of user '%s' not synchronized on device '%s'. Aborting.",$user, $devid)); + return false; + } + + // delete all uuids + foreach ($device->GetAllFolderIds() as $folderid) + StateManager::UnLinkState($device, $folderid); + + // remove hierarchcache + StateManager::UnLinkState($device, false); + + ZPush::GetStateMachine()->SetState($device->GetData(), $devid, IStateMachine::DEVICEDATA); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::ResyncDevice(): all folders synchronized to device '%s' of user '%s' marked to be re-synchronized.", $devid, $user)); + } + catch (StateNotFoundException $e) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::ResyncDevice(): state for device '%s' of user '%s' can not be found or saved", $devid, $user)); + return false; + } + } + return true; + } + + /** + * Clears loop detection data + * + * @param string $user (opt) user which data should be removed - user may not be specified without device id + * @param string $devid (opt) device id which data to be removed + * + * @return boolean + * @access public + */ + static public function ClearLoopDetectionData($user = false, $devid = false) { + $loopdetection = new LoopDetection(); + return $loopdetection->ClearData($user, $devid); + } + + /** + * Returns loop detection data of a user & device + * + * @param string $user + * @param string $devid + * + * @return array/boolean returns false if data is not available + * @access public + */ + static public function GetLoopDetectionData($user, $devid) { + $loopdetection = new LoopDetection(); + return $loopdetection->GetCachedData($user, $devid); + } + + /** + * Fixes states with usernames in different cases + * + * @return boolean + * @access public + */ + static public function FixStatesDifferentUsernameCases() { + $processed = 0; + $dropedUsers = 0; + $fixedUsers = 0; + + $devices = ZPush::GetStateMachine()->GetAllDevices(false); + foreach ($devices as $devid) { + $users = self::ListUsers($devid); + $obsoleteUsers = array(); + + // find obsolete uppercase users + foreach ($users as $username) { + $processed++; + $lowUsername = strtolower($username); + if ($lowUsername === $username) + continue; // default case + + $obsoleteUsers[] = $username; + } + + // remove or transform obsolete users + if (!empty($obsoleteUsers)) { + // load the device data + try { + $devData = ZPush::GetStateMachine()->GetState($devid, IStateMachine::DEVICEDATA); + + $devices = $devData->devices; + $knownUsers = array_keys($devData->devices); + ZLog::Write(LOGLEVEL_DEBUG, print_r($devData,1),false); + + foreach ($obsoleteUsers as $ouser) { + $lowerOUser = strtolower($ouser); + // there is a lowercase user, drop the uppercase one + if (in_array($lowerOUser, $knownUsers)) { + unset($devices[$ouser]); + $dropedUsers++; + ZLog::Write(LOGLEVEL_DEBUG, print_r(array_keys($devices),1)); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::FixStatesDifferentUsernameCases(): user '%s' of device '%s' is obsolete as a lowercase username is known", $ouser, $devid)); + } + // there is only an uppercase user, save it as lowercase + else { + $devices[$lowerOUser] = $devices[$ouser]; + unset($devices[$ouser]); + $fixedUsers++; + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::FixStatesDifferentUsernameCases(): user '%s' of device '%s' was saved as '%s'", $ouser, $devid, $lowerOUser)); + } + } + + unset($devData->device); + // save the devicedata + $devData->devices = $devices; + ZPush::GetStateMachine()->SetState($devData, $devid, IStateMachine::DEVICEDATA); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::FixStatesDifferentUsernameCases(): updated device '%s' and user(s) %s were dropped or converted", $devid, implode(", ", $obsoleteUsers))); + } + catch (StateNotFoundException $e) { + ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::FixStatesDifferentUsernameCases(): state for device '%s' can not be found", $devid)); + } + } + } + + return array($processed, $fixedUsers, $dropedUsers); + } + + /** + * Fixes states of available device data to the user linking + * + * @return int + * @access public + */ + static public function FixStatesDeviceToUserLinking() { + $seen = 0; + $fixed = 0; + $devices = ZPush::GetStateMachine()->GetAllDevices(false); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::FixStatesDeviceToUserLinking(): found %d devices", count($devices))); + + foreach ($devices as $devid) { + $users = self::ListUsers($devid); + foreach ($users as $username) { + $seen++; + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::FixStatesDeviceToUserLinking(): linking user '%s' to device '%d'", $username, $devid)); + + if (ZPush::GetStateMachine()->LinkUserDevice($username, $devid)) + $fixed++; + } + } + return array($seen, $fixed); + } + + /** + * Fixes states of the user linking to the states + * and removes all obsolete states + * + * @return boolean + * @access public + */ + static public function FixStatesUserToStatesLinking() { + $processed = 0; + $deleted = 0; + $devices = ZPush::GetStateMachine()->GetAllDevices(false); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::FixStatesUserToStatesLinking(): found %d devices", count($devices))); + + foreach ($devices as $devid) { + try { + // we work on device level + $devicedata = ZPush::GetStateMachine()->GetState($devid, IStateMachine::DEVICEDATA); + $knownUuids = array(); + + // get all known UUIDs for this device + foreach (self::ListUsers($devid) as $username) { + $device = new ASDevice($devid, ASDevice::UNDEFINED, $username, ASDevice::UNDEFINED); + $device->SetData($devicedata, false); + + // get all known uuids of this device + $folders = $device->GetAllFolderIds(); + + // add a "false" folder id so the hierarchy UUID is retrieved + $folders[] = false; + + foreach ($folders as $folderid) { + $uuid = $device->GetFolderUUID($folderid); + if ($uuid) + $knownUuids[] = $uuid; + } + + } + } + catch (StateNotFoundException $e) {} + + // get all uuids for deviceid from statemachine + $existingStates = ZPush::GetStateMachine()->GetAllStatesForDevice($devid); + $processed = count($existingStates); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZPushAdmin::FixStatesUserToStatesLinking(): found %d valid uuids and %d states for device device '%s'", count($knownUuids), $processed, $devid)); + + // remove states for all unknown uuids + foreach ($existingStates as $obsoleteState) { + if ($obsoleteState['type'] === IStateMachine::DEVICEDATA) + continue; + + if (!in_array($obsoleteState['uuid'], $knownUuids)) { + if (is_numeric($obsoleteState['counter'])) + $obsoleteState['counter']++; + + ZPush::GetStateMachine()->CleanStates($devid, $obsoleteState['type'], $obsoleteState['uuid'], $obsoleteState['counter']); + $deleted++; + } + } + } + return array($processed, $deleted); + } + +} + +?> \ No newline at end of file diff --git a/sources/lib/wbxml/wbxmldecoder.php b/sources/lib/wbxml/wbxmldecoder.php new file mode 100644 index 0000000..ea8a703 --- /dev/null +++ b/sources/lib/wbxml/wbxmldecoder.php @@ -0,0 +1,668 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class WBXMLDecoder extends WBXMLDefs { + private $in; + + private $version; + private $publicid; + private $publicstringid; + private $charsetid; + private $stringtable; + + private $tagcp = 0; + private $attrcp = 0; + + private $ungetbuffer; + + private $logStack = array(); + + private $inputBuffer = ""; + private $isWBXML = true; + + const VERSION = 0x03; + + /** + * WBXML Decode Constructor + * + * @param stream $input the incoming data stream + * + * @access public + */ + public function WBXMLDecoder($input) { + // make sure WBXML_DEBUG is defined. It should be at this point + if (!defined('WBXML_DEBUG')) define('WBXML_DEBUG', false); + + $this->in = $input; + + $this->readVersion(); + if (isset($this->version) && $this->version != self::VERSION) { + $this->isWBXML = false; + return; + } + + $this->publicid = $this->getMBUInt(); + if($this->publicid == 0) { + $this->publicstringid = $this->getMBUInt(); + } + + $this->charsetid = $this->getMBUInt(); + $this->stringtable = $this->getStringTable(); + } + + /** + * Returns either start, content or end, and auto-concatenates successive content + * + * @access public + * @return element/value + */ + public function getElement() { + $element = $this->getToken(); + + switch($element[EN_TYPE]) { + case EN_TYPE_STARTTAG: + return $element; + case EN_TYPE_ENDTAG: + return $element; + case EN_TYPE_CONTENT: + while(1) { + $next = $this->getToken(); + if($next == false) + return false; + else if($next[EN_TYPE] == EN_CONTENT) { + $element[EN_CONTENT] .= $next[EN_CONTENT]; + } else { + $this->ungetElement($next); + break; + } + } + return $element; + } + + return false; + } + + /** + * Get a peek at the next element + * + * @access public + * @return element + */ + public function peek() { + $element = $this->getElement(); + $this->ungetElement($element); + return $element; + } + + /** + * Get the element of a StartTag + * + * @param $tag + * + * @access public + * @return element/boolean returns false if not available + */ + public function getElementStartTag($tag) { + $element = $this->getToken(); + + if($element[EN_TYPE] == EN_TYPE_STARTTAG && $element[EN_TAG] == $tag) + return $element; + else { + ZLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("WBXMLDecoder->getElementStartTag(): unmatched WBXML tag: '%s' matching '%s' type '%s' flags '%s'", $tag, ((isset($element[EN_TAG]))?$element[EN_TAG]:""), ((isset($element[EN_TYPE]))?$element[EN_TYPE]:""), ((isset($element[EN_FLAGS]))?$element[EN_FLAGS]:""))); + $this->ungetElement($element); + } + + return false; + } + + /** + * Get the element of a EndTag + * + * @access public + * @return element/boolean returns false if not available + */ + public function getElementEndTag() { + $element = $this->getToken(); + + if($element[EN_TYPE] == EN_TYPE_ENDTAG) + return $element; + else { + ZLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("WBXMLDecoder->getElementEndTag(): unmatched WBXML tag: '%s' type '%s' flags '%s'", ((isset($element[EN_TAG]))?$element[EN_TAG]:""), ((isset($element[EN_TYPE]))?$element[EN_TYPE]:""), ((isset($element[EN_FLAGS]))?$element[EN_FLAGS]:""))); + + $bt = debug_backtrace(); + ZLog::Write(LOGLEVEL_ERROR, sprintf("WBXMLDecoder->getElementEndTag(): could not read end tag in '%s'. Please enable the LOGLEVEL_WBXML and send the log to the Z-Push dev team.", $bt[0]["file"] . ":" . $bt[0]["line"])); + + // log the remaining wbxml content + $this->ungetElement($element); + while($el = $this->getElement()); + } + + return false; + } + + /** + * Get the content of an element + * + * @access public + * @return string/boolean returns false if not available + */ + public function getElementContent() { + $element = $this->getToken(); + + if($element[EN_TYPE] == EN_TYPE_CONTENT) { + return $element[EN_CONTENT]; + } + else { + ZLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("WBXMLDecoder->getElementContent(): unmatched WBXML content: '%s' type '%s' flags '%s'", ((isset($element[EN_TAG]))?$element[EN_TAG]:""), ((isset($element[EN_TYPE]))?$element[EN_TYPE]:""), ((isset($element[EN_FLAGS]))?$element[EN_FLAGS]:""))); + $this->ungetElement($element); + } + + return false; + } + + /** + * 'Ungets' an element writing it into a buffer to be 'get' again + * + * @param element $element the element to get ungetten + * + * @access public + * @return + */ + public function ungetElement($element) { + if($this->ungetbuffer) + ZLog::Write(LOGLEVEL_ERROR,sprintf("WBXMLDecoder->ungetElement(): WBXML double unget on tag: '%s' type '%s' flags '%s'", ((isset($element[EN_TAG]))?$element[EN_TAG]:""), ((isset($element[EN_TYPE]))?$element[EN_TYPE]:""), ((isset($element[EN_FLAGS]))?$element[EN_FLAGS]:""))); + + $this->ungetbuffer = $element; + } + + /** + * Returns the plain input stream + * + * @access public + * @return string + */ + public function GetPlainInputStream() { + $plain = $this->inputBuffer; + while($data = fread($this->in, 4096)) + $plain .= $data; + + return $plain; + } + + /** + * Returns if the input is WBXML + * + * @access public + * @return boolean + */ + public function IsWBXML() { + return $this->isWBXML; + } + + + + /**---------------------------------------------------------------------------------------------------------- + * Private WBXMLDecoder stuff + */ + + /** + * Returns the next token + * + * @access private + * @return token + */ + private function getToken() { + // See if there's something in the ungetBuffer + if($this->ungetbuffer) { + $element = $this->ungetbuffer; + $this->ungetbuffer = false; + return $element; + } + + $el = $this->_getToken(); + $this->logToken($el); + + return $el; + } + + /** + * Log the a token to ZLog + * + * @param string $el token + * + * @access private + * @return + */ + private function logToken($el) { + if(!WBXML_DEBUG) + return; + + $spaces = str_repeat(" ", count($this->logStack)); + + switch($el[EN_TYPE]) { + case EN_TYPE_STARTTAG: + if($el[EN_FLAGS] & EN_FLAGS_CONTENT) { + ZLog::Write(LOGLEVEL_WBXML,"I " . $spaces . " <". $el[EN_TAG] . ">"); + array_push($this->logStack, $el[EN_TAG]); + } else + ZLog::Write(LOGLEVEL_WBXML,"I " . $spaces . " <" . $el[EN_TAG] . "/>"); + + break; + case EN_TYPE_ENDTAG: + $tag = array_pop($this->logStack); + ZLog::Write(LOGLEVEL_WBXML,"I " . $spaces . ""); + break; + case EN_TYPE_CONTENT: + ZLog::Write(LOGLEVEL_WBXML,"I " . $spaces . " " . $el[EN_CONTENT]); + break; + } + } + + /** + * Returns either a start tag, content or end tag + * + * @access private + * @return + */ + private function _getToken() { + // Get the data from the input stream + $element = array(); + + while(1) { + $byte = $this->getByte(); + + if(!isset($byte)) + break; + + switch($byte) { + case WBXML_SWITCH_PAGE: + $this->tagcp = $this->getByte(); + continue; + + case WBXML_END: + $element[EN_TYPE] = EN_TYPE_ENDTAG; + return $element; + + case WBXML_ENTITY: + $entity = $this->getMBUInt(); + $element[EN_TYPE] = EN_TYPE_CONTENT; + $element[EN_CONTENT] = $this->entityToCharset($entity); + return $element; + + case WBXML_STR_I: + $element[EN_TYPE] = EN_TYPE_CONTENT; + $element[EN_CONTENT] = $this->getTermStr(); + return $element; + + case WBXML_LITERAL: + $element[EN_TYPE] = EN_TYPE_STARTTAG; + $element[EN_TAG] = $this->getStringTableEntry($this->getMBUInt()); + $element[EN_FLAGS] = 0; + return $element; + + case WBXML_EXT_I_0: + case WBXML_EXT_I_1: + case WBXML_EXT_I_2: + $this->getTermStr(); + // Ignore extensions + continue; + + case WBXML_PI: + // Ignore PI + $this->getAttributes(); + continue; + + case WBXML_LITERAL_C: + $element[EN_TYPE] = EN_TYPE_STARTTAG; + $element[EN_TAG] = $this->getStringTableEntry($this->getMBUInt()); + $element[EN_FLAGS] = EN_FLAGS_CONTENT; + return $element; + + case WBXML_EXT_T_0: + case WBXML_EXT_T_1: + case WBXML_EXT_T_2: + $this->getMBUInt(); + // Ingore extensions; + continue; + + case WBXML_STR_T: + $element[EN_TYPE] = EN_TYPE_CONTENT; + $element[EN_CONTENT] = $this->getStringTableEntry($this->getMBUInt()); + return $element; + + case WBXML_LITERAL_A: + $element[EN_TYPE] = EN_TYPE_STARTTAG; + $element[EN_TAG] = $this->getStringTableEntry($this->getMBUInt()); + $element[EN_ATTRIBUTES] = $this->getAttributes(); + $element[EN_FLAGS] = EN_FLAGS_ATTRIBUTES; + return $element; + case WBXML_EXT_0: + case WBXML_EXT_1: + case WBXML_EXT_2: + continue; + + case WBXML_OPAQUE: + $length = $this->getMBUInt(); + $element[EN_TYPE] = EN_TYPE_CONTENT; + $element[EN_CONTENT] = $this->getOpaque($length); + return $element; + + case WBXML_LITERAL_AC: + $element[EN_TYPE] = EN_TYPE_STARTTAG; + $element[EN_TAG] = $this->getStringTableEntry($this->getMBUInt()); + $element[EN_ATTRIBUTES] = $this->getAttributes(); + $element[EN_FLAGS] = EN_FLAGS_ATTRIBUTES | EN_FLAGS_CONTENT; + return $element; + + default: + $element[EN_TYPE] = EN_TYPE_STARTTAG; + $element[EN_TAG] = $this->getMapping($this->tagcp, $byte & 0x3f); + $element[EN_FLAGS] = ($byte & 0x80 ? EN_FLAGS_ATTRIBUTES : 0) | ($byte & 0x40 ? EN_FLAGS_CONTENT : 0); + if($byte & 0x80) + $element[EN_ATTRIBUTES] = $this->getAttributes(); + return $element; + } + } + } + + /** + * Gets attributes + * + * @access private + * @return + */ + private function getAttributes() { + $attributes = array(); + $attr = ""; + + while(1) { + $byte = $this->getByte(); + + if(count($byte) == 0) + break; + + switch($byte) { + case WBXML_SWITCH_PAGE: + $this->attrcp = $this->getByte(); + break; + + case WBXML_END: + if($attr != "") + $attributes += $this->splitAttribute($attr); + + return $attributes; + + case WBXML_ENTITY: + $entity = $this->getMBUInt(); + $attr .= $this->entityToCharset($entity); + return $attr; /* fmbiete's contribution r1534, ZP-324 */ + + case WBXML_STR_I: + $attr .= $this->getTermStr(); + return $attr; /* fmbiete's contribution r1534, ZP-324 */ + + case WBXML_LITERAL: + if($attr != "") + $attributes += $this->splitAttribute($attr); + + $attr = $this->getStringTableEntry($this->getMBUInt()); + return $attr; /* fmbiete's contribution r1534, ZP-324 */ + + case WBXML_EXT_I_0: + case WBXML_EXT_I_1: + case WBXML_EXT_I_2: + $this->getTermStr(); + continue; + + case WBXML_PI: + case WBXML_LITERAL_C: + // Invalid + return false; + + case WBXML_EXT_T_0: + case WBXML_EXT_T_1: + case WBXML_EXT_T_2: + $this->getMBUInt(); + continue; + + case WBXML_STR_T: + $attr .= $this->getStringTableEntry($this->getMBUInt()); + return $attr; /* fmbiete's contribution r1534, ZP-324 */ + + case WBXML_LITERAL_A: + return false; + + case WBXML_EXT_0: + case WBXML_EXT_1: + case WBXML_EXT_2: + continue; + + case WBXML_OPAQUE: + $length = $this->getMBUInt(); + $attr .= $this->getOpaque($length); + return $attr; /* fmbiete's contribution r1534, ZP-324 */ + + case WBXML_LITERAL_AC: + return false; + + default: + if($byte < 128) { + if($attr != "") { + $attributes += $this->splitAttribute($attr); + $attr = ""; + } + } + $attr .= $this->getMapping($this->attrcp, $byte); + break; + } + } + } + + /** + * Splits an attribute + * + * @param string $attr attribute to be splitted + * + * @access private + * @return array + */ + private function splitAttribute($attr) { + $attributes = array(); + + $pos = strpos($attr,chr(61)); // equals sign + + if($pos) + $attributes[substr($attr, 0, $pos)] = substr($attr, $pos+1); + else + $attributes[$attr] = null; + + return $attributes; + } + + /** + * Reads from the stream until getting a string terminator + * + * @access private + * @return string + */ + private function getTermStr() { + $str = ""; + while(1) { + $in = $this->getByte(); + + if($in == 0) + break; + else + $str .= chr($in); + } + + return $str; + } + + /** + * Reads $len from the input stream + * + * @param int $len + * + * @access private + * @return string + */ + private function getOpaque($len) { + // TODO check if it's possible to do it other way + // fread stops reading because the following condition is true (from php.net): + // if the stream is read buffered and it does not represent a plain file, + // at most one read of up to a number of bytes equal to the chunk size + // (usually 8192) is made; depending on the previously buffered data, + // the size of the returned data may be larger than the chunk size. + + // using only return fread it will return only a part of stream if chunk is smaller + // than $len. Read from stream in a loop until the $len is reached. + $d = ""; + $l = 0; + while (1) { + $l = (($len - strlen($d)) > 8192) ? 8192 : ($len - strlen($d)); + if ($l > 0) { + $data = fread($this->in, $l); + + // Stream ends prematurely on instable connections and big mails + if ($data === false || feof($this->in)) + throw new HTTPReturnCodeException(sprintf("WBXMLDecoder->getOpaque() connection unavailable while trying to read %d bytes from stream. Aborting after %d bytes read.", $len, strlen($d)), HTTP_CODE_500, null, LOGLEVEL_WARN); + else + $d .= $data; + } + if (strlen($d) >= $len) break; + } + return $d; + } + + /** + * Reads one byte from the input stream + * + * @access private + * @return int + */ + private function getByte() { + $ch = fread($this->in, 1); + if(strlen($ch) > 0) + return ord($ch); + else + return; + } + + /** + * Reads string length from the input stream + * + * @access private + * @return + */ + private function getMBUInt() { + $uint = 0; + + while(1) { + $byte = $this->getByte(); + + $uint |= $byte & 0x7f; + + if($byte & 0x80) + $uint = $uint << 7; + else + break; + } + + return $uint; + } + + /** + * Reads string table from the input stream + * + * @access private + * @return int + */ + private function getStringTable() { + $stringtable = ""; + + $length = $this->getMBUInt(); + if($length > 0) + $stringtable = fread($this->in, $length); + + return $stringtable; + } + + /** + * Returns the mapping for a specified codepage and id + * + * @param $cp codepage + * @param $id + * + * @access public + * @return string + */ + private function getMapping($cp, $id) { + if(!isset($this->dtd["codes"][$cp]) || !isset($this->dtd["codes"][$cp][$id])) + return false; + else { + if(isset($this->dtd["namespaces"][$cp])) { + return $this->dtd["namespaces"][$cp] . ":" . $this->dtd["codes"][$cp][$id]; + } else + return $this->dtd["codes"][$cp][$id]; + } + } + + /** + * Reads one byte from the input stream + * + * @access private + * @return void + */ + private function readVersion() { + $ch = $this->getByte(); + + if($ch != NULL) { + $this->inputBuffer .= chr($ch); + $this->version = $ch; + } + } +} + +?> \ No newline at end of file diff --git a/sources/lib/wbxml/wbxmldefs.php b/sources/lib/wbxml/wbxmldefs.php new file mode 100644 index 0000000..dd616b1 --- /dev/null +++ b/sources/lib/wbxml/wbxmldefs.php @@ -0,0 +1,769 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +define('WBXML_SWITCH_PAGE', 0x00); +define('WBXML_END', 0x01); +define('WBXML_ENTITY', 0x02); +define('WBXML_STR_I', 0x03); +define('WBXML_LITERAL', 0x04); +define('WBXML_EXT_I_0', 0x40); +define('WBXML_EXT_I_1', 0x41); +define('WBXML_EXT_I_2', 0x42); +define('WBXML_PI', 0x43); +define('WBXML_LITERAL_C', 0x44); +define('WBXML_EXT_T_0', 0x80); +define('WBXML_EXT_T_1', 0x81); +define('WBXML_EXT_T_2', 0x82); +define('WBXML_STR_T', 0x83); +define('WBXML_LITERAL_A', 0x84); +define('WBXML_EXT_0', 0xC0); +define('WBXML_EXT_1', 0xC1); +define('WBXML_EXT_2', 0xC2); +define('WBXML_OPAQUE', 0xC3); +define('WBXML_LITERAL_AC', 0xC4); + +define('EN_TYPE', 1); +define('EN_TAG', 2); +define('EN_CONTENT', 3); +define('EN_FLAGS', 4); +define('EN_ATTRIBUTES', 5); + +define('EN_TYPE_STARTTAG', 1); +define('EN_TYPE_ENDTAG', 2); +define('EN_TYPE_CONTENT', 3); + +define('EN_FLAGS_CONTENT', 1); +define('EN_FLAGS_ATTRIBUTES', 2); + +class WBXMLDefs { + /** + * The WBXML DTDs + */ + protected $dtd = array( + "codes" => array ( + 0 => array ( + 0x05 => "Synchronize", + 0x06 => "Replies", //Responses + 0x07 => "Add", + 0x08 => "Modify", //Change + 0x09 => "Remove", //Delete + 0x0a => "Fetch", + 0x0b => "SyncKey", + 0x0c => "ClientEntryId", //ClientId + 0x0d => "ServerEntryId", //ServerId + 0x0e => "Status", + 0x0f => "Folder", //collection + 0x10 => "FolderType", //class + 0x11 => "Version", + 0x12 => "FolderId", //CollectionId + 0x13 => "GetChanges", + 0x14 => "MoreAvailable", + 0x15 => "WindowSize", //WindowSize - MaxItems before z-push 2 + 0x16 => "Perform", //Commands + 0x17 => "Options", + 0x18 => "FilterType", + 0x19 => "Truncation", //2.0 and 2.5 + 0x1a => "RtfTruncation", //2.0 and 2.5 + 0x1b => "Conflict", + 0x1c => "Folders", //Collections + 0x1d => "Data", + 0x1e => "DeletesAsMoves", + 0x1f => "NotifyGUID", //2.0 and 2.5 + 0x20 => "Supported", + 0x21 => "SoftDelete", + 0x22 => "MIMESupport", + 0x23 => "MIMETruncation", + 0x24 => "Wait", //12.1 and 14.0 + 0x25 => "Limit", //12.1 and 14.0 + 0x26 => "Partial", //12.1 and 14.0 + 0x27 => "ConversationMode", //14.0 + 0x28 => "MaxItems", //14.0 + 0x29 => "HeartbeatInterval", //14.0 Either this tag or the Wait tag can be present, but not both. + ), + 1 => array ( + 0x05 => "Anniversary", + 0x06 => "AssistantName", + 0x07 => "AssistnamePhoneNumber", //AssistantTelephoneNumber + 0x08 => "Birthday", + 0x09 => "Body", // 2.5, but is in code page 17 in ActiveSync versions 12.0, 12.1, and 14.0. + 0x0a => "BodySize", //2.0 and 2.5 + 0x0b => "BodyTruncated", //2.0 and 2.5 + 0x0c => "Business2PhoneNumber", + 0x0d => "BusinessCity", + 0x0e => "BusinessCountry", + 0x0f => "BusinessPostalCode", + 0x10 => "BusinessState", + 0x11 => "BusinessStreet", + 0x12 => "BusinessFaxNumber", + 0x13 => "BusinessPhoneNumber", + 0x14 => "CarPhoneNumber", + 0x15 => "Categories", + 0x16 => "Category", + 0x17 => "Children", + 0x18 => "Child", + 0x19 => "CompanyName", + 0x1a => "Department", + 0x1b => "Email1Address", + 0x1c => "Email2Address", + 0x1d => "Email3Address", + 0x1e => "FileAs", + 0x1f => "FirstName", + 0x20 => "Home2PhoneNumber", + 0x21 => "HomeCity", + 0x22 => "HomeCountry", + 0x23 => "HomePostalCode", + 0x24 => "HomeState", + 0x25 => "HomeStreet", + 0x26 => "HomeFaxNumber", + 0x27 => "HomePhoneNumber", + 0x28 => "JobTitle", + 0x29 => "LastName", + 0x2a => "MiddleName", + 0x2b => "MobilePhoneNumber", + 0x2c => "OfficeLocation", + 0x2d => "OtherCity", + 0x2e => "OtherCountry", + 0x2f => "OtherPostalCode", + 0x30 => "OtherState", + 0x31 => "OtherStreet", + 0x32 => "PagerNumber", + 0x33 => "RadioPhoneNumber", + 0x34 => "Spouse", + 0x35 => "Suffix", + 0x36 => "Title", + 0x37 => "WebPage", + 0x38 => "YomiCompanyName", + 0x39 => "YomiFirstName", + 0x3a => "YomiLastName", + 0x3b => "Rtf", //CompressedRTF - 2.5 + 0x3c => "Picture", + 0x3d => "Alias", //14.0 + 0x3e => "WeightedRank" //14.0 + ), + 2 => array ( + 0x05 => "Attachment", //2.5, 12.0, 12.1 and 14.0 + 0x06 => "Attachments", //2.5, 12.0, 12.1 and 14.0 + 0x07 => "AttName", //2.5, 12.0, 12.1 and 14.0 + 0x08 => "AttSize", //2.5, 12.0, 12.1 and 14.0 + 0x09 => "AttOid", //2.5, 12.0, 12.1 and 14.0 + 0x0a => "AttMethod", //2.5, 12.0, 12.1 and 14.0 + 0x0b => "AttRemoved", //2.5, 12.0, 12.1 and 14.0 + 0x0c => "Body", // 2.5, but is in code page 17 in ActiveSync versions 12.0, 12.1, and 14.0. + 0x0d => "BodySize", //2.5, 12.0, 12.1 and 14.0 + 0x0e => "BodyTruncated", //2.5, 12.0, 12.1 and 14.0 + 0x0f => "DateReceived", //2.5, 12.0, 12.1 and 14.0 + 0x10 => "DisplayName", //2.5, 12.0, 12.1 and 14.0 + 0x11 => "DisplayTo", //2.5, 12.0, 12.1 and 14.0 + 0x12 => "Importance", //2.5, 12.0, 12.1 and 14.0 + 0x13 => "MessageClass", //2.5, 12.0, 12.1 and 14.0 + 0x14 => "Subject", //2.5, 12.0, 12.1 and 14.0 + 0x15 => "Read", //2.5, 12.0, 12.1 and 14.0 + 0x16 => "To", //2.5, 12.0, 12.1 and 14.0 + 0x17 => "Cc", //2.5, 12.0, 12.1 and 14.0 + 0x18 => "From", //2.5, 12.0, 12.1 and 14.0 + 0x19 => "Reply-To", //ReplyTo 2.5, 12.0, 12.1 and 14.0 + 0x1a => "AllDayEvent", //2.5, 12.0, 12.1 and 14.0 + 0x1b => "Categories", //2.5, 12.0, 12.1 and 14.0 + 0x1c => "Category", //2.5, 12.0, 12.1 and 14.0 + 0x1d => "DtStamp", //2.5, 12.0, 12.1 and 14.0 + 0x1e => "EndTime", //2.5, 12.0, 12.1 and 14.0 + 0x1f => "InstanceType", //2.5, 12.0, 12.1 and 14.0 + 0x20 => "BusyStatus", //2.5, 12.0, 12.1 and 14.0 + 0x21 => "Location", //2.5, 12.0, 12.1 and 14.0 + 0x22 => "MeetingRequest", //2.5, 12.0, 12.1 and 14.0 + 0x23 => "Organizer", //2.5, 12.0, 12.1 and 14.0 + 0x24 => "RecurrenceId", //2.5, 12.0, 12.1 and 14.0 + 0x25 => "Reminder", //2.5, 12.0, 12.1 and 14.0 + 0x26 => "ResponseRequested", //2.5, 12.0, 12.1 and 14.0 + 0x27 => "Recurrences", //2.5, 12.0, 12.1 and 14.0 + 0x28 => "Recurrence", //2.5, 12.0, 12.1 and 14.0 + 0x29 => "Type", //Recurrence_Type //2.5, 12.0, 12.1 and 14.0 + 0x2a => "Until", //Recurrence_Until //2.5, 12.0, 12.1 and 14.0 + 0x2b => "Occurrences", //Recurrence_Occurrences //2.5, 12.0, 12.1 and 14.0 + 0x2c => "Interval", //Recurrence_Interval //2.5, 12.0, 12.1 and 14.0 + 0x2d => "DayOfWeek", //Recurrence_DayOfWeek //2.5, 12.0, 12.1 and 14.0 + 0x2e => "DayOfMonth", //Recurrence_DayOfMonth //2.5, 12.0, 12.1 and 14.0 + 0x2f => "WeekOfMonth", //Recurrence_WeekOfMonth //2.5, 12.0, 12.1 and 14.0 + 0x30 => "MonthOfYear", //Recurrence_MonthOfYear //2.5, 12.0, 12.1 and 14.0 + 0x31 => "StartTime", //2.5, 12.0, 12.1 and 14.0 + 0x32 => "Sensitivity", //2.5, 12.0, 12.1 and 14.0 + 0x33 => "TimeZone", //2.5, 12.0, 12.1 and 14.0 + 0x34 => "GlobalObjId", //2.5, 12.0, 12.1 and 14.0 + 0x35 => "ThreadTopic", //2.5, 12.0, 12.1 and 14.0 + 0x36 => "MIMEData", //2.5 + 0x37 => "MIMETruncated", //2.5 + 0x38 => "MIMESize", //2.5 + 0x39 => "InternetCPID", //2.5, 12.0, 12.1 and 14.0 + 0x3a => "Flag", //12.0, 12.1 and 14.0 + 0x3b => "FlagStatus", //12.0, 12.1 and 14.0 + 0x3c => "ContentClass", //12.0, 12.1 and 14.0 + 0x3d => "FlagType", //12.0, 12.1 and 14.0 + 0x3e => "CompleteTime", //14.0 + 0x3f => "DisallowNewTimeProposal", //14.0 + ), + 3 => array ( //Code page 3 is no longer in use, however, tokens 05 through 17 have been defined. 20100501 + 0x05 => "Notify", + 0x06 => "Notification", + 0x07 => "Version", + 0x08 => "Lifetime", + 0x09 => "DeviceInfo", + 0x0a => "Enable", + 0x0b => "Folder", + 0x0c => "ServerEntryId", + 0x0d => "DeviceAddress", + 0x0e => "ValidCarrierProfiles", + 0x0f => "CarrierProfile", + 0x10 => "Status", + 0x11 => "Replies", +// 0x05 => "Version='1.1'", + 0x12 => "Devices", + 0x13 => "Device", + 0x14 => "Id", + 0x15 => "Expiry", + 0x16 => "NotifyGUID", + ), + 4 => array ( + 0x05 => "Timezone", //2.5, 12.0, 12.1 and 14.0 + 0x06 => "AllDayEvent", //2.5, 12.0, 12.1 and 14.0 + 0x07 => "Attendees", //2.5, 12.0, 12.1 and 14.0 + 0x08 => "Attendee", //2.5, 12.0, 12.1 and 14.0 + 0x09 => "Email", //Attendee_Email //2.5, 12.0, 12.1 and 14.0 + 0x0a => "Name", //Attendee_Name //2.5, 12.0, 12.1 and 14.0 + 0x0b => "Body", //2.5, but is in code page 17 in ActiveSync versions 12.0, 12.1, and 14.0 + 0x0c => "BodyTruncated", //2.5, 12.0, 12.1 and 14.0 + 0x0d => "BusyStatus", //2.5, 12.0, 12.1 and 14.0 + 0x0e => "Categories", //2.5, 12.0, 12.1 and 14.0 + 0x0f => "Category", //2.5, 12.0, 12.1 and 14.0 + 0x10 => "Rtf", //2.5 + 0x11 => "DtStamp", //2.5, 12.0, 12.1 and 14.0 + 0x12 => "EndTime", //2.5, 12.0, 12.1 and 14.0 + 0x13 => "Exception", //2.5, 12.0, 12.1 and 14.0 + 0x14 => "Exceptions", //2.5, 12.0, 12.1 and 14.0 + 0x15 => "Deleted", //Exception_Deleted //2.5, 12.0, 12.1 and 14.0 + 0x16 => "ExceptionStartTime", //Exception_StartTime //2.5, 12.0, 12.1 and 14.0 + 0x17 => "Location", //2.5, 12.0, 12.1 and 14.0 + 0x18 => "MeetingStatus", //2.5, 12.0, 12.1 and 14.0 + 0x19 => "OrganizerEmail", //Organizer_Email //2.5, 12.0, 12.1 and 14.0 + 0x1a => "OrganizerName", //Organizer_Name //2.5, 12.0, 12.1 and 14.0 + 0x1b => "Recurrence", //2.5, 12.0, 12.1 and 14.0 + 0x1c => "Type", //Recurrence_Type //2.5, 12.0, 12.1 and 14.0 + 0x1d => "Until", //Recurrence_Until //2.5, 12.0, 12.1 and 14.0 + 0x1e => "Occurrences", //Recurrence_Occurrences //2.5, 12.0, 12.1 and 14.0 + 0x1f => "Interval", //Recurrence_Interval //2.5, 12.0, 12.1 and 14.0 + 0x20 => "DayOfWeek", //Recurrence_DayOfWeek //2.5, 12.0, 12.1 and 14.0 + 0x21 => "DayOfMonth", //Recurrence_DayOfMonth //2.5, 12.0, 12.1 and 14.0 + 0x22 => "WeekOfMonth", //Recurrence_WeekOfMonth //2.5, 12.0, 12.1 and 14.0 + 0x23 => "MonthOfYear", //Recurrence_MonthOfYear //2.5, 12.0, 12.1 and 14.0 + 0x24 => "Reminder", //Reminder_MinsBefore //2.5, 12.0, 12.1 and 14.0 + 0x25 => "Sensitivity", //2.5, 12.0, 12.1 and 14.0 + 0x26 => "Subject", //2.5, 12.0, 12.1 and 14.0 + 0x27 => "StartTime", //2.5, 12.0, 12.1 and 14.0 + 0x28 => "UID", //2.5, 12.0, 12.1 and 14.0 + 0x29 => "Attendee_Status", //12.0, 12.1 and 14.0 + 0x2a => "Attendee_Type", //12.0, 12.1 and 14.0 + 0x2b => "Attachment", //12.0, 12.1 and 14.0 + 0x2c => "Attachments", //12.0, 12.1 and 14.0 + 0x2d => "AttName", //12.0, 12.1 and 14.0 + 0x2e => "AttSize", //12.0, 12.1 and 14.0 + 0x2f => "AttOid", //12.0, 12.1 and 14.0 + 0x30 => "AttMethod", //12.0, 12.1 and 14.0 + 0x31 => "AttRemoved", //12.0, 12.1 and 14.0 + 0x32 => "DisplayName", //12.0, 12.1 and 14.0 + 0x33 => "DisallowNewTimeProposal", //14.0 + 0x34 => "ResponseRequested", //14.0 + 0x35 => "AppointmentReplyTime", //14.0 + 0x36 => "ResponseType", //14.0 + 0x37 => "CalendarType", //14.0 + 0x38 => "IsLeapMonth", //14.0 + 0x39 => "FirstDayOfWeek", //post 14.0 20100501 + 0x3a => "OnlineMeetingInternalLink", //post 14.0 20100501 + 0x3b => "OnlineMeetingExternalLink", //post 14.0 20120630 + ), + 5 => array ( + 0x05 => "Moves", + 0x06 => "Move", + 0x07 => "SrcMsgId", + 0x08 => "SrcFldId", + 0x09 => "DstFldId", + 0x0a => "Response", + 0x0b => "Status", + 0x0c => "DstMsgId", + ), + 6 => array ( + 0x05 => "GetItemEstimate", + 0x06 => "Version", //only 12.1 20100501 + 0x07 => "Folders", //Collections + 0x08 => "Folder", //Collection + 0x09 => "FolderType", //Class //only 12.1 //The tag defined in code page 0 should be used in all other instances. 20100501 + 0x0a => "FolderId", //CollectionId + 0x0b => "DateTime", //not supported by 14. only supported 12.1. 20100501 + 0x0c => "Estimate", + 0x0d => "Response", + 0x0e => "Status", + ), + 7 => array ( + 0x05 => "Folders", //2.0 + 0x06 => "Folder", //2.0 + 0x07 => "DisplayName", + 0x08 => "ServerEntryId", //ServerId + 0x09 => "ParentId", + 0x0a => "Type", + 0x0b => "Response", //2.0 + 0x0c => "Status", + 0x0d => "ContentClass", //2.0 + 0x0e => "Changes", + 0x0f => "Add", + 0x10 => "Remove", + 0x11 => "Update", + 0x12 => "SyncKey", + 0x13 => "FolderCreate", + 0x14 => "FolderDelete", + 0x15 => "FolderUpdate", + 0x16 => "FolderSync", + 0x17 => "Count", + 0x18 => "Version", //2.0 - not defined in 20100501 + ), + 8 => array ( + 0x05 => "CalendarId", + 0x06 => "FolderId", //CollectionId + 0x07 => "MeetingResponse", + 0x08 => "RequestId", + 0x09 => "Request", + 0x0a => "Result", + 0x0b => "Status", + 0x0c => "UserResponse", + 0x0d => "Version", //2.0 - not defined in 20100501 + 0x0e => "InstanceId" // first in 20100501 + ), + 9 => array ( + 0x05 => "Body", //2.5, but is in code page 17 in ActiveSync versions 12.0, 12.1, and 14.0 + 0x06 => "BodySize", //2.5, but is in code page 17 as the EstimatedDataSize tag in ActiveSync versions 12.0, 12.1 and 14.0 + 0x07 => "BodyTruncated", //2.5, but is in code page 17 as the Truncated tag in ActiveSync versions 12.0, 12.1, and 14.0 + 0x08 => "Categories", //2.5, 12.0, 12.1 and 14.0 + 0x09 => "Category", //2.5, 12.0, 12.1 and 14.0 + 0x0a => "Complete", //2.5, 12.0, 12.1 and 14.0 + 0x0b => "DateCompleted", //2.5, 12.0, 12.1 and 14.0 + 0x0c => "DueDate", //2.5, 12.0, 12.1 and 14.0 + 0x0d => "UtcDueDate", //2.5, 12.0, 12.1 and 14.0 + 0x0e => "Importance", //2.5, 12.0, 12.1 and 14.0 + 0x0f => "Recurrence", //2.5, 12.0, 12.1 and 14.0 + 0x10 => "Type", //Recurrence_Type //2.5, 12.0, 12.1 and 14.0 + 0x11 => "Start", //Recurrence_Start //2.5, 12.0, 12.1 and 14.0 + 0x12 => "Until", //Recurrence_Until //2.5, 12.0, 12.1 and 14.0 + 0x13 => "Occurrences", //Recurrence_Occurrences //2.5, 12.0, 12.1 and 14.0 + 0x14 => "Interval", //Recurrence_Interval //2.5, 12.0, 12.1 and 14.0 + 0x16 => "DayOfWeek", //Recurrence_DayOfMonth //2.5, 12.0, 12.1 and 14.0 + 0x15 => "DayOfMonth", //Recurrence_DayOfWeek //2.5, 12.0, 12.1 and 14.0 + 0x17 => "WeekOfMonth", //Recurrence_WeekOfMonth //2.5, 12.0, 12.1 and 14.0 + 0x18 => "MonthOfYear", //Recurrence_MonthOfYear //2.5, 12.0, 12.1 and 14.0 + 0x19 => "Regenerate", //Recurrence_Regenerate //2.5, 12.0, 12.1 and 14.0 + 0x1a => "DeadOccur", //Recurrence_DeadOccur //2.5, 12.0, 12.1 and 14.0 + 0x1b => "ReminderSet", //2.5, 12.0, 12.1 and 14.0 + 0x1c => "ReminderTime", //2.5, 12.0, 12.1 and 14.0 + 0x1d => "Sensitivity", //2.5, 12.0, 12.1 and 14.0 + 0x1e => "StartDate", //2.5, 12.0, 12.1 and 14.0 + 0x1f => "UtcStartDate", //2.5, 12.0, 12.1 and 14.0 + 0x20 => "Subject", //2.5, 12.0, 12.1 and 14.0 + 0x21 => "Rtf", //CompressedRTF //2.5, but is in code page 17 as the Type tag in Active Sync versions 12.0, 12.1, and 14.0 + 0x22 => "OrdinalDate", //12.0, 12.1 and 14.0 + 0x23 => "SubOrdinalDate", //12.0, 12.1 and 14.0 + 0x24 => "CalendarType", //14.0 + 0x25 => "IsLeapMonth", //14.0 + 0x26 => "FirstDayOfWeek", // first in 20100501 post 14.0 + ), + 0xa => array ( + 0x05 => "ResolveRecipients", + 0x06 => "Response", + 0x07 => "Status", + 0x08 => "Type", + 0x09 => "Recipient", + 0x0a => "DisplayName", + 0x0b => "EmailAddress", + 0x0c => "Certificates", + 0x0d => "Certificate", + 0x0e => "MiniCertificate", + 0x0f => "Options", + 0x10 => "To", + 0x11 => "CertificateRetrieval", + 0x12 => "RecipientCount", + 0x13 => "MaxCertificates", + 0x14 => "MaxAmbiguousRecipients", + 0x15 => "CertificateCount", + 0x16 => "Availability", //14.0 + 0x17 => "StartTime", //14.0 + 0x18 => "EndTime", //14.0 + 0x19 => "MergedFreeBusy", //14.0 + 0x1A => "Picture", // first in 20100501 post 14.0 + 0x1B => "MaxSize", // first in 20100501 post 14.0 + 0x1C => "Data", // first in 20100501 post 14.0 + 0x1D => "MaxPictures", // first in 20100501 post 14.0 + ), + 0xb => array ( + 0x05 => "ValidateCert", + 0x06 => "Certificates", + 0x07 => "Certificate", + 0x08 => "CertificateChain", + 0x09 => "CheckCRL", + 0x0a => "Status", + ), + 0xc => array ( + 0x05 => "CustomerId", + 0x06 => "GovernmentId", + 0x07 => "IMAddress", + 0x08 => "IMAddress2", + 0x09 => "IMAddress3", + 0x0a => "ManagerName", + 0x0b => "CompanyMainPhone", + 0x0c => "AccountName", + 0x0d => "NickName", + 0x0e => "MMS", + ), + 0xd => array ( + 0x05 => "Ping", + 0x06 => "AutdState", //(Not used by protocol) + 0x07 => "Status", + 0x08 => "LifeTime", //HeartbeatInterval + 0x09 => "Folders", + 0x0a => "Folder", + 0x0b => "ServerEntryId", //Id + 0x0c => "FolderType", //Class + 0x0d => "MaxFolders", + 0x0e => "Version" //not defined in 20100501 + ), + 0xe => array ( + 0x05 => "Provision", //2.5, 12.0, 12.1 and 14.0 + 0x06 => "Policies", //2.5, 12.0, 12.1 and 14.0 + 0x07 => "Policy", //2.5, 12.0, 12.1 and 14.0 + 0x08 => "PolicyType", //2.5, 12.0, 12.1 and 14.0 + 0x09 => "PolicyKey", //2.5, 12.0, 12.1 and 14.0 + 0x0A => "Data", //2.5, 12.0, 12.1 and 14.0 + 0x0B => "Status", //2.5, 12.0, 12.1 and 14.0 + 0x0C => "RemoteWipe", //2.5, 12.0, 12.1 and 14.0 + 0x0D => "EASProvisionDoc", //12.0, 12.1 and 14.0 + 0x0E => "DevicePasswordEnabled", //12.0, 12.1 and 14.0 + 0x0F => "AlphanumericDevicePasswordRequired", //12.0, 12.1 and 14.0 + 0x10 => "DeviceEncryptionEnabled", //12.0, 12.1 and 14.0 + //0x10 => "RequireStorageCardEncryption", //12.1 and 14.0 + 0x11 => "PasswordRecoveryEnabled", //12.0, 12.1 and 14.0 + 0x12 => "DocumentBrowseEnabled", //2.0 and 2.5. + 0x13 => "AttachmentsEnabled", //12.0, 12.1 and 14.0 + 0x14 => "MinDevicePasswordLength", //12.0, 12.1 and 14.0 + 0x15 => "MaxInactivityTimeDeviceLock", //12.0, 12.1 and 14.0 + 0x16 => "MaxDevicePasswordFailedAttempts", //12.0, 12.1 and 14.0 + 0x17 => "MaxAttachmentSize", //12.0, 12.1 and 14.0 + 0x18 => "AllowSimpleDevicePassword", //12.0, 12.1 and 14.0 + 0x19 => "DevicePasswordExpiration", //12.0, 12.1 and 14.0 + 0x1A => "DevicePasswordHistory", //12.0, 12.1 and 14.0 + 0x1B => "AllowStorageCard", //12.1 and 14.0 + 0x1C => "AllowCamera", //12.1 and 14.0 + 0x1D => "RequireDeviceEncryption", //12.1 and 14.0 + 0x1E => "AllowUnsignedApplications", //12.1 and 14.0 + 0x1F => "AllowUnsignedInstallationPackages", //12.1 and 14.0 + 0x20 => "MinDevicePasswordComplexCharacters", //12.1 and 14.0 + 0x21 => "AllowWiFi", //12.1 and 14.0 + 0x22 => "AllowTextMessaging", //12.1 and 14.0 + 0x23 => "AllowPOPIMAPEmail", //12.1 and 14.0 + 0x24 => "AllowBluetooth", //12.1 and 14.0 + 0x25 => "AllowIrDA", //12.1 and 14.0 + 0x26 => "RequireManualSyncWhenRoaming", //12.1 and 14.0 + 0x27 => "AllowDesktopSync", //12.1 and 14.0 + 0x28 => "MaxCalendarAgeFilter", //12.1 and 14.0 + 0x29 => "AllowHTMLEmail", //12.1 and 14.0 + 0x2A => "MaxEmailAgeFilter", //12.1 and 14.0 + 0x2B => "MaxEmailBodyTruncationSize", //12.1 and 14.0 + 0x2C => "MaxEmailHTMLBodyTruncationSize", //12.1 and 14.0 + 0x2D => "RequireSignedSMIMEMessages", //12.1 and 14.0 + 0x2E => "RequireEncryptedSMIMEMessages", //12.1 and 14.0 + 0x2F => "RequireSignedSMIMEAlgorithm", //12.1 and 14.0 + 0x30 => "RequireEncryptionSMIMEAlgorithm", //12.1 and 14.0 + 0x31 => "AllowSMIMEEncryptionAlgorithmNegotiation", //12.1 and 14.0 + 0x32 => "AllowSMIMESoftCerts", //12.1 and 14.0 + 0x33 => "AllowBrowser", //12.1 and 14.0 + 0x34 => "AllowConsumerEmail", //12.1 and 14.0 + 0x35 => "AllowRemoteDesktop", //12.1 and 14.0 + 0x36 => "AllowInternetSharing", //12.1 and 14.0 + 0x37 => "UnapprovedInROMApplicationList", //12.1 and 14.0 + 0x38 => "ApplicationName", //12.1 and 14.0 + 0x39 => "ApprovedApplicationList", //12.1 and 14.0 + 0x3A => "Hash", //12.1 and 14.0 + ), + 0xf => array( + 0x05 => "Search", //12.0, 12.1 and 14.0 + 0x07 => "Store", //12.0, 12.1 and 14.0 + 0x08 => "Name", //12.0, 12.1 and 14.0 + 0x09 => "Query", //12.0, 12.1 and 14.0 + 0x0A => "Options", //12.0, 12.1 and 14.0 + 0x0B => "Range", //12.0, 12.1 and 14.0 + 0x0C => "Status", //12.0, 12.1 and 14.0 + 0x0D => "Response", //12.0, 12.1 and 14.0 + 0x0E => "Result", //12.0, 12.1 and 14.0 + 0x0F => "Properties", //12.0, 12.1 and 14.0 + 0x10 => "Total", //12.0, 12.1 and 14.0 + 0x11 => "EqualTo", //12.0, 12.1 and 14.0 + 0x12 => "Value", //12.0, 12.1 and 14.0 + 0x13 => "And", //12.0, 12.1 and 14.0 + 0x14 => "Or", //14.0 + 0x15 => "FreeText", //12.0, 12.1 and 14.0 + 0x17 => "DeepTraversal", //12.0, 12.1 and 14.0 + 0x18 => "LongId", //12.0, 12.1 and 14.0 + 0x19 => "RebuildResults", //12.0, 12.1 and 14.0 + 0x1A => "LessThan", //12.0, 12.1 and 14.0 + 0x1B => "GreaterThan", //12.0, 12.1 and 14.0 + 0x1C => "Schema", //12.0, 12.1 and 14.0 + 0x1D => "Supported", //12.0, 12.1 and 14.0 + 0x1E => "UserName", //12.1 and 14.0 + 0x1F => "Password", //12.1 and 14.0 + 0x20 => "ConversationId", //14.0 + 0x21 => "Picture", // first in 20100501 post 14.0 + 0x22 => "MaxSize", // first in 20100501 post 14.0 + 0x23 => "MaxPictures", // first in 20100501 post 14.0 + ), + 0x10 => array( + 0x05 => "DisplayName", + 0x06 => "Phone", + 0x07 => "Office", + 0x08 => "Title", + 0x09 => "Company", + 0x0A => "Alias", + 0x0B => "FirstName", + 0x0C => "LastName", + 0x0D => "HomePhone", + 0x0E => "MobilePhone", + 0x0F => "EmailAddress", + 0x10 => "Picture", // first in 20100501 post 14.0 + 0x11 => "Status", // first in 20100501 post 14.0 + 0x12 => "Data", // first in 20100501 post 14.0 + ), + 0x11 => array( //12.0, 12.1 and 14.0 + 0x05 => "BodyPreference", + 0x06 => "Type", + 0x07 => "TruncationSize", + 0x08 => "AllOrNone", + 0x0A => "Body", + 0x0B => "Data", + 0x0C => "EstimatedDataSize", + 0x0D => "Truncated", + 0x0E => "Attachments", + 0x0F => "Attachment", + 0x10 => "DisplayName", + 0x11 => "FileReference", + 0x12 => "Method", + 0x13 => "ContentId", + 0x14 => "ContentLocation", //not used + 0x15 => "IsInline", + 0x16 => "NativeBodyType", + 0x17 => "ContentType", + 0x18 => "Preview", //14.0 + 0x19 => "BodyPartPreference", // first in 20100501 post 14.0 + 0x1A => "BodyPart", // first in 20100501 post 14.0 + 0x1B => "Status", // first in 20100501 post 14.0 + ), + 0x12 => array( //12.0, 12.1 and 14.0 + 0x05 => "Settings", //12.0, 12.1 and 14.0 + 0x06 => "Status", //12.0, 12.1 and 14.0 + 0x07 => "Get", //12.0, 12.1 and 14.0 + 0x08 => "Set", //12.0, 12.1 and 14.0 + 0x09 => "Oof", //12.0, 12.1 and 14.0 + 0x0A => "OofState", //12.0, 12.1 and 14.0 + 0x0B => "StartTime", //12.0, 12.1 and 14.0 + 0x0C => "EndTime", //12.0, 12.1 and 14.0 + 0x0D => "OofMessage", //12.0, 12.1 and 14.0 + 0x0E => "AppliesToInternal", //12.0, 12.1 and 14.0 + 0x0F => "AppliesToExternalKnown", //12.0, 12.1 and 14.0 + 0x10 => "AppliesToExternalUnknown", //12.0, 12.1 and 14.0 + 0x11 => "Enabled", //12.0, 12.1 and 14.0 + 0x12 => "ReplyMessage", //12.0, 12.1 and 14.0 + 0x13 => "BodyType", //12.0, 12.1 and 14.0 + 0x14 => "DevicePassword", //12.0, 12.1 and 14.0 + 0x15 => "Password", //12.0, 12.1 and 14.0 + 0x16 => "DeviceInformaton", //12.0, 12.1 and 14.0 + 0x17 => "Model", //12.0, 12.1 and 14.0 + 0x18 => "IMEI", //12.0, 12.1 and 14.0 + 0x19 => "FriendlyName", //12.0, 12.1 and 14.0 + 0x1A => "OS", //12.0, 12.1 and 14.0 + 0x1B => "OSLanguage", //12.0, 12.1 and 14.0 + 0x1C => "PhoneNumber", //12.0, 12.1 and 14.0 + 0x1D => "UserInformation", //12.0, 12.1 and 14.0 + 0x1E => "EmailAddresses", //12.0, 12.1 and 14.0 + 0x1F => "SmtpAddress", //12.0, 12.1 and 14.0 + 0x20 => "UserAgent", //12.1 and 14.0 + 0x21 => "EnableOutboundSMS", //14.0 + 0x22 => "MobileOperator", //14.0 + 0x23 => "PrimarySmtpAddress", // first in 20100501 post 14.0 + 0x24 => "Accounts", // first in 20100501 post 14.0 + 0x25 => "Account", // first in 20100501 post 14.0 + 0x26 => "AccountId", // first in 20100501 post 14.0 + 0x27 => "AccountName", // first in 20100501 post 14.0 + 0x28 => "UserDisplayName", // first in 20100501 post 14.0 + 0x29 => "SendDisabled", // first in 20100501 post 14.0 + 0x2B => "ihsManagementInformation", // first in 20100501 post 14.0 + ), + 0x13 => array( //12.0, 12.1 and 14.0 + 0x05 => "LinkId", + 0x06 => "DisplayName", + 0x07 => "IsFolder", + 0x08 => "CreationDate", + 0x09 => "LastModifiedDate", + 0x0A => "IsHidden", + 0x0B => "ContentLength", + 0x0C => "ContentType", + ), + 0x14 => array( //12.0, 12.1 and 14.0 + 0x05 => "ItemOperations", + 0x06 => "Fetch", + 0x07 => "Store", + 0x08 => "Options", + 0x09 => "Range", + 0x0A => "Total", + 0x0B => "Properties", + 0x0C => "Data", + 0x0D => "Status", + 0x0E => "Response", + 0x0F => "Version", + 0x10 => "Schema", + 0x11 => "Part", + 0x12 => "EmptyFolderContents", + 0x13 => "DeleteSubFolders", + 0x14 => "UserName", //12.1 and 14.0 + 0x15 => "Password", //12.1 and 14.0 + 0x16 => "Move", //14.0 + 0x17 => "DstFldId", //14.0 + 0x18 => "ConversationId", //14.0 + 0x19 => "MoveAlways", //14.0 + ), + 0x15 => array( //14.0 + 0x05 => "SendMail", + 0x06 => "SmartForward", + 0x07 => "SmartReply", + 0x08 => "SaveInSentItems", + 0x09 => "ReplaceMime", + 0x0A => "Type", + 0x0B => "Source", + 0x0C => "FolderId", + 0x0D => "ItemId", + 0x0E => "LongId", + 0x0F => "InstanceId", + 0x10 => "MIME", + 0x11 => "ClientId", + 0x12 => "Status", + 0x13 => "AccountId", // first in 20100501 post 14.0 + ), + 0x16 => array( // 14.0 + 0x05 => "UmCallerId", + 0x06 => "UmUserNotes", + 0x07 => "UmAttDuration", + 0x08 => "UmAttOrder", + 0x09 => "ConversationId", + 0x0A => "ConversationIndex", + 0x0B => "LastVerbExecuted", + 0x0C => "LastVerbExecutionTime", + 0x0D => "ReceivedAsBcc", + 0x0E => "Sender", + 0x0F => "CalendarType", + 0x10 => "IsLeapMonth", + 0x11 => "AccountId", // first in 20100501 post 14.0 + 0x12 => "FirstDayOfWeek", // first in 20100501 post 14.0 + 0x13 => "MeetingMessageType", // first in 20100501 post 14.0 + ), + 0x17 => array( //14.0 + 0x05 => "Subject", + 0x06 => "MessageClass", + 0x07 => "LastModifiedDate", + 0x08 => "Categories", + 0x09 => "Category", + ), + 0x18 => array( // post 14.0 + 0x05 => "RightsManagementSupport", + 0x06 => "RightsManagementTemplates", + 0x07 => "RightsManagementTemplate", + 0x08 => "RightsManagementLicense", + 0x09 => "EditAllowed", + 0x0A => "ReplyAllowed", + 0x0B => "ReplyAllAllowed", + 0x0C => "ForwardAllowed", + 0x0D => "ModifyRecipientsAllowed", + 0x0E => "ExtractAllowed", + 0x0F => "PrintAllowed", + 0x10 => "ExportAllowed", + 0x11 => "ProgrammaticAccessAllowed", + 0x12 => "RMOwner", + 0x13 => "ContentExpiryDate", + 0x14 => "TemplateID", + 0x15 => "TemplateName", + 0x16 => "TemplateDescription", + 0x17 => "ContentOwner", + 0x18 => "RemoveRightsManagementDistribution", + ), + ), + "namespaces" => array( + //0 => "AirSync", // + 1 => "POOMCONTACTS", + 2 => "POOMMAIL", + 3 => "AirNotify", //no longer used + 4 => "POOMCAL", + 5 => "Move", + 6 => "GetItemEstimate", + 7 => "FolderHierarchy", + 8 => "MeetingResponse", + 9 => "POOMTASKS", + 0xA => "ResolveRecipients", + 0xB => "ValidateCert", + 0xC => "POOMCONTACTS2", + 0xD => "Ping", + 0xE => "Provision",// + 0xF => "Search",// + 0x10 => "GAL", + 0x11 => "AirSyncBase", //12.0, 12.1 and 14.0 + 0x12 => "Settings", //12.0, 12.1 and 14.0. + 0x13 => "DocumentLibrary", //12.0, 12.1 and 14.0 + 0x14 => "ItemOperations", //12.0, 12.1 and 14.0 + 0x15 => "ComposeMail", //14.0 + 0x16 => "POOMMAIL2", //14.0 + 0x17 => "Notes", //14.0 + 0x18 => "RightsManagement", + ) + ); +} + +?> \ No newline at end of file diff --git a/sources/lib/wbxml/wbxmlencoder.php b/sources/lib/wbxml/wbxmlencoder.php new file mode 100644 index 0000000..cf36ec3 --- /dev/null +++ b/sources/lib/wbxml/wbxmlencoder.php @@ -0,0 +1,507 @@ +. +* +* Consult LICENSE file for details +************************************************/ + + +class WBXMLEncoder extends WBXMLDefs { + private $_dtd; + private $_out; + + private $_tagcp; + private $_attrcp; + + private $logStack = array(); + + // We use a delayed output mechanism in which we only output a tag when it actually has something + // in it. This can cause entire XML trees to disappear if they don't have output data in them; Ie + // calling 'startTag' 10 times, and then 'endTag' will cause 0 bytes of output apart from the header. + + // Only when content() is called do we output the current stack of tags + + private $_stack; + + private $multipart; // the content is multipart + private $bodyparts; + + public function WBXMLEncoder($output, $multipart = false) { + // make sure WBXML_DEBUG is defined. It should be at this point + if (!defined('WBXML_DEBUG')) define('WBXML_DEBUG', false); + + $this->_out = $output; + + $this->_tagcp = 0; + $this->_attrcp = 0; + + // reverse-map the DTD + foreach($this->dtd["namespaces"] as $nsid => $nsname) { + $this->_dtd["namespaces"][$nsname] = $nsid; + } + + foreach($this->dtd["codes"] as $cp => $value) { + $this->_dtd["codes"][$cp] = array(); + foreach($this->dtd["codes"][$cp] as $tagid => $tagname) { + $this->_dtd["codes"][$cp][$tagname] = $tagid; + } + } + $this->_stack = array(); + $this->multipart = $multipart; + $this->bodyparts = array(); + } + + /** + * Puts the WBXML header on the stream + * + * @access public + * @return + */ + public function startWBXML() { + if ($this->multipart) { + header("Content-Type: application/vnd.ms-sync.multipart"); + ZLog::Write(LOGLEVEL_DEBUG, "WBXMLEncoder->startWBXML() type: vnd.ms-sync.multipart"); + } + else { + header("Content-Type: application/vnd.ms-sync.wbxml"); + ZLog::Write(LOGLEVEL_DEBUG, "WBXMLEncoder->startWBXML() type: vnd.ms-sync.wbxml"); + } + + $this->outByte(0x03); // WBXML 1.3 + $this->outMBUInt(0x01); // Public ID 1 + $this->outMBUInt(106); // UTF-8 + $this->outMBUInt(0x00); // string table length (0) + } + + /** + * Puts a StartTag on the output stack + * + * @param $tag + * @param $attributes + * @param $nocontent + * + * @access public + * @return + */ + public function startTag($tag, $attributes = false, $nocontent = false) { + $stackelem = array(); + + if(!$nocontent) { + $stackelem['tag'] = $tag; + $stackelem['attributes'] = $attributes; + $stackelem['nocontent'] = $nocontent; + $stackelem['sent'] = false; + + array_push($this->_stack, $stackelem); + + // If 'nocontent' is specified, then apparently the user wants to force + // output of an empty tag, and we therefore output the stack here + } else { + $this->_outputStack(); + $this->_startTag($tag, $attributes, $nocontent); + } + } + + /** + * Puts an EndTag on the stack + * + * @access public + * @return + */ + public function endTag() { + $stackelem = array_pop($this->_stack); + + // Only output end tags for items that have had a start tag sent + if($stackelem['sent']) { + $this->_endTag(); + + if(count($this->_stack) == 0) + ZLog::Write(LOGLEVEL_DEBUG, "WBXMLEncoder->endTag() WBXML output completed"); + + if(count($this->_stack) == 0 && $this->multipart == true) { + $this->processMultipart(); + } + } + } + + /** + * Puts content on the output stack + * + * @param $content + * + * @access public + * @return string + */ + public function content($content) { + // We need to filter out any \0 chars because it's the string terminator in WBXML. We currently + // cannot send \0 characters within the XML content anywhere. + $content = str_replace("\0","",$content); + + if("x" . $content == "x") + return; + $this->_outputStack(); + $this->_content($content); + } + + /** + * Gets the value of multipart + * + * @access public + * @return boolean + */ + public function getMultipart() { + return $this->multipart; + } + + /** + * Adds a bodypart + * + * @param Stream $bp + * + * @access public + * @return void + */ + public function addBodypartStream($bp) { + if ($this->multipart) + $this->bodyparts[] = $bp; + } + + /** + * Gets the number of bodyparts + * + * @access public + * @return int + */ + public function getBodypartsCount() { + return count($this->bodyparts); + } + + /**---------------------------------------------------------------------------------------------------------- + * Private WBXMLEncoder stuff + */ + + /** + * Output any tags on the stack that haven't been output yet + * + * @access private + * @return + */ + private function _outputStack() { + for($i=0;$i_stack);$i++) { + if(!$this->_stack[$i]['sent']) { + $this->_startTag($this->_stack[$i]['tag'], $this->_stack[$i]['attributes'], $this->_stack[$i]['nocontent']); + $this->_stack[$i]['sent'] = true; + } + } + } + + /** + * Outputs an actual start tag + * + * @access private + * @return + */ + private function _startTag($tag, $attributes = false, $nocontent = false) { + $this->logStartTag($tag, $attributes, $nocontent); + + $mapping = $this->getMapping($tag); + + if(!$mapping) + return false; + + if($this->_tagcp != $mapping["cp"]) { + $this->outSwitchPage($mapping["cp"]); + $this->_tagcp = $mapping["cp"]; + } + + $code = $mapping["code"]; + if(isset($attributes) && is_array($attributes) && count($attributes) > 0) { + $code |= 0x80; + } + + if(!isset($nocontent) || !$nocontent) + $code |= 0x40; + + $this->outByte($code); + + if($code & 0x80) + $this->outAttributes($attributes); + } + + /** + * Outputs actual data + * + * @access private + * @return + */ + private function _content($content) { + $this->logContent($content); + $this->outByte(WBXML_STR_I); + $this->outTermStr($content); + } + + /** + * Outputs an actual end tag + * + * @access private + * @return + */ + private function _endTag() { + $this->logEndTag(); + $this->outByte(WBXML_END); + } + + /** + * Outputs a byte + * + * @param $byte + * + * @access private + * @return + */ + private function outByte($byte) { + fwrite($this->_out, chr($byte)); + } + + /** + * Outputs a string table + * + * @param $uint + * + * @access private + * @return + */ + private function outMBUInt($uint) { + while(1) { + $byte = $uint & 0x7f; + $uint = $uint >> 7; + if($uint == 0) { + $this->outByte($byte); + break; + } else { + $this->outByte($byte | 0x80); + } + } + } + + /** + * Outputs content with string terminator + * + * @param $content + * + * @access private + * @return + */ + private function outTermStr($content) { + fwrite($this->_out, $content); + fwrite($this->_out, chr(0)); + } + + /** + * Output attributes + * We don't actually support this, because to do so, we would have + * to build a string table before sending the data (but we can't + * because we're streaming), so we'll just send an END, which just + * terminates the attribute list with 0 attributes. + * + * @access private + * @return + */ + private function outAttributes() { + $this->outByte(WBXML_END); + } + + /** + * Switches the codepage + * + * @param $page + * + * @access private + * @return + */ + private function outSwitchPage($page) { + $this->outByte(WBXML_SWITCH_PAGE); + $this->outByte($page); + } + + /** + * Get the mapping for a tag + * + * @param $tag + * + * @access private + * @return array + */ + private function getMapping($tag) { + $mapping = array(); + + $split = $this->splitTag($tag); + + if(isset($split["ns"])) { + $cp = $this->_dtd["namespaces"][$split["ns"]]; + } + else { + $cp = 0; + } + + $code = $this->_dtd["codes"][$cp][$split["tag"]]; + + $mapping["cp"] = $cp; + $mapping["code"] = $code; + + return $mapping; + } + + /** + * Split a tag from a the fulltag (namespace + tag) + * + * @param $fulltag + * + * @access private + * @return array keys: 'ns' (namespace), 'tag' (tag) + */ + private function splitTag($fulltag) { + $ns = false; + $pos = strpos($fulltag, chr(58)); // chr(58) == ':' + + if($pos) { + $ns = substr($fulltag, 0, $pos); + $tag = substr($fulltag, $pos+1); + } + else { + $tag = $fulltag; + } + + $ret = array(); + if($ns) + $ret["ns"] = $ns; + $ret["tag"] = $tag; + + return $ret; + } + + /** + * Logs a StartTag to ZLog + * + * @param $tag + * @param $attr + * @param $nocontent + * + * @access private + * @return + */ + private function logStartTag($tag, $attr, $nocontent) { + if(!WBXML_DEBUG) + return; + + $spaces = str_repeat(" ", count($this->logStack)); + if($nocontent) + ZLog::Write(LOGLEVEL_WBXML,"O " . $spaces . " <$tag/>"); + else { + array_push($this->logStack, $tag); + ZLog::Write(LOGLEVEL_WBXML,"O " . $spaces . " <$tag>"); + } + } + + /** + * Logs a EndTag to ZLog + * + * @access private + * @return + */ + private function logEndTag() { + if(!WBXML_DEBUG) + return; + + $spaces = str_repeat(" ", count($this->logStack)); + $tag = array_pop($this->logStack); + ZLog::Write(LOGLEVEL_WBXML,"O " . $spaces . ""); + } + + /** + * Logs content to ZLog + * + * @param $content + * + * @access private + * @return + */ + private function logContent($content) { + if(!WBXML_DEBUG) + return; + + $spaces = str_repeat(" ", count($this->logStack)); + ZLog::Write(LOGLEVEL_WBXML,"O " . $spaces . $content); + } + + /** + * Processes the multipart response + * + * @access private + * @return void + */ + private function processMultipart() { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("WBXMLEncoder->processMultipart() with %d parts to be processed", $this->getBodypartsCount())); + $len = ob_get_length(); + $buffer = ob_get_clean(); + $nrBodyparts = $this->getBodypartsCount(); + $blockstart = (($nrBodyparts + 1) * 2) * 4 + 4; + + $data = pack("iii", ($nrBodyparts + 1), $blockstart, $len); + + ob_start(null, 1048576); + + foreach ($this->bodyparts as $bp) { + $blockstart = $blockstart + $len; + $len = fstat($bp); + $len = (isset($len['size'])) ? $len['size'] : 0; + $data .= pack("ii", $blockstart, $len); + } + + fwrite($this->_out, $data); + fwrite($this->_out, $buffer); + foreach($this->bodyparts as $bp) { + while (!feof($bp)) { + fwrite($this->_out, fread($bp, 4096)); + } + } + } +} + +?> \ No newline at end of file diff --git a/sources/lib/webservice/webservice.php b/sources/lib/webservice/webservice.php new file mode 100644 index 0000000..36040a5 --- /dev/null +++ b/sources/lib/webservice/webservice.php @@ -0,0 +1,95 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +class Webservice { + private $server; + + /** + * Handles a webservice command + * + * @param int $commandCode + * + * @access public + * @return boolean + * @throws SoapFault + */ + public function Handle($commandCode) { + if (Request::GetDeviceType() !== "webservice" || Request::GetDeviceID() !== "webservice") + throw new FatalException("Invalid device id and type for webservice execution"); + + if (Request::GetGETUser() != Request::GetAuthUser()) + ZLog::Write(LOGLEVEL_INFO, sprintf("Webservice::HandleWebservice('%s'): user '%s' executing action for user '%s'", $commandCode, Request::GetAuthUser(), Request::GetGETUser())); + + // initialize non-wsdl soap server + $this->server = new SoapServer(null, array('uri' => "http://z-push.sf.net/webservice")); + + // the webservice command is handled by its class + if ($commandCode == ZPush::COMMAND_WEBSERVICE_DEVICE) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Webservice::HandleWebservice('%s'): executing WebserviceDevice service", $commandCode)); + + include_once('webservicedevice.php'); + $this->server->setClass("WebserviceDevice"); + } + + // the webservice command is handled by its class + if ($commandCode == ZPush::COMMAND_WEBSERVICE_USERS) { + if (!defined("ALLOW_WEBSERVICE_USERS_ACCESS") || ALLOW_WEBSERVICE_USERS_ACCESS !== true) + throw new HTTPReturnCodeException(sprintf("Access to the WebserviceUsers service is disabled in configuration. Enable setting ALLOW_WEBSERVICE_USERS_ACCESS.", Request::GetAuthUser()), 403); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Webservice::HandleWebservice('%s'): executing WebserviceUsers service", $commandCode)); + + if(ZPush::GetBackend()->Setup("SYSTEM", true) == false) + throw new AuthenticationRequiredException(sprintf("User '%s' has no admin privileges", Request::GetAuthUser())); + + include_once('webserviceusers.php'); + $this->server->setClass("WebserviceUsers"); + } + + $this->server->handle(); + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("Webservice::HandleWebservice('%s'): sucessfully sent %d bytes", $commandCode, ob_get_length())); + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/webservice/webservicedevice.php b/sources/lib/webservice/webservicedevice.php new file mode 100644 index 0000000..6c4566d --- /dev/null +++ b/sources/lib/webservice/webservicedevice.php @@ -0,0 +1,135 @@ +. +* +* Consult LICENSE file for details +************************************************/ +include ('lib/utils/zpushadmin.php'); + +class WebserviceDevice { + + /** + * Returns a list of all known devices of the Request::GetGETUser() + * + * @access public + * @return array + */ + public function ListDevicesDetails() { + $user = Request::GetGETUser(); + $devices = ZPushAdmin::ListDevices($user); + $output = array(); + + ZLog::Write(LOGLEVEL_INFO, sprintf("WebserviceDevice::ListDevicesDetails(): found %d devices of user '%s'", count($devices), $user)); + ZPush::GetTopCollector()->AnnounceInformation(sprintf("Retrieved details of %d devices", count($devices)), true); + + foreach ($devices as $devid) + $output[] = ZPushAdmin::GetDeviceDetails($devid, $user); + + return $output; + } + + /** + * Remove all state data for a device of the Request::GetGETUser() + * + * @param string $deviceId the device id + * + * @access public + * @return boolean + * @throws SoapFault + */ + public function RemoveDevice($deviceId) { + $deviceId = preg_replace("/[^A-Za-z0-9]/", "", $deviceId); + ZLog::Write(LOGLEVEL_INFO, sprintf("WebserviceDevice::RemoveDevice('%s'): remove device state data of user '%s'", $deviceId, Request::GetGETUser())); + + if (! ZPushAdmin::RemoveDevice(Request::GetGETUser(), $deviceId)) { + ZPush::GetTopCollector()->AnnounceInformation(ZLog::GetLastMessage(LOGLEVEL_ERROR), true); + throw new SoapFault("ERROR", ZLog::GetLastMessage(LOGLEVEL_ERROR)); + } + + ZPush::GetTopCollector()->AnnounceInformation(sprintf("Removed device id '%s'", $deviceId), true); + return true; + } + + /** + * Marks a device of the Request::GetGETUser() to be remotely wiped + * + * @param string $deviceId the device id + * + * @access public + * @return boolean + * @throws SoapFault + */ + public function WipeDevice($deviceId) { + $deviceId = preg_replace("/[^A-Za-z0-9]/", "", $deviceId); + ZLog::Write(LOGLEVEL_INFO, sprintf("WebserviceDevice::WipeDevice('%s'): mark device of user '%s' for remote wipe", $deviceId, Request::GetGETUser())); + + if (! ZPushAdmin::WipeDevice(Request::GetAuthUser(), Request::GetGETUser(), $deviceId)) { + ZPush::GetTopCollector()->AnnounceInformation(ZLog::GetLastMessage(LOGLEVEL_ERROR), true); + throw new SoapFault("ERROR", ZLog::GetLastMessage(LOGLEVEL_ERROR)); + } + + ZPush::GetTopCollector()->AnnounceInformation(sprintf("Wipe requested - device id '%s'", $deviceId), true); + return true; + } + + /** + * Marks a a device of the Request::GetGETUser() for resynchronization + * + * @param string $deviceId the device id + * + * @access public + * @return boolean + * @throws SoapFault + */ + public function ResyncDevice($deviceId) { + $deviceId = preg_replace("/[^A-Za-z0-9]/", "", $deviceId); + ZLog::Write(LOGLEVEL_INFO, sprintf("WebserviceDevice::ResyncDevice('%s'): mark device of user '%s' for resynchronization", $deviceId, Request::GetGETUser())); + + if (! ZPushAdmin::ResyncDevice(Request::GetGETUser(), $deviceId)) { + ZPush::GetTopCollector()->AnnounceInformation(ZLog::GetLastMessage(LOGLEVEL_ERROR), true); + throw new SoapFault("ERROR", ZLog::GetLastMessage(LOGLEVEL_ERROR)); + } + + ZPush::GetTopCollector()->AnnounceInformation(sprintf("Resync requested - device id '%s'", $deviceId), true); + return true; + } +} +?> \ No newline at end of file diff --git a/sources/lib/webservice/webserviceusers.php b/sources/lib/webservice/webserviceusers.php new file mode 100644 index 0000000..1ce0eb6 --- /dev/null +++ b/sources/lib/webservice/webserviceusers.php @@ -0,0 +1,102 @@ +. +* +* Consult LICENSE file for details +************************************************/ +include ('lib/utils/zpushadmin.php'); + +class WebserviceUsers { + + /** + * Returns a list of all known devices + * + * @access public + * @return array + */ + public function ListDevices() { + return ZPushAdmin::ListDevices(false); + } + + /** + * Returns a list of all known devices of the users + * + * @access public + * @return array + */ + public function ListDevicesAndUsers() { + $devices = ZPushAdmin::ListDevices(false); + $output = array(); + + ZLog::Write(LOGLEVEL_INFO, sprintf("WebserviceUsers::ListDevicesAndUsers(): found %d devices", count($devices))); + ZPush::GetTopCollector()->AnnounceInformation(sprintf("Retrieved details of %d devices and getting users", count($devices)), true); + + foreach ($devices as $devid) + $output[$devid] = ZPushAdmin::ListUsers($devid); + + return $output; + } + + /** + * Returns a list of all known devices with users and when they synchronized for the first time + * + * @access public + * @return array + */ + public function ListDevicesDetails() { + $devices = ZPushAdmin::ListDevices(false); + $output = array(); + + ZLog::Write(LOGLEVEL_INFO, sprintf("WebserviceUsers::ListLastSync(): found %d devices", count($devices))); + ZPush::GetTopCollector()->AnnounceInformation(sprintf("Retrieved details of %d devices and getting users", count($devices)), true); + + foreach ($devices as $deviceId) { + $output[$deviceId] = array(); + $users = ZPushAdmin::ListUsers($deviceId); + foreach ($users as $user) { + $output[$deviceId][$user] = ZPushAdmin::GetDeviceDetails($deviceId, $user); + } + } + + + return $output; + } +} +?> \ No newline at end of file diff --git a/sources/tools/migrate-2.0.x-2.1.0.php b/sources/tools/migrate-2.0.x-2.1.0.php new file mode 100755 index 0000000..22045cc --- /dev/null +++ b/sources/tools/migrate-2.0.x-2.1.0.php @@ -0,0 +1,217 @@ +#!/usr/bin/php +. +* +* Consult LICENSE file for details +************************************************/ + +// Please adjust to match your z-push installation directory, usually /usr/share/z-push +define('ZPUSH_BASE_PATH', "../"); + + + +/************************************************ + * MAIN +*/ +try { + if (!isset($_SERVER["TERM"]) || !isset($_SERVER["LOGNAME"])) + die("This script should not be called in a browser."); + + if (!defined('ZPUSH_BASE_PATH') || !file_exists(ZPUSH_BASE_PATH . "/config.php")) + die("ZPUSH_BASE_PATH not set correctly or no config.php file found\n"); + + define('BASE_PATH_CLI', ZPUSH_BASE_PATH ."/"); + set_include_path(get_include_path() . PATH_SEPARATOR . ZPUSH_BASE_PATH); + + include('lib/core/zpushdefs.php'); + include('lib/core/zpush.php'); + include('lib/core/zlog.php'); + include('lib/core/statemanager.php'); + include('lib/core/stateobject.php'); + include('lib/core/asdevice.php'); + include('lib/core/interprocessdata.php'); + include('lib/exceptions/exceptions.php'); + include('lib/utils/utils.php'); + include('lib/request/request.php'); + include('lib/request/requestprocessor.php'); + include('lib/interface/ibackend.php'); + include('lib/interface/ichanges.php'); + include('lib/interface/iexportchanges.php'); + include('lib/interface/iimportchanges.php'); + include('lib/interface/isearchprovider.php'); + include('lib/interface/istatemachine.php'); + include('config.php'); + + ZPush::CheckConfig(); + $migrate = new StateMigrator20xto210(); + + if (!$migrate->MigrationNecessary()) + echo "Migration script was run before and eventually no migration is necessary. Rerunning checks\n"; + + $migrate->DoMigration(); +} +catch (ZPushException $zpe) { + die(get_class($zpe) . ": ". $zpe->getMessage() . "\n"); +} + +echo "terminated\n"; + + +class StateMigrator20xto210 { + const FROMVERSION = "1"; // IStateMachine::STATEVERSION_01 + const TOVERSION = "2"; // IStateMachine::STATEVERSION_02 + + private $sm; + + /** + * Constructor + */ + public function StateMigrator20xto210() { + $this->sm = false; + } + + /** + * Checks if the migration is necessary + * + * @access public + * @throws FatalMisconfigurationException + * @throws FatalNotImplementedException + * @return boolean + */ + public function MigrationNecessary() { + try { + $this->sm = ZPush::GetStateMachine(); + } + catch (HTTPReturnCodeException $e) { + echo "Check states: states versions do not match and need to be migrated\n\n"; + + // we just try to get the statemachine again + // the exception is only thrown the first time + $this->sm = ZPush::GetStateMachine(); + } + + if (!$this->sm) + throw new FatalMisconfigurationException("Could not get StateMachine from ZPush::GetStateMachine()"); + + if (!($this->sm instanceof FileStateMachine)) { + throw new FatalNotImplementedException("This conversion script is only able to convert states of the FileStateMachine"); + } + + if ($this->sm->GetStateVersion() == ZPush::GetLatestStateVersion()) + return false; + + if ($this->sm->GetStateVersion() !== self::FROMVERSION || ZPush::GetLatestStateVersion() !== self::TOVERSION) + throw new FatalNotImplementedException(sprintf("This script only converts from state version %d to %d. Currently the system is on %d and should go to %d. Please contact support.", self::FROMVERSION, self::TOVERSION, $this->sm->GetStateVersion(), ZPush::GetLatestStateVersion())); + + // do migration + return true; + } + + /** + * Execute the migration + * + * @access public + * @return true + */ + public function DoMigration() { + // go through all files + $files = glob(STATE_DIR. "/*/*/*", GLOB_NOSORT); + $filetotal = count($files); + $filecount = 0; + $rencount = 0; + $igncount = 0; + + foreach ($files as $file) { + $filecount++; + $newfile = strtolower($file); + echo "\033[1G"; + + if ($file !== $newfile) { + $rencount++; + rename ($file, $newfile); + } + else + $igncount++; + + printf("Migrating file %d/%d\t%s", $filecount, $filetotal, $file); + } + echo "\033[1G". sprintf("Migrated total of %d files, %d renamed and %d ignored (as already correct)%s\n\n", $filetotal, $rencount, $igncount, str_repeat(" ", 50)); + + // get all states of synchronized devices + $alldevices = $this->sm->GetAllDevices(false); + foreach ($alldevices as $devid) { + $lowerDevid = strtolower($devid); + + echo "Processing device: ". $devid . "\t"; + + // update device data + $devState = ZPush::GetStateMachine()->GetState($lowerDevid, IStateMachine::DEVICEDATA); + $newdata = array(); + foreach ($devState->devices as $user => $dev) { + if (!isset($dev->deviceidOrg)) + $dev->deviceidOrg = $dev->deviceid; + + $dev->deviceid = strtolower($dev->deviceid); + $dev->useragenthistory = array_unique($dev->useragenthistory); + $newdata[$user] = $dev; + } + $devState->devices = $newdata; + $this->sm->SetState($devState, $lowerDevid, IStateMachine::DEVICEDATA); + + // go through the users again: device was updated sucessfully, now we change the global user <-> device link + foreach ($devState->devices as $user => $dev) { + printf("\n\tUn-linking %s with old device id %s", $user, $dev->deviceidOrg); + $this->sm->UnLinkUserDevice($user, $dev->deviceidOrg); + printf("\n\tRe-linking %s with new device id %s", $user, $dev->deviceid); + $this->sm->LinkUserDevice($user, $dev->deviceid); + } + + echo "\n\tcompleted\n"; + } + echo "\nSetting new StateVersion\n"; + $this->sm->SetStateVersion(self::TOVERSION); + echo "Migration completed!\n\n"; + + return true; + } +} + +?> diff --git a/sources/version.php b/sources/version.php new file mode 100644 index 0000000..b658850 --- /dev/null +++ b/sources/version.php @@ -0,0 +1,46 @@ +. +* +* Consult LICENSE file for details +************************************************/ + +define("ZPUSH_VERSION", "2.1.3-1892"); + +?> \ No newline at end of file diff --git a/sources/z-push-admin.php b/sources/z-push-admin.php new file mode 100755 index 0000000..fc42374 --- /dev/null +++ b/sources/z-push-admin.php @@ -0,0 +1,844 @@ +#!/usr/bin/php +. +* +* Consult LICENSE file for details +************************************************/ + +include('lib/core/zpushdefs.php'); +include('lib/core/zpush.php'); +include('lib/core/stateobject.php'); +include('lib/core/syncparameters.php'); +include('lib/core/bodypreference.php'); +include('lib/core/contentparameters.php'); +include('lib/core/synccollections.php'); +include('lib/core/zlog.php'); +include('lib/core/statemanager.php'); +include('lib/core/streamer.php'); +include('lib/core/asdevice.php'); +include('lib/core/interprocessdata.php'); +include('lib/core/loopdetection.php'); +include('lib/exceptions/exceptions.php'); +include('lib/utils/utils.php'); +include('lib/utils/zpushadmin.php'); +include('lib/request/request.php'); +include('lib/request/requestprocessor.php'); +include('lib/interface/ibackend.php'); +include('lib/interface/ichanges.php'); +include('lib/interface/iexportchanges.php'); +include('lib/interface/iimportchanges.php'); +include('lib/interface/isearchprovider.php'); +include('lib/interface/istatemachine.php'); +include('lib/syncobjects/syncobject.php'); +include('lib/syncobjects/syncbasebody.php'); +include('lib/syncobjects/syncbaseattachment.php'); +include('lib/syncobjects/syncmailflags.php'); +include('lib/syncobjects/syncrecurrence.php'); +include('lib/syncobjects/syncappointment.php'); +include('lib/syncobjects/syncappointmentexception.php'); +include('lib/syncobjects/syncattachment.php'); +include('lib/syncobjects/syncattendee.php'); +include('lib/syncobjects/syncmeetingrequestrecurrence.php'); +include('lib/syncobjects/syncmeetingrequest.php'); +include('lib/syncobjects/syncmail.php'); +include('lib/syncobjects/syncnote.php'); +include('lib/syncobjects/synccontact.php'); +include('lib/syncobjects/syncfolder.php'); +include('lib/syncobjects/syncprovisioning.php'); +include('lib/syncobjects/synctaskrecurrence.php'); +include('lib/syncobjects/synctask.php'); +include('lib/syncobjects/syncoofmessage.php'); +include('lib/syncobjects/syncoof.php'); +include('lib/syncobjects/syncuserinformation.php'); +include('lib/syncobjects/syncdeviceinformation.php'); +include('lib/syncobjects/syncdevicepassword.php'); +include('lib/syncobjects/syncitemoperationsattachment.php'); +include('config.php'); +include('version.php'); + +/** + * //TODO resync of single folders of a users device + */ + +/************************************************ + * MAIN + */ + define('BASE_PATH_CLI', dirname(__FILE__) ."/"); + set_include_path(get_include_path() . PATH_SEPARATOR . BASE_PATH_CLI); + try { + ZPush::CheckConfig(); + ZPushAdminCLI::CheckEnv(); + ZPushAdminCLI::CheckOptions(); + + if (! ZPushAdminCLI::SureWhatToDo()) { + // show error message if available + if (ZPushAdminCLI::GetErrorMessage()) + echo "ERROR: ". ZPushAdminCLI::GetErrorMessage() . "\n"; + + echo ZPushAdminCLI::UsageInstructions(); + exit(1); + } + + ZPushAdminCLI::RunCommand(); + } + catch (ZPushException $zpe) { + die(get_class($zpe) . ": ". $zpe->getMessage() . "\n"); + } + + +/************************************************ + * Z-Push-Admin CLI + */ +class ZPushAdminCLI { + const COMMAND_SHOWALLDEVICES = 1; + const COMMAND_SHOWDEVICESOFUSER = 2; + const COMMAND_SHOWUSERSOFDEVICE = 3; + const COMMAND_WIPEDEVICE = 4; + const COMMAND_REMOVEDEVICE = 5; + const COMMAND_RESYNCDEVICE = 6; + const COMMAND_CLEARLOOP = 7; + const COMMAND_SHOWLASTSYNC = 8; + const COMMAND_RESYNCFOLDER = 9; + const COMMAND_FIXSTATES = 10; + + static private $command; + static private $user = false; + static private $device = false; + static private $type = false; + static private $errormessage; + + /** + * Returns usage instructions + * + * @return string + * @access public + */ + static public function UsageInstructions() { + return "Usage:\n\tz-push-admin.php -a ACTION [options]\n\n" . + "Parameters:\n\t-a list/wipe/remove/resync/clearloop\n\t[-u] username\n\t[-d] deviceid\n\n" . + "Actions:\n" . + "\tlist\t\t\t\t Lists all devices and synchronized users\n" . + "\tlist -u USER\t\t\t Lists all devices of user USER\n" . + "\tlist -d DEVICE\t\t\t Lists all users of device DEVICE\n" . + "\tlastsync\t\t\t Lists all devices and synchronized users and the last synchronization time\n" . + "\twipe -u USER\t\t\t Remote wipes all devices of user USER\n" . + "\twipe -d DEVICE\t\t\t Remote wipes device DEVICE\n" . + "\twipe -u USER -d DEVICE\t\t Remote wipes device DEVICE of user USER\n" . + "\tremove -u USER\t\t\t Removes all state data of all devices of user USER\n" . + "\tremove -d DEVICE\t\t Removes all state data of all users synchronized on device DEVICE\n" . + "\tremove -u USER -d DEVICE\t Removes all related state data of device DEVICE of user USER\n" . + "\tresync -u USER -d DEVICE\t Resynchronizes all data of device DEVICE of user USER\n" . + "\tresync -t TYPE \t\t\t Resynchronizes all folders of type 'email', 'calendar', 'contact', 'task' or 'note' for all devices and users.\n" . + "\tresync -t TYPE -u USER \t\t Resynchronizes all folders of type 'email', 'calendar', 'contact', 'task' or 'note' for the user USER.\n" . + "\tresync -t TYPE -u USER -d DEVICE Resynchronizes all folders of type 'email', 'calendar', 'contact', 'task' or 'note' for a specified device and user.\n" . + "\tresync -t FOLDERID -u USER\t Resynchronize the specified folder id only. The USER should be specified.\n" . + "\tclearloop\t\t\t Clears system wide loop detection data\n" . + "\tclearloop -d DEVICE -u USER\t Clears all loop detection data of a device DEVICE and an optional user USER\n" . + "\tfixstates\t\t\t Checks the states for integrity and fixes potential issues\n" . + "\n"; + } + + /** + * Checks the environment + * + * @return + * @access public + */ + static public function CheckEnv() { + if (!isset($_SERVER["TERM"]) || !isset($_SERVER["LOGNAME"])) + self::$errormessage = "This script should not be called in a browser."; + + if (!function_exists("getopt")) + self::$errormessage = "PHP Function getopt not found. Please check your PHP version and settings."; + } + + /** + * Checks the options from the command line + * + * @return + * @access public + */ + static public function CheckOptions() { + if (self::$errormessage) + return; + + $options = getopt("u:d:a:t:"); + + // get 'user' + if (isset($options['u']) && !empty($options['u'])) + self::$user = strtolower(trim($options['u'])); + else if (isset($options['user']) && !empty($options['user'])) + self::$user = strtolower(trim($options['user'])); + + // get 'device' + if (isset($options['d']) && !empty($options['d'])) + self::$device = strtolower(trim($options['d'])); + else if (isset($options['device']) && !empty($options['device'])) + self::$device = strtolower(trim($options['device'])); + + // get 'action' + $action = false; + if (isset($options['a']) && !empty($options['a'])) + $action = strtolower(trim($options['a'])); + elseif (isset($options['action']) && !empty($options['action'])) + $action = strtolower(trim($options['action'])); + + // get 'type' + if (isset($options['t']) && !empty($options['t'])) + self::$type = strtolower(trim($options['t'])); + elseif (isset($options['type']) && !empty($options['type'])) + self::$type = strtolower(trim($options['type'])); + + // get a command for the requested action + switch ($action) { + // list data + case "list": + if (self::$user === false && self::$device === false) + self::$command = self::COMMAND_SHOWALLDEVICES; + + if (self::$user !== false) + self::$command = self::COMMAND_SHOWDEVICESOFUSER; + + if (self::$device !== false) + self::$command = self::COMMAND_SHOWUSERSOFDEVICE; + break; + + // list data + case "lastsync": + self::$command = self::COMMAND_SHOWLASTSYNC; + break; + + // remove wipe device + case "wipe": + if (self::$user === false && self::$device === false) + self::$errormessage = "Not possible to execute remote wipe. Device, user or both must be specified."; + else + self::$command = self::COMMAND_WIPEDEVICE; + break; + + // remove device data of user + case "remove": + if (self::$user === false && self::$device === false) + self::$errormessage = "Not possible to remove data. Device, user or both must be specified."; + else + self::$command = self::COMMAND_REMOVEDEVICE; + break; + + // resync a device + case "resync": + case "re-sync": + case "sync": + case "resynchronize": + case "re-synchronize": + case "synchronize": + // full resync + if (self::$type === false) { + if (self::$user === false || self::$device === false) + self::$errormessage = "Not possible to resynchronize device. Device and user must be specified."; + else + self::$command = self::COMMAND_RESYNCDEVICE; + } + else { + self::$command = self::COMMAND_RESYNCFOLDER; + } + break; + + // clear loop detection data + case "clearloop": + case "clearloopdetection": + self::$command = self::COMMAND_CLEARLOOP; + break; + + // clear loop detection data + case "fixstates": + case "fix": + self::$command = self::COMMAND_FIXSTATES; + break; + + + default: + self::UsageInstructions(); + } + } + + /** + * Indicates if the options from the command line + * could be processed correctly + * + * @return boolean + * @access public + */ + static public function SureWhatToDo() { + return isset(self::$command); + } + + /** + * Returns a errormessage of things which could have gone wrong + * + * @return string + * @access public + */ + static public function GetErrorMessage() { + return (isset(self::$errormessage))?self::$errormessage:""; + } + + /** + * Runs a command requested from an action of the command line + * + * @return + * @access public + */ + static public function RunCommand() { + echo "\n"; + switch(self::$command) { + case self::COMMAND_SHOWALLDEVICES: + self::CommandShowDevices(); + break; + + case self::COMMAND_SHOWDEVICESOFUSER: + self::CommandShowDevices(); + break; + + case self::COMMAND_SHOWUSERSOFDEVICE: + self::CommandDeviceUsers(); + break; + + case self::COMMAND_SHOWLASTSYNC: + self::CommandShowLastSync(); + break; + + case self::COMMAND_WIPEDEVICE: + if (self::$device) + echo sprintf("Are you sure you want to REMOTE WIPE device '%s' [y/N]: ", self::$device); + else + echo sprintf("Are you sure you want to REMOTE WIPE all devices of user '%s' [y/N]: ", self::$user); + + $confirm = strtolower(trim(fgets(STDIN))); + if ( $confirm === 'y' || $confirm === 'yes') + self::CommandWipeDevice(); + else + echo "Aborted!\n"; + break; + + case self::COMMAND_REMOVEDEVICE: + self::CommandRemoveDevice(); + break; + + case self::COMMAND_RESYNCDEVICE: + if (self::$device == false) { + echo sprintf("Are you sure you want to re-synchronize all devices of user '%s' [y/N]: ", self::$user); + $confirm = strtolower(trim(fgets(STDIN))); + if ( !($confirm === 'y' || $confirm === 'yes')) { + echo "Aborted!\n"; + exit(1); + } + } + self::CommandResyncDevices(); + break; + + case self::COMMAND_RESYNCFOLDER: + if (self::$device == false && self::$user == false) { + echo "Are you sure you want to re-synchronize this folder type of all devices and users [y/N]: "; + $confirm = strtolower(trim(fgets(STDIN))); + if ( !($confirm === 'y' || $confirm === 'yes')) { + echo "Aborted!\n"; + exit(1); + } + } + self::CommandResyncFolder(); + break; + + case self::COMMAND_CLEARLOOP: + self::CommandClearLoopDetectionData(); + break; + + case self::COMMAND_FIXSTATES: + self::CommandFixStates(); + break; + + } + echo "\n"; + } + + /** + * Command "Show all devices" and "Show devices of user" + * Prints the device id of/and connected users + * + * @return + * @access public + */ + static public function CommandShowDevices() { + $devicelist = ZPushAdmin::ListDevices(self::$user); + if (empty($devicelist)) + echo "\tno devices found\n"; + else { + if (self::$user === false) { + echo "All synchronized devices\n\n"; + echo str_pad("Device id", 36). "Synchronized users\n"; + echo "-----------------------------------------------------\n"; + } + else + echo "Synchronized devices of user: ". self::$user. "\n"; + } + + foreach ($devicelist as $deviceId) { + if (self::$user === false) { + echo str_pad($deviceId, 36) . implode (",", ZPushAdmin::ListUsers($deviceId)) ."\n"; + } + else + self::printDeviceData($deviceId, self::$user); + } + } + + /** + * Command "Show all devices and users with last sync time" + * Prints the device id of/and connected users + * + * @return + * @access public + */ + static public function CommandShowLastSync() { + $devicelist = ZPushAdmin::ListDevices(false); + if (empty($devicelist)) + echo "\tno devices found\n"; + else { + echo "All known devices and users and their last synchronization time\n\n"; + echo str_pad("Device id", 36). str_pad("Synchronized user", 30)."Last sync time\n"; + echo "-----------------------------------------------------------------------------------------------------\n"; + } + + foreach ($devicelist as $deviceId) { + $users = ZPushAdmin::ListUsers($deviceId); + foreach ($users as $user) { + $device = ZPushAdmin::GetDeviceDetails($deviceId, $user); + echo str_pad($deviceId, 36) . str_pad($user, 30). ($device->GetLastSyncTime() ? strftime("%Y-%m-%d %H:%M", $device->GetLastSyncTime()) : "never") . "\n"; + } + } + } + + /** + * Command "Show users of device" + * Prints informations about all users which use a device + * + * @return + * @access public + */ + static public function CommandDeviceUsers() { + $users = ZPushAdmin::ListUsers(self::$device); + + if (empty($users)) + echo "\tno user data synchronized to device\n"; + + foreach ($users as $user) { + echo "Synchronized by user: ". $user. "\n"; + self::printDeviceData(self::$device, $user); + } + } + + /** + * Command "Wipe device" + * Marks a device of that user to be remotely wiped + * + * @return + * @access public + */ + static public function CommandWipeDevice() { + $stat = ZPushAdmin::WipeDevice($_SERVER["LOGNAME"], self::$user, self::$device); + + if (self::$user !== false && self::$device !== false) { + echo sprintf("Mark device '%s' of user '%s' to be wiped: %s", self::$device, self::$user, ($stat)?'OK':ZLog::GetLastMessage(LOGLEVEL_ERROR)). "\n"; + + if ($stat) { + echo "Updated information about this device:\n"; + self::printDeviceData(self::$device, self::$user); + } + } + elseif (self::$user !== false) { + echo sprintf("Mark devices of user '%s' to be wiped: %s", self::$user, ($stat)?'OK':ZLog::GetLastMessage(LOGLEVEL_ERROR)). "\n"; + self::CommandShowDevices(); + } + } + + /** + * Command "Remove device" + * Remove a device of that user from the device list + * + * @return + * @access public + */ + static public function CommandRemoveDevice() { + $stat = ZPushAdmin::RemoveDevice(self::$user, self::$device); + if (self::$user === false) + echo sprintf("State data of device '%s' removed: %s", self::$device, ($stat)?'OK':ZLog::GetLastMessage(LOGLEVEL_ERROR)). "\n"; + elseif (self::$device === false) + echo sprintf("State data of all devices of user '%s' removed: %s", self::$user, ($stat)?'OK':ZLog::GetLastMessage(LOGLEVEL_ERROR)). "\n"; + else + echo sprintf("State data of device '%s' of user '%s' removed: %s", self::$device, self::$user, ($stat)?'OK':ZLog::GetLastMessage(LOGLEVEL_ERROR)). "\n"; + } + + /** + * Command "Resync device(s)" + * Resyncs one or all devices of that user + * + * @return + * @access public + */ + static public function CommandResyncDevices() { + $stat = ZPushAdmin::ResyncDevice(self::$user, self::$device); + echo sprintf("Resync of device '%s' of user '%s': %s", self::$device, self::$user, ($stat)?'Requested':ZLog::GetLastMessage(LOGLEVEL_ERROR)). "\n"; + } + + /** + * Command "Resync folder(s)" + * Resyncs a folder type of a specific device/user or of all users + * + * @return + * @access public + */ + static public function CommandResyncFolder() { + // if no device is specified, search for all devices of a user. If user is not set, all devices are returned. + if (self::$device === false) { + $devicelist = ZPushAdmin::ListDevices(self::$user); + if (empty($devicelist)) { + echo "\tno devices/users found\n"; + return true; + } + } + else + $devicelist = array(self::$device); + + foreach ($devicelist as $deviceId) { + $users = ZPushAdmin::ListUsers($deviceId); + foreach ($users as $user) { + if (self::$user && self::$user != $user) + continue; + self::resyncFolder($deviceId, $user, self::$type); + } + } + + } + + /** + * Command to clear the loop detection data + * Mobiles may enter loop detection (one-by-one synchring due to timeouts / erros). + * + * @return + * @access public + */ + static public function CommandClearLoopDetectionData() { + $stat = false; + $stat = ZPushAdmin::ClearLoopDetectionData(self::$user, self::$device); + if (self::$user === false && self::$device === false) + echo sprintf("System wide loop detection data removed: %s", ($stat)?'OK':ZLog::GetLastMessage(LOGLEVEL_ERROR)). "\n"; + elseif (self::$user === false) + echo sprintf("Loop detection data of device '%s' removed: %s", self::$device, ($stat)?'OK':ZLog::GetLastMessage(LOGLEVEL_ERROR)). "\n"; + elseif (self::$device === false && self::$user !== false) + echo sprintf("Error: %s", ($stat)?'OK':ZLog::GetLastMessage(LOGLEVEL_WARN)). "\n"; + else + echo sprintf("Loop detection data of device '%s' of user '%s' removed: %s", self::$device, self::$user, ($stat)?'OK':ZLog::GetLastMessage(LOGLEVEL_ERROR)). "\n"; + } + + /** + * Resynchronizes a folder type of a device & user + * + * @param string $deviceId the id of the device + * @param string $user the user + * @param string $type the folder type + * + * @return + * @access private + */ + static private function resyncFolder($deviceId, $user, $type) { + $device = ZPushAdmin::GetDeviceDetails($deviceId, $user); + + if (! $device instanceof ASDevice) { + echo sprintf("Folder resync failed: %s\n", ZLog::GetLastMessage(LOGLEVEL_ERROR)); + return false; + } + + $folders = array(); + foreach ($device->GetAllFolderIds() as $folderid) { + // if submitting a folderid as type to resync a specific folder. + if ($folderid == $type) { + printf("Found and resynching requested folderid '%s' on device '%s' of user '%s'\n", $folderid, $deviceId, $user); + $folders[] = $folderid; + break; + } + + if ($device->GetFolderUUID($folderid)) { + $foldertype = $device->GetFolderType($folderid); + switch($foldertype) { + case SYNC_FOLDER_TYPE_APPOINTMENT: + case SYNC_FOLDER_TYPE_USER_APPOINTMENT: + if ($type == "calendar") + $folders[] = $folderid; + break; + case SYNC_FOLDER_TYPE_CONTACT: + case SYNC_FOLDER_TYPE_USER_CONTACT: + if ($type == "contact") + $folders[] = $folderid; + break; + case SYNC_FOLDER_TYPE_TASK: + case SYNC_FOLDER_TYPE_USER_TASK: + if ($type == "task") + $folders[] = $folderid; + break; + case SYNC_FOLDER_TYPE_NOTE: + case SYNC_FOLDER_TYPE_USER_NOTE: + if ($type == "note") + $folders[] = $folderid; + break; + default: + if ($type == "email") + $folders[] = $folderid; + break; + } + } + } + + $stat = ZPushAdmin::ResyncFolder($user, $deviceId, $folders); + echo sprintf("Resync of %d folders of type %s on device '%s' of user '%s': %s\n", count($folders), $type, $deviceId, $user, ($stat)?'Requested':ZLog::GetLastMessage(LOGLEVEL_ERROR)); + } + + /** + * Fixes the states for potential issues + * + * @return + * @access private + */ + static private function CommandFixStates() { + echo "Validating and fixing states (this can take some time):\n"; + + echo "\tChecking username casings: "; + if ($stat = ZPushAdmin::FixStatesDifferentUsernameCases()) + printf("Processed: %d - Converted: %d - Removed: %d\n", $stat[0], $stat[1], $stat[2]); + else + echo ZLog::GetLastMessage(LOGLEVEL_ERROR) . "\n"; + + // fixes ZP-339 + echo "\tChecking available devicedata & user linking: "; + if ($stat = ZPushAdmin::FixStatesDeviceToUserLinking()) + printf("Processed: %d - Fixed: %d\n", $stat[0], $stat[1]); + else + echo ZLog::GetLastMessage(LOGLEVEL_ERROR) . "\n"; + + echo "\tChecking for unreferenced (obsolete) state files: "; + if (($stat = ZPushAdmin::FixStatesUserToStatesLinking()) !== false) + printf("Processed: %d - Deleted: %d\n", $stat[0], $stat[1]); + else + echo ZLog::GetLastMessage(LOGLEVEL_ERROR) . "\n"; + } + + /** + * Prints detailed informations about a device + * + * @param string $deviceId the id of the device + * @param string $user the user + * + * @return + * @access private + */ + static private function printDeviceData($deviceId, $user) { + $device = ZPushAdmin::GetDeviceDetails($deviceId, $user); + + if (! $device instanceof ASDevice) { + echo sprintf("Folder resync failed: %s\n", ZLog::GetLastMessage(LOGLEVEL_ERROR)); + return false; + } + + // Gather some statistics about synchronized folders + $folders = $device->GetAllFolderIds(); + $synchedFolders = 0; + $synchedFolderTypes = array(); + $syncedFoldersInProgress = 0; + foreach ($folders as $folderid) { + if ($device->GetFolderUUID($folderid)) { + $synchedFolders++; + $type = $device->GetFolderType($folderid); + switch($type) { + case SYNC_FOLDER_TYPE_APPOINTMENT: + case SYNC_FOLDER_TYPE_USER_APPOINTMENT: + $gentype = "Calendars"; + break; + case SYNC_FOLDER_TYPE_CONTACT: + case SYNC_FOLDER_TYPE_USER_CONTACT: + $gentype = "Contacts"; + break; + case SYNC_FOLDER_TYPE_TASK: + case SYNC_FOLDER_TYPE_USER_TASK: + $gentype = "Tasks"; + break; + case SYNC_FOLDER_TYPE_NOTE: + case SYNC_FOLDER_TYPE_USER_NOTE: + $gentype = "Notes"; + break; + default: + $gentype = "Emails"; + break; + } + if (!isset($synchedFolderTypes[$gentype])) + $synchedFolderTypes[$gentype] = 0; + $synchedFolderTypes[$gentype]++; + + // set the folder name for all folders which are not fully synchronized yet + $fstatus = $device->GetFolderSyncStatus($folderid); + if ($fstatus !== false && is_array($fstatus)) { + // TODO would be nice if we could see the real name of the folder, right now we use the folder type as name + $fstatus['name'] = $gentype; + $device->SetFolderSyncStatus($folderid, $fstatus); + $syncedFoldersInProgress++; + } + } + } + $folderinfo = ""; + foreach ($synchedFolderTypes as $gentype=>$count) { + $folderinfo .= $gentype; + if ($count>1) $folderinfo .= "($count)"; + $folderinfo .= " "; + } + if (!$folderinfo) $folderinfo = "None available"; + + echo "-----------------------------------------------------\n"; + echo "DeviceId:\t\t$deviceId\n"; + echo "Device type:\t\t". ($device->GetDeviceType() !== ASDevice::UNDEFINED ? $device->GetDeviceType() : "unknown") ."\n"; + echo "UserAgent:\t\t".($device->GetDeviceUserAgent()!== ASDevice::UNDEFINED ? $device->GetDeviceUserAgent() : "unknown") ."\n"; + // TODO implement $device->GetDeviceUserAgentHistory() + + // device information transmitted during Settings command + if ($device->GetDeviceModel()) + echo "Device Model:\t\t". $device->GetDeviceModel(). "\n"; + if ($device->GetDeviceIMEI()) + echo "Device IMEI:\t\t". $device->GetDeviceIMEI(). "\n"; + if ($device->GetDeviceFriendlyName()) + echo "Device friendly name:\t". $device->GetDeviceFriendlyName(). "\n"; + if ($device->GetDeviceOS()) + echo "Device OS:\t\t". $device->GetDeviceOS(). "\n"; + if ($device->GetDeviceOSLanguage()) + echo "Device OS Language:\t". $device->GetDeviceOSLanguage(). "\n"; + if ($device->GetDevicePhoneNumber()) + echo "Device Phone nr:\t". $device->GetDevicePhoneNumber(). "\n"; + if ($device->GetDeviceMobileOperator()) + echo "Device Operator:\t". $device->GetDeviceMobileOperator(). "\n"; + if ($device->GetDeviceEnableOutboundSMS()) + echo "Device Outbound SMS:\t". $device->GetDeviceEnableOutboundSMS(). "\n"; + + echo "ActiveSync version:\t".($device->GetASVersion() ? $device->GetASVersion() : "unknown") ."\n"; + echo "First sync:\t\t". strftime("%Y-%m-%d %H:%M", $device->GetFirstSyncTime()) ."\n"; + echo "Last sync:\t\t". ($device->GetLastSyncTime() ? strftime("%Y-%m-%d %H:%M", $device->GetLastSyncTime()) : "never")."\n"; + echo "Total folders:\t\t". count($folders). "\n"; + echo "Synchronized folders:\t". $synchedFolders; + if ($syncedFoldersInProgress > 0) + echo " (". $syncedFoldersInProgress. " in progress)"; + echo "\n"; + echo "Synchronized data:\t$folderinfo\n"; + if ($syncedFoldersInProgress > 0) { + echo "Synchronization progress:\n"; + foreach ($folders as $folderid) { + $d = $device->GetFolderSyncStatus($folderid); + if ($d) { + $status = ""; + if ($d['total'] > 0) { + $percent = round($d['done']*100/$d['total']); + $status = sprintf("Status: %s%d%% (%d/%d)", ($percent < 10)?" ":"", $percent, $d['done'], $d['total']); + } + printf("\tFolder: %s%s Sync: %s %s\n", $d['name'], str_repeat(" ", 12-strlen($d['name'])), $d['status'], $status); + } + } + } + echo "Status:\t\t\t"; + switch ($device->GetWipeStatus()) { + case SYNC_PROVISION_RWSTATUS_OK: + echo "OK\n"; + break; + case SYNC_PROVISION_RWSTATUS_PENDING: + echo "Pending wipe\n"; + break; + case SYNC_PROVISION_RWSTATUS_REQUESTED: + echo "Wipe requested on device\n"; + break; + case SYNC_PROVISION_RWSTATUS_WIPED: + echo "Wiped\n"; + break; + default: + echo "Not available\n"; + break; + } + + echo "WipeRequest on:\t\t". ($device->GetWipeRequestedOn() ? strftime("%Y-%m-%d %H:%M", $device->GetWipeRequestedOn()) : "not set")."\n"; + echo "WipeRequest by:\t\t". ($device->GetWipeRequestedBy() ? $device->GetWipeRequestedBy() : "not set")."\n"; + echo "Wiped on:\t\t". ($device->GetWipeActionOn() ? strftime("%Y-%m-%d %H:%M", $device->GetWipeActionOn()) : "not set")."\n"; + + echo "Attention needed:\t"; + + if ($device->GetDeviceError()) + echo $device->GetDeviceError() ."\n"; + else if (!isset($device->ignoredmessages) || empty($device->ignoredmessages)) { + echo "No errors known\n"; + } + else { + printf("%d messages need attention because they could not be synchronized\n", count($device->ignoredmessages)); + foreach ($device->ignoredmessages as $im) { + $info = ""; + if (isset($im->asobject->subject)) + $info .= sprintf("Subject: '%s'", $im->asobject->subject); + if (isset($im->asobject->fileas)) + $info .= sprintf("FileAs: '%s'", $im->asobject->fileas); + if (isset($im->asobject->from)) + $info .= sprintf(" - From: '%s'", $im->asobject->from); + if (isset($im->asobject->starttime)) + $info .= sprintf(" - On: '%s'", strftime("%Y-%m-%d %H:%M", $im->asobject->starttime)); + $reason = $im->reasonstring; + if ($im->reasoncode == 2) + $reason = "Message was causing loop"; + printf("\tBroken object:\t'%s' ignored on '%s'\n", $im->asclass, strftime("%Y-%m-%d %H:%M", $im->timestamp)); + printf("\tInformation:\t%s\n", $info); + printf("\tReason: \t%s (%s)\n", $reason, $im->reasoncode); + printf("\tItem/Parent id: %s/%s\n", $im->id, $im->folderid); + echo "\n"; + } + } + + } +} + + +?> \ No newline at end of file diff --git a/sources/z-push-top.php b/sources/z-push-top.php new file mode 100755 index 0000000..b0fbf93 --- /dev/null +++ b/sources/z-push-top.php @@ -0,0 +1,769 @@ +#!/usr/bin/php +. +* +* Consult LICENSE file for details +************************************************/ + +include('lib/exceptions/exceptions.php'); +include('lib/core/zpushdefs.php'); +include('lib/core/zpush.php'); +include('lib/core/zlog.php'); +include('lib/core/interprocessdata.php'); +include('lib/core/topcollector.php'); +include('lib/utils/utils.php'); +include('lib/request/request.php'); +include('lib/request/requestprocessor.php'); +include('config.php'); +include('version.php'); + +/************************************************ + * MAIN + */ + declare(ticks = 1); + define('BASE_PATH_CLI', dirname(__FILE__) ."/"); + + try { + ZPush::CheckConfig(); + if (!function_exists("pcntl_signal")) + throw new FatalException("Function pcntl_signal() is not available. Please install package 'php5-pcntl' (or similar) on your system."); + + $zpt = new ZPushTop(); + + // check if help was requested from CLI + if (in_array('-h', $argv) || in_array('--help', $argv)) { + echo $zpt->UsageInstructions(); + exit(1); + } + + if ($zpt->IsAvailable()) { + pcntl_signal(SIGINT, array($zpt, "SignalHandler")); + $zpt->run(); + $zpt->scrClear(); + system("stty sane"); + } + else + echo "Z-Push shared memory interprocess communication is not available.\n"; + } + catch (ZPushException $zpe) { + die(get_class($zpe) . ": ". $zpe->getMessage() . "\n"); + } + + echo "terminated\n"; + + +/************************************************ + * Z-Push-Top + */ +class ZPushTop { + // show options + const SHOW_DEFAULT = 0; + const SHOW_ACTIVE_ONLY = 1; + const SHOW_UNKNOWN_ONLY = 2; + const SHOW_TERM_DEFAULT_TIME = 5; // 5 secs + + private $topCollector; + private $starttime; + private $action; + private $filter; + private $status; + private $statusexpire; + private $wide; + private $wasEnabled; + private $terminate; + private $scrSize; + private $pingInterval; + private $showPush; + private $showTermSec; + + private $linesActive = array(); + private $linesOpen = array(); + private $linesUnknown = array(); + private $linesTerm = array(); + private $pushConn = 0; + private $activeConn = array(); + private $activeHosts = array(); + private $activeUsers = array(); + private $activeDevices = array(); + + /** + * Constructor + * + * @access public + */ + public function ZPushTop() { + $this->starttime = time(); + $this->currenttime = time(); + $this->action = ""; + $this->filter = false; + $this->status = false; + $this->statusexpire = 0; + $this->helpexpire = 0; + $this->doingTail = false; + $this->wide = false; + $this->terminate = false; + $this->showPush = true; + $this->showOption = self::SHOW_DEFAULT; + $this->showTermSec = self::SHOW_TERM_DEFAULT_TIME; + $this->scrSize = array('width' => 80, 'height' => 24); + $this->pingInterval = (defined('PING_INTERVAL') && PING_INTERVAL > 0) ? PING_INTERVAL : 12; + + // get a TopCollector + $this->topCollector = new TopCollector(); + } + + /** + * Requests data from the running Z-Push processes + * + * @access private + * @return + */ + private function initialize() { + // request feedback from active processes + $this->wasEnabled = $this->topCollector->CollectData(); + + // remove obsolete data + $this->topCollector->ClearLatest(true); + + // start with default colours + $this->scrDefaultColors(); + } + + /** + * Main loop of Z-Push-top + * Runs until termination is requested + * + * @access public + * @return + */ + public function run() { + $this->initialize(); + + do { + $this->currenttime = time(); + + // see if shared memory is active + if (!$this->IsAvailable()) + $this->terminate = true; + + // active processes should continue sending data + $this->topCollector->CollectData(); + + // get and process data from processes + $this->topCollector->ClearLatest(); + $topdata = $this->topCollector->ReadLatest(); + $this->processData($topdata); + + // clear screen + $this->scrClear(); + + // check if screen size changed + $s = $this->scrGetSize(); + if ($this->scrSize['width'] != $s['width']) { + if ($s['width'] > 180) + $this->wide = true; + else + $this->wide = false; + } + $this->scrSize = $s; + + // print overview + $this->scrOverview(); + + // wait for user input + $this->readLineProcess(); + } + while($this->terminate != true); + } + + /** + * Indicates if TopCollector is available collecting data + * + * @access public + * @return boolean + */ + public function IsAvailable() { + return $this->topCollector->IsActive(); + } + + /** + * Processes data written by the running processes + * + * @param array $data + * + * @access private + * @return + */ + private function processData($data) { + $this->linesActive = array(); + $this->linesOpen = array(); + $this->linesUnknown = array(); + $this->linesTerm = array(); + $this->pushConn = 0; + $this->activeConn = array(); + $this->activeHosts = array(); + $this->activeUsers = array(); + $this->activeDevices = array(); + + if (!is_array($data)) + return; + + foreach ($data as $devid=>$users) { + foreach ($users as $user=>$pids) { + foreach ($pids as $pid=>$line) { + if (!is_array($line)) + continue; + + $line['command'] = Utils::GetCommandFromCode($line['command']); + + if ($line["ended"] == 0) { + $this->activeDevices[$devid] = 1; + $this->activeUsers[$user] = 1; + $this->activeConn[$pid] = 1; + $this->activeHosts[$line['ip']] = 1; + + $line["time"] = $this->currenttime - $line['start']; + if ($line['push'] === true) $this->pushConn += 1; + + // ignore push connections + if ($line['push'] === true && ! $this->showPush) + continue; + + if ($this->filter !== false) { + $f = $this->filter; + if (!($line["pid"] == $f || $line["ip"] == $f || strtolower($line['command']) == strtolower($f) || preg_match("/.*?$f.*?/i", $line['user']) || + preg_match("/.*?$f.*?/i", $line['devagent']) || preg_match("/.*?$f.*?/i", $line['devid']) || preg_match("/.*?$f.*?/i", $line['addinfo']) )) + continue; + } + + $lastUpdate = $this->currenttime - $line["update"]; + if ($this->currenttime - $line["update"] < 2) + $this->linesActive[$line["update"].$line["pid"]] = $line; + else if (($line['push'] === true && $lastUpdate > ($this->pingInterval+2)) || ($line['push'] !== true && $lastUpdate > 4)) + $this->linesUnknown[$line["update"].$line["pid"]] = $line; + else + $this->linesOpen[$line["update"].$line["pid"]] = $line; + } + else { + // do not show terminated + expired connections + if ($line['ended'] + $this->showTermSec < $this->currenttime) + continue; + + if ($this->filter !== false) { + $f = $this->filter; + if (!($line['pid'] == $f || $line['ip'] == $f || strtolower($line['command']) == strtolower($f) || preg_match("/.*?$f.*?/i", $line['user']) || + preg_match("/.*?$f.*?/i", $line['devagent']) || preg_match("/.*?$f.*?/i", $line['devid']) || preg_match("/.*?$f.*?/i", $line['addinfo']) )) + continue; + } + + $line['time'] = $line['ended'] - $line['start']; + $this->linesTerm[$line['update'].$line['pid']] = $line; + } + } + } + } + + // sort by execution time + krsort($this->linesActive); + krsort($this->linesOpen); + krsort($this->linesUnknown); + krsort($this->linesTerm); + } + + /** + * Prints data to the terminal + * + * @access private + * @return + */ + private function scrOverview() { + $linesAvail = $this->scrSize['height'] - 8; + $lc = 1; + $this->scrPrintAt($lc,0, "\033[1mZ-Push top live statistics\033[0m\t\t\t\t\t". @strftime("%d/%m/%Y %T")."\n"); $lc++; + + $this->scrPrintAt($lc,0, sprintf("Open connections: %d\t\t\t\tUsers:\t %d\tZ-Push: %s ",count($this->activeConn),count($this->activeUsers), $this->getVersion())); $lc++; + $this->scrPrintAt($lc,0, sprintf("Push connections: %d\t\t\t\tDevices: %d\tPHP-MAPI: %s", $this->pushConn, count($this->activeDevices),phpversion("mapi"))); $lc++; + $this->scrPrintAt($lc,0, sprintf(" Hosts:\t %d", $this->pushConn, count($this->activeHosts))); $lc++; + $lc++; + + $this->scrPrintAt($lc,0, "\033[4m". $this->getLine(array('pid'=>'PID', 'ip'=>'IP', 'user'=>'USER', 'command'=>'COMMAND', 'time'=>'TIME', 'devagent'=>'AGENT', 'devid'=>'DEVID', 'addinfo'=>'Additional Information')). str_repeat(" ",20)."\033[0m"); $lc++; + + // print help text if requested + $hl = 0; + if ($this->helpexpire > $this->currenttime) { + $help = $this->scrHelp(); + $linesAvail -= count($help); + $hl = $this->scrSize['height'] - count($help) -1; + foreach ($help as $h) { + $this->scrPrintAt($hl,0, $h); + $hl++; + } + } + + $toPrintActive = $linesAvail; + $toPrintOpen = $linesAvail; + $toPrintUnknown = $linesAvail; + $toPrintTerm = $linesAvail; + + // default view: show all unknown, no terminated and half active+open + if (count($this->linesActive) + count($this->linesOpen) + count($this->linesUnknown) > $linesAvail) { + $toPrintUnknown = count($this->linesUnknown); + $toPrintActive = count($this->linesActive); + $toPrintOpen = $linesAvail-$toPrintUnknown-$toPrintActive; + $toPrintTerm = 0; + } + + if ($this->showOption == self::SHOW_ACTIVE_ONLY) { + $toPrintActive = $linesAvail; + $toPrintOpen = 0; + $toPrintUnknown = 0; + $toPrintTerm = 0; + } + + if ($this->showOption == self::SHOW_UNKNOWN_ONLY) { + $toPrintActive = 0; + $toPrintOpen = 0; + $toPrintUnknown = $linesAvail; + $toPrintTerm = 0; + } + + $linesprinted = 0; + foreach ($this->linesActive as $time=>$l) { + if ($linesprinted >= $toPrintActive) + break; + + $this->scrPrintAt($lc,0, "\033[01m" . $this->getLine($l) ."\033[0m"); + $lc++; + $linesprinted++; + } + + $linesprinted = 0; + foreach ($this->linesOpen as $time=>$l) { + if ($linesprinted >= $toPrintOpen) + break; + + $this->scrPrintAt($lc,0, $this->getLine($l)); + $lc++; + $linesprinted++; + } + + $linesprinted = 0; + foreach ($this->linesUnknown as $time=>$l) { + if ($linesprinted >= $toPrintUnknown) + break; + + $color = "0;31m"; + if ($l['push'] == false && $time - $l["start"] > 30) + $color = "1;31m"; + $this->scrPrintAt($lc,0, "\033[0". $color . $this->getLine($l) ."\033[0m"); + $lc++; + $linesprinted++; + } + + if ($toPrintTerm > 0) + $toPrintTerm = $linesAvail - $lc +6; + + $linesprinted = 0; + foreach ($this->linesTerm as $time=>$l){ + if ($linesprinted >= $toPrintTerm) + break; + + $this->scrPrintAt($lc,0, "\033[01;30m" . $this->getLine($l) ."\033[0m"); + $lc++; + $linesprinted++; + } + + // add the lines used when displaying the help text + $lc += $hl; + $this->scrPrintAt($lc,0, "\033[K"); $lc++; + $this->scrPrintAt($lc,0, "Colorscheme: \033[01mActive \033[0mOpen \033[01;31mUnknown \033[01;30mTerminated\033[0m"); + + // remove old status + if ($this->statusexpire < $this->currenttime) + $this->status = false; + + // show request information and help command + if ($this->starttime + 6 > $this->currenttime) { + $this->status = sprintf("Requesting information (takes up to %dsecs)", $this->pingInterval). str_repeat(".", ($this->currenttime-$this->starttime)) . " type \033[01;31mh\033[00;31m or \033[01;31mhelp\033[00;31m for usage instructions"; + $this->statusexpire = $this->currenttime+1; + } + + + $str = ""; + if (! $this->showPush) + $str .= "\033[00;32mPush: \033[01;32mNo\033[0m "; + + if ($this->showOption == self::SHOW_ACTIVE_ONLY) + $str .= "\033[01;32mActive only\033[0m "; + + if ($this->showOption == self::SHOW_UNKNOWN_ONLY) + $str .= "\033[01;32mUnknown only\033[0m "; + + if ($this->showTermSec != self::SHOW_TERM_DEFAULT_TIME) + $str .= "\033[01;32mTerminated: ". $this->showTermSec. "s\033[0m "; + + if ($this->filter !== false || ($this->status !== false && $this->statusexpire > $this->currenttime)) { + // print filter in green + if ($this->filter !== false) + $str .= "\033[00;32mFilter: \033[01;32m$this->filter\033[0m "; + // print status in red + if ($this->status !== false) + $str .= "\033[00;31m$this->status\033[0m"; + } + $this->scrPrintAt(5,0, $str); + + $this->scrPrintAt(4,0,"Action: \033[01m".$this->action . "\033[0m"); + } + + /** + * Waits for a keystroke and processes the requested command + * + * @access private + * @return + */ + private function readLineProcess() { + $ans = explode("^^", `bash -c "read -n 1 -t 1 ANS ; echo \\\$?^^\\\$ANS;"`); + + if ($ans[0] < 128) { + if (isset($ans[1]) && bin2hex(trim($ans[1])) == "7f") { + $this->action = substr($this->action,0,-1); + } + + if (isset($ans[1]) && $ans[1] != "" ){ + $this->action .= trim(preg_replace("/[^A-Za-z0-9:]/","",$ans[1])); + } + + if (bin2hex($ans[0]) == "30" && bin2hex($ans[1]) == "0a") { + $cmds = explode(':', $this->action); + if ($cmds[0] == "quit" || $cmds[0] == "q" || (isset($cmds[1]) && $cmds[0] == "" && $cmds[1] == "q")) { + $this->topCollector->CollectData(true); + $this->topCollector->ClearLatest(true); + + $this->terminate = true; + } + else if ($cmds[0] == "clear" ) { + $this->topCollector->ClearLatest(true); + $this->topCollector->CollectData(true); + $this->topCollector->ReInitSharedMem(); + } + else if ($cmds[0] == "filter" || $cmds[0] == "f") { + if (!isset($cmds[1]) || $cmds[1] == "") { + $this->filter = false; + $this->status = "No filter"; + $this->statusexpire = $this->currenttime+5; + } + else { + $this->filter = $cmds[1]; + $this->status = false; + } + } + else if ($cmds[0] == "option" || $cmds[0] == "o") { + if (!isset($cmds[1]) || $cmds[1] == "") { + $this->status = sprintf("Option value needs to be specified. See 'help' or 'h' for instructions", $cmds[1]); + $this->statusexpire = $this->currenttime+5; + } + else if ($cmds[1] == "p" || $cmds[1] == "push" || $cmds[1] == "ping") + $this->showPush = !$this->showPush; + else if ($cmds[1] == "a" || $cmds[1] == "active") + $this->showOption = self::SHOW_ACTIVE_ONLY; + else if ($cmds[1] == "u" || $cmds[1] == "unknown") + $this->showOption = self::SHOW_UNKNOWN_ONLY; + else if ($cmds[1] == "d" || $cmds[1] == "default") { + $this->showOption = self::SHOW_DEFAULT; + $this->showTermSec = self::SHOW_TERM_DEFAULT_TIME; + $this->showPush = true; + } + else if (is_numeric($cmds[1])) + $this->showTermSec = $cmds[1]; + else { + $this->status = sprintf("Option '%s' unknown", $cmds[1]); + $this->statusexpire = $this->currenttime+5; + } + } + else if ($cmds[0] == "reset" || $cmds[0] == "r") { + $this->filter = false; + $this->wide = false; + $this->helpexpire = 0; + $this->status = "resetted"; + $this->statusexpire = $this->currenttime+2; + } + else if ($cmds[0] == "wide" || $cmds[0] == "w") { + $this->wide = true; + $this->status = "w i d e view"; + $this->statusexpire = $this->currenttime+2; + } + else if ($cmds[0] == "help" || $cmds[0] == "h") { + $this->helpexpire = $this->currenttime+20; + } + else if (($cmds[0] == "log" || $cmds[0] == "l") && isset($cmds[1]) ) { + if (!file_exists(LOGFILE)) { + $this->status = "Logfile can not be found: ". LOGFILE; + } + else { + system('bash -c "fgrep -a '.escapeshellarg($cmds[1]).' '. LOGFILE .' | less +G" > `tty`'); + $this->status = "Returning from log, updating data"; + } + $this->statusexpire = time()+5; // it might be much "later" now + } + else if (($cmds[0] == "tail" || $cmds[0] == "t")) { + if (!file_exists(LOGFILE)) { + $this->status = "Logfile can not be found: ". LOGFILE; + } + else { + $this->doingTail = true; + $this->scrClear(); + $this->scrPrintAt(1,0,$this->scrAsBold("Press CTRL+C to return to Z-Push-Top\n\n")); + $secondary = ""; + if (isset($cmds[1])) $secondary = " -n 200 | grep ".escapeshellarg($cmds[1]); + system('bash -c "tail -f '. LOGFILE . $secondary . '" > `tty`'); + $this->doingTail = false; + $this->status = "Returning from tail, updating data"; + } + $this->statusexpire = time()+5; // it might be much "later" now + } + + else if ($cmds[0] != "") { + $this->status = sprintf("Command '%s' unknown", $cmds[0]); + $this->statusexpire = $this->currenttime+8; + } + $this->action = ""; + } + } + } + + /** + * Signal handler function + * + * @param int $signo signal number + * + * @access public + * @return + */ + public function SignalHandler($signo) { + // don't terminate if the signal was sent by terminating tail + if (!$this->doingTail) { + $this->topCollector->CollectData(true); + $this->topCollector->ClearLatest(true); + $this->terminate = true; + } + } + + /** + * Returns usage instructions + * + * @return string + * @access public + */ + public function UsageInstructions() { + $help = "Usage:\n\tz-push-top.php\n\n" . + " Z-Push-Top is a live top-like overview of what Z-Push is doing. It does not have specific command line options.\n\n". + " When Z-Push-Top is running you can specify certain actions and options which can be executed (listed below).\n". + " This help information can also be shown inside Z-Push-Top by hitting 'help' or 'h'.\n\n"; + $scrhelp = $this->scrHelp(); + unset($scrhelp[0]); + + $help .= implode("\n", $scrhelp); + $help .= "\n\n"; + return $help; + } + + + /** + * Prints a 'help' text at the end of the page + * + * @access private + * @return array with help lines + */ + private function scrHelp() { + $h = array(); + $secs = $this->helpexpire - $this->currenttime; + $h[] = "Actions supported by Z-Push-Top (help page still displayed for ".$secs."secs)"; + $h[] = " ".$this->scrAsBold("Action")."\t\t".$this->scrAsBold("Comment"); + $h[] = " ".$this->scrAsBold("h")." or ".$this->scrAsBold("help")."\t\tDisplays this information."; + $h[] = " ".$this->scrAsBold("q").", ".$this->scrAsBold("quit")." or ".$this->scrAsBold(":q")."\t\tExits Z-Push-Top."; + $h[] = " ".$this->scrAsBold("w")." or ".$this->scrAsBold("wide")."\t\tTries not to truncate data. Automatically done if more than 180 columns available."; + $h[] = " ".$this->scrAsBold("f:VAL")." or ".$this->scrAsBold("filter:VAL")."\tOnly display connections which contain VAL. This value is case-insensitive."; + $h[] = " ".$this->scrAsBold("f:")." or ".$this->scrAsBold("filter:")."\t\tWithout a search word: resets the filter."; + $h[] = " ".$this->scrAsBold("l:STR")." or ".$this->scrAsBold("log:STR")."\tIssues 'less +G' on the logfile, after grepping on the optional STR."; + $h[] = " ".$this->scrAsBold("t:STR")." or ".$this->scrAsBold("tail:STR")."\tIssues 'tail -f' on the logfile, grepping for optional STR."; + $h[] = " ".$this->scrAsBold("r")." or ".$this->scrAsBold("reset")."\t\tResets 'wide' or 'filter'."; + $h[] = " ".$this->scrAsBold("o:")." or ".$this->scrAsBold("option:")."\t\tSets display options. Valid options specified below"; + $h[] = " ".$this->scrAsBold(" p")." or ".$this->scrAsBold("push")."\t\tLists/not lists active and open push connections."; + $h[] = " ".$this->scrAsBold(" a")." or ".$this->scrAsBold("action")."\t\tLists only active connections."; + $h[] = " ".$this->scrAsBold(" u")." or ".$this->scrAsBold("unknown")."\tLists only unknown connections."; + $h[] = " ".$this->scrAsBold(" 10")." or ".$this->scrAsBold("20")."\t\tLists terminated connections for 10 or 20 seconds. Any other number can be used."; + $h[] = " ".$this->scrAsBold(" d")." or ".$this->scrAsBold("default")."\tUses default options"; + + return $h; + } + + /** + * Encapsulates string with different color escape characters + * + * @param string $text + * + * @access private + * @return string same text as bold + */ + private function scrAsBold($text) { + return "\033[01m" . $text ."\033[0m"; + } + + /** + * Prints one line of precessed data + * + * @param array $l line information + * + * @access private + * @return string + */ + 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']); + 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']); + } + + /** + * Pads and trims string + * + * @param string $string to be trimmed/padded + * @param int $size characters to be considered + * @param boolean $cutmiddle (optional) indicates where to long information should + * be trimmed of, false means at the end + * + * @access private + * @return string + */ + private function ptStr($str, $size, $cutmiddle = false) { + if (strlen($str) < $size) + return str_pad($str, $size); + else if ($cutmiddle == true) { + $cut = ($size-2)/2; + return $this->ptStr(substr($str,0,$cut) ."..". substr($str,(-1)*($cut-1)), $size); + } + else { + return substr($str,0,$size-3).".. "; + } + } + + /** + * Tries to discover the size of the current terminal + * + * @access private + * @return array 'width' and 'height' as keys + */ + private function scrGetSize() { + preg_match_all("/rows.([0-9]+);.columns.([0-9]+);/", strtolower(exec('stty -a | fgrep columns')), $output); + if(sizeof($output) == 3) + return array('width' => $output[2][0], 'height' => $output[1][0]); + + return array('width' => 80, 'height' => 24); + } + + /** + * Returns the version of the current Z-Push installation + * + * @access private + * @return string + */ + private function getVersion() { + if (ZPUSH_VERSION == "SVN checkout" && file_exists(REAL_BASE_PATH.".svn/entries")) { + $svn = file(REAL_BASE_PATH.".svn/entries"); + return "SVN " . substr(trim($svn[4]),stripos($svn[4],"z-push")+7) ." r".trim($svn[3]); + } + return ZPUSH_VERSION; + } + + /** + * Converts seconds in MM:SS + * + * @param int $s seconds + * + * @access private + * @return string + */ + private function sec2min($s) { + if (!is_int($s)) + return $s; + return sprintf("%02.2d:%02.2d", floor($s/60), $s%60); + } + + /** + * Resets the default colors of the terminal + * + * @access private + * @return + */ + private function scrDefaultColors() { + echo "\033[0m"; + } + + /** + * Clears screen of the terminal + * + * @param array $data + * + * @access private + * @return + */ + public function scrClear() { + echo "\033[2J"; + } + + /** + * Prints a text at a specific screen/terminal coordinates + * + * @param int $row row number + * @param int $col column number + * @param string $text to be printed + * + * @access private + * @return + */ + private function scrPrintAt($row, $col, $text="") { + echo "\033[".$row.";".$col."H".$text; + } + +} + +?> \ No newline at end of file