From accc3e59043d4cd607cfbb9dad77152de3232535 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 13 Apr 2015 16:11:52 +0000 Subject: [PATCH] Initial version of the tracking plugin --- emailtracking/.idea/.name | 1 + emailtracking/.idea/emailtracking.iml | 8 + emailtracking/.idea/encodings.xml | 4 + emailtracking/.idea/misc.xml | 7 + emailtracking/.idea/modules.xml | 8 + emailtracking/.idea/scopes/scope_settings.xml | 5 + emailtracking/.idea/vcs.xml | 6 + emailtracking/.idea/workspace.xml | 619 ++++++++++++ emailtracking/build.xml | 198 ++++ emailtracking/config.php | 13 + emailtracking/js/EmailTrackingPlugin.js | 202 ++++ emailtracking/js/data/ResponseHandler.js | 25 + emailtracking/js/ui/TrackingInfoPanel.js | 116 +++ emailtracking/manifest.xml | 37 + .../php/class.plugintrackingmodule.php | 62 ++ emailtracking/php/lib/class.EmailTracker.php | 120 +++ emailtracking/php/lib/database_structure.sql | 48 + emailtracking/php/lib/medoo.php | 936 ++++++++++++++++++ emailtracking/php/plugin.emailtracking.php | 151 +++ emailtracking/php/track.php | 32 + .../resources/css/emailtracking-styles.css | 11 + .../resources/icons/ico_tracking_off.png | Bin 0 -> 465 bytes .../resources/icons/ico_tracking_on.png | Bin 0 -> 787 bytes .../resources/icons/ico_trackingxcf.xcf | Bin 0 -> 2747 bytes 24 files changed, 2609 insertions(+) create mode 100644 emailtracking/.idea/.name create mode 100644 emailtracking/.idea/emailtracking.iml create mode 100644 emailtracking/.idea/encodings.xml create mode 100644 emailtracking/.idea/misc.xml create mode 100644 emailtracking/.idea/modules.xml create mode 100644 emailtracking/.idea/scopes/scope_settings.xml create mode 100644 emailtracking/.idea/vcs.xml create mode 100644 emailtracking/.idea/workspace.xml create mode 100644 emailtracking/build.xml create mode 100644 emailtracking/config.php create mode 100644 emailtracking/js/EmailTrackingPlugin.js create mode 100644 emailtracking/js/data/ResponseHandler.js create mode 100644 emailtracking/js/ui/TrackingInfoPanel.js create mode 100644 emailtracking/manifest.xml create mode 100644 emailtracking/php/class.plugintrackingmodule.php create mode 100644 emailtracking/php/lib/class.EmailTracker.php create mode 100644 emailtracking/php/lib/database_structure.sql create mode 100644 emailtracking/php/lib/medoo.php create mode 100644 emailtracking/php/plugin.emailtracking.php create mode 100644 emailtracking/php/track.php create mode 100644 emailtracking/resources/css/emailtracking-styles.css create mode 100644 emailtracking/resources/icons/ico_tracking_off.png create mode 100644 emailtracking/resources/icons/ico_tracking_on.png create mode 100644 emailtracking/resources/icons/ico_trackingxcf.xcf diff --git a/emailtracking/.idea/.name b/emailtracking/.idea/.name new file mode 100644 index 0000000..a255bf6 --- /dev/null +++ b/emailtracking/.idea/.name @@ -0,0 +1 @@ +emailtracking \ No newline at end of file diff --git a/emailtracking/.idea/emailtracking.iml b/emailtracking/.idea/emailtracking.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/emailtracking/.idea/emailtracking.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/emailtracking/.idea/encodings.xml b/emailtracking/.idea/encodings.xml new file mode 100644 index 0000000..d821048 --- /dev/null +++ b/emailtracking/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/emailtracking/.idea/misc.xml b/emailtracking/.idea/misc.xml new file mode 100644 index 0000000..c753c0f --- /dev/null +++ b/emailtracking/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/emailtracking/.idea/modules.xml b/emailtracking/.idea/modules.xml new file mode 100644 index 0000000..30add55 --- /dev/null +++ b/emailtracking/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/emailtracking/.idea/scopes/scope_settings.xml b/emailtracking/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/emailtracking/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/emailtracking/.idea/vcs.xml b/emailtracking/.idea/vcs.xml new file mode 100644 index 0000000..78e665f --- /dev/null +++ b/emailtracking/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/emailtracking/.idea/workspace.xml b/emailtracking/.idea/workspace.xml new file mode 100644 index 0000000..4cf7b5d --- /dev/null +++ b/emailtracking/.idea/workspace.xml @@ -0,0 +1,619 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + $USER_HOME$/.subversion + 125 + + + + + 1425490373578 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/emailtracking/build.xml b/emailtracking/build.xml new file mode 100644 index 0000000..f15b50e --- /dev/null +++ b/emailtracking/build.xml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + var Ext = {}; + var Zarafa = {}; + var container = {}; + var _ = function(key, domain) {}; + var dgettext = function(domain, msgid) {}; + var dngettext = function(domain, msgid, msgid_plural, count) {}; + var dnpgettext = function(domain, msgctxt, msgid, msgid_plural, count) {}; + var dpgettext = function(domain, msgctxt, msgid) {}; + var ngettext = function(msgid, msgid_plural, count) {}; + var npgettext = function(msgctxt, msgid, msgid_plural, count) {}; + var pgettext = function(msgctxt, msgid) {}; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/emailtracking/config.php b/emailtracking/config.php new file mode 100644 index 0000000..9e6ee63 --- /dev/null +++ b/emailtracking/config.php @@ -0,0 +1,13 @@ + diff --git a/emailtracking/js/EmailTrackingPlugin.js b/emailtracking/js/EmailTrackingPlugin.js new file mode 100644 index 0000000..5b96bd3 --- /dev/null +++ b/emailtracking/js/EmailTrackingPlugin.js @@ -0,0 +1,202 @@ +Ext.namespace('Zarafa.plugins.emailtracking'); + +/** + * @class Zarafa.plugins.emailtracking.EmailTrackingPlugin + * @extends Zarafa.core.Plugin + */ +Zarafa.plugins.emailtracking.EmailTrackingPlugin = Ext.extend(Zarafa.core.Plugin, { + /* + * Called after constructor. + * Registers insertion points. + * @protected + */ + initPlugin : function() + { + Zarafa.plugins.emailtracking.EmailTrackingPlugin.superclass.initPlugin.apply(this, arguments); + + // Tracking button in mailcreatecontentpanel + this.registerInsertionPoint('context.mail.mailcreatecontentpanel.toolbar.options', this.showTrackButton, this); + + // Insertion point which shows the read status + this.registerInsertionPoint('previewpanel.toolbar.detaillinks', this.showTrackingInfo, this); + + Zarafa.core.data.SharedComponentType.addProperty('plugin.emailtracking.ui.trackinginfopanel'); + }, + /** + * Displays Tracking information in the previewpanel + * + * @return {Object} a box which on record update displays Tracking information + */ + showTrackingInfo : function() + { + return { + xtype: 'button', + style: 'margin-top: 4px; border: 2px solid red;', + plugins : [ 'zarafa.recordcomponentupdaterplugin' ], + autoEl: { + tag: 'div', + ref: 'trackingInfoBox' + }, + scope : this, + update : this.onTrackingInfo, + handler : this.onTrackingButton + }; + }, + + /** + * Handler for the button which is displayed when a encrypted / signed message is openend + * When an encrypted message is opened, we will send a request to unlock the certificate. + * When an signed email is openend, we will show a popup with extra information about the signed message + */ + onTrackingButton: function(button, config) + { + var user = container.getUser(); + + container.getRequest().singleRequest( + 'plugintrackingmodule', + 'gettrackinglog', + { + 'trackingid' : button.record.get("trackingcode"), + 'user' : user.getSMTPAddress() + }, + new Zarafa.plugins.emailtracking.data.ResponseHandler({ + successCallback : this.onTrackingLogLoaded + }) + ); + }, + + onTrackingLogLoaded: function(response) { + if(response.status) { + Zarafa.core.data.UIFactory.openLayerComponent(Zarafa.core.data.SharedComponentType['plugin.emailtracking.ui.trackinginfopanel'], response, { + manager: Ext.WindowMgr + }); + } else { + container.getNotifier().notify('info.saved', _('Error'), "No tracking log available!"); + } + }, + + /** + * Function which displays information in the previewpanel. + * In the case of a encrypted message, we either show a button to unlock the certificate + * or shows the message that the message has been decrypted. + * If the message is signed we display information depending on the state of the verification. + * + * @param {Zarafa.core.data.IPMRecord} record record which is displayed + * @param {Boolean} contentReset force the component to perform a full update of the data. + */ + onTrackingInfo : function(record, resetContent) { + // Set button.record for use in onSmimeButton + this.record = record; + var infoBox = this.getEl(); + + // Set smimeBox to empty value by default, to override previous S/MIME message text + infoBox.update(""); + + if (this.record.opened) { + // get the tracking code from the body + var body = this.record.getBody(true); + var trackingRegex = /(?:^|.*)track.php\?img=(.*?)(?:\"|\s|$)/g; + var match = trackingRegex.exec(body); + + if(match) { + infoBox.update("This message is getting tracked (" + match[1] + ")"); + infoBox.show(); + this.record.set("trackingcode", match[1]); + } + } else { + infoBox.hide(); + } + }, + + /** + * Create button which adds some tracking specials to the mail content. + * + * @return {Config} creates a button for tracking email + */ + showTrackButton : function() + { + return { + xtype : 'button', + text : _('Track'), + tooltip: { + title: _('Track message'), + text: _('Track this message and get detailed information about the current status.') + }, + iconCls : 'icon_tracking_off', + handler : this.onTrackButton, + scope : this + }; + }, + + /** + * Handler for the sign button, when clicked it checks if the private certificate exists. + * If we have signing already set and click it again, we unset it. + * If we already set have encryption set, we set a special message_class for both sign+ecnrypt. + * + * + * @param {Ext.button} button + * @param {Object} config + */ + onTrackButton : function(button, config) + { + var owner = button.ownerCt; + var record = owner.record; + var doTrack = button.iconCls === "icon_tracking_on" ? false : true; + if(record && doTrack) { + button.setIconClass('icon_tracking_on'); + record.set('message_class', 'IPM.Note.Tracking'); + record.set('entryid', record.entryid); + } else { + record.set('message_class', 'IPM.Note'); + button.setIconClass('icon_tracking_off'); + } + + owner.dialog.saveRecord(); + }, + + /* + * Bid for the type of shared component + * and the given record. + * This will bid on calendar.dialogs.importevents + * @param {Zarafa.core.data.SharedComponentType} type Type of component a context can bid for. + * @param {Zarafa.mail.dialogs.MailCreateContentPanel} owner Optionally passed panel + * @return {Number} The bid for the shared component + */ + bidSharedComponent : function(type, record) { + var bid = -1; + + switch(type) { + case Zarafa.core.data.SharedComponentType['plugin.emailtracking.ui.trackinginfopanel']: + bid = 1; + break; + } + return bid; + }, + + /** + * Will return the reference to the shared component. + * Based on the type of component requested a component is returned. + * @param {Zarafa.core.data.SharedComponentType} type Type of component a context can bid for. + * @param {Zarafa.mail.dialogs.MailCreateContentPanel} owner Optionally passed panel + * @return {Ext.Component} Component + */ + getSharedComponent : function(type, record) { + var component; + + switch(type) { + case Zarafa.core.data.SharedComponentType['plugin.emailtracking.ui.trackinginfopanel']: + component = Zarafa.plugins.emailtracking.ui.TrackingInfoPanel; + break; + } + + return component; + } +}); + +Zarafa.onReady(function() { + container.registerPlugin(new Zarafa.core.PluginMetaData({ + name : 'emailtracking', + displayName : _('Tracking Plugin'), + pluginConstructor : Zarafa.plugins.emailtracking.EmailTrackingPlugin + })); +}); diff --git a/emailtracking/js/data/ResponseHandler.js b/emailtracking/js/data/ResponseHandler.js new file mode 100644 index 0000000..a4191cb --- /dev/null +++ b/emailtracking/js/data/ResponseHandler.js @@ -0,0 +1,25 @@ +Ext.namespace('Zarafa.plugins.emailtracking.data'); + +/** + * @class Zarafa.plugins.emailtracking.data.ResponseHandler + * @extends Zarafa.core.data.AbstractResponseHandler + * + * Emailtracking specific response handler. + */ +Zarafa.plugins.emailtracking.data.ResponseHandler = Ext.extend(Zarafa.core.data.AbstractResponseHandler, { + + /** + * @cfg {Function} successCallback The function which + * will be called after success request. + */ + successCallback : null, + + /** + * @param {Object} response Object contained the response data. + */ + doGettrackinglog : function(response) { + this.successCallback(response); + } +}); + +Ext.reg('emailtracking.responsehandler', Zarafa.plugins.emailtracking.data.ResponseHandler); \ No newline at end of file diff --git a/emailtracking/js/ui/TrackingInfoPanel.js b/emailtracking/js/ui/TrackingInfoPanel.js new file mode 100644 index 0000000..e3f6ba0 --- /dev/null +++ b/emailtracking/js/ui/TrackingInfoPanel.js @@ -0,0 +1,116 @@ +Ext.namespace('Zarafa.plugins.emailtracking.ui'); + +/** + * @class Zarafa.plugins.emailtracking.ui.TrackingInfoPanel + * @extends Zarafa.core.ui.ContentPanel + * + * Show the tracking table. + * @xtype emailtracking.trackinginfopanel + */ +Zarafa.plugins.emailtracking.ui.TrackingInfoPanel = Ext.extend(Zarafa.core.ui.ContentPanel, { + + /** + * @var object The server response + */ + record : null, + + store: null, + + /** + * @constructor + * @param config Configuration structure + */ + constructor : function(config) { + config = config || {}; + + // fill the store + // create the data store + this.store = new Ext.data.ArrayStore({ + fields: [ + {name: 'ip'}, + {name: 'timestamp', type: 'date'}, + {name: 'referrer'}, + {name: 'client'}, + {name: 'tracking_type'} + ] + }); + + var data = []; + var internals = container.getSettingsModel().get('zarafa/v1/plugins/emailtracking/internals'); + Ext.each(config.record.log, function(rec) { + // hide entries from the own system (only external tracking will be possible!) + + if(internals || (rec.referrer != window.location.href)) { + data.push([rec.ip_addr, rec.timestamp, rec.referrer, rec.client, rec.tracking_type]); + } + }); + + this.store.loadData(data); + + Ext.applyIf(config, { + layout : 'fit', + title : _('Tracking information'), + closeOnSave : true, + width : 340, + height : 300, + //Add panel + items : [{ + xtype: "grid", + style: 'margin-top:20px;', + store: this.store, + columns: [ + { + header : 'IP Address', + width : 75, + sortable : true, + dataIndex: 'ip' + }, + { + id : "timestamp", + header : 'Date', + width : 75, + sortable : true, + renderer : Ext.util.Format.dateRenderer('m/d/Y H:m:s'), + dataIndex: 'timestamp' + }, + { + header : 'Referrer', + width : 150, + hidden : true, + sortable : true, + dataIndex: 'referrer' + }, + { + header : 'Client', + width : 150, + hidden : true, + sortable : true, + dataIndex: 'client' + }, + { + header : 'Type', + width : 60, + hidden : true, + sortable : true, + dataIndex: 'tracking_type' + } + ], + viewConfig: { + forceFit: true + }, + sm: new Ext.grid.RowSelectionModel({singleSelect:true}), + frame: true + + },{ + xtype: 'button', + text: _('Close'), + handler: this.close, + scope: this + }] + }); + + Zarafa.plugins.emailtracking.ui.TrackingInfoPanel.superclass.constructor.call(this, config); + } +}); + +Ext.reg('emailtracking.trackinginfopanel' ,Zarafa.plugins.emailtracking.ui.TrackingInfoPanel); \ No newline at end of file diff --git a/emailtracking/manifest.xml b/emailtracking/manifest.xml new file mode 100644 index 0000000..cd1c7af --- /dev/null +++ b/emailtracking/manifest.xml @@ -0,0 +1,37 @@ + + + + + 0.1 + Tracking Plugin + Tracking Plugin + Christoph Haas + http://www.sprinternet.at + Enables Email Tracking in Webapp + + + config.php + + + + + + php/plugin.emailtracking.php + php/class.plugintrackingmodule.php + + + js/emailtracking.js + js/emailtracking-debug.js + js/EmailTrackingPlugin.js + js/ui/TrackingInfoPanel.js + js/data/ResponseHandler.js + + + resources/css/emailtracking.css + resources/css/emailtracking.css + resources/css/emailtracking-styles.css + + + + + diff --git a/emailtracking/php/class.plugintrackingmodule.php b/emailtracking/php/class.plugintrackingmodule.php new file mode 100644 index 0000000..75360c9 --- /dev/null +++ b/emailtracking/php/class.plugintrackingmodule.php @@ -0,0 +1,62 @@ +store = $GLOBALS['mapisession']->getDefaultMessageStore(); + parent::Module($id, $data); + } + + /** + * Executes all the actions in the $data variable. + * @return boolean true on success of false on fialure. + */ + function execute() + { + $this->emailTracker = new EmailTracker(); + + foreach($this->data as $actionType => $actionData) + { + if(isset($actionType)) { + try { + switch($actionType) + { + case "gettrackinglog" : + $data = $this->getTrackingLog($actionData); + $this->addActionData('gettrackinglog', $data); + $GLOBALS['bus']->addData($this->getResponseData()); + break; + default: + $this->handleUnknownActionType($actionType); + } + } catch (Exception $e) { + $this->sendFeedback(false, parent::errorDetailsFromException($e)); + } + } + } + } + + function getTrackingLog($actionData) { + $data = array(); + $data["trackingid"] = $actionData["trackingid"]; + $data["log"] = $this->emailTracker->getAllLogs($data["trackingid"]); + $data["status"] = true; + + return $data; + } +} diff --git a/emailtracking/php/lib/class.EmailTracker.php b/emailtracking/php/lib/class.EmailTracker.php new file mode 100644 index 0000000..4f8939b --- /dev/null +++ b/emailtracking/php/lib/class.EmailTracker.php @@ -0,0 +1,120 @@ + 'mysql', + 'database_name' => PLUGIN_EMAILTRACKING_DB_DB, + 'server' => PLUGIN_EMAILTRACKING_DB_HOST, + 'username' => PLUGIN_EMAILTRACKING_DB_USER, + 'password' => PLUGIN_EMAILTRACKING_DB_PASS, + 'port' => PLUGIN_EMAILTRACKING_DB_PORT, + 'charset' => 'utf8', + ); + $this->dbObj = new medoo($dbOptions); + } + + /** + * Generates a new ID out of some values + * + * @param $emailId + * @param $destAddr + * @param $srcAddr + * @param $subject + * @return string + */ + public function getNewTrackingCode ($emailId, $destAddr, $srcAddr, $subject) { + $currentTimeStamp = time(); + + $trackingHash = md5($emailId . $destAddr . $subject . $currentTimeStamp); + + $data = array( + "email_id" => $emailId, + "destination_addr" => $destAddr, + "source_addr" => $srcAddr, + "subject" => $subject, + "current_time" => date('Y-m-d H:i:s',$currentTimeStamp), + "generated_id" => $trackingHash + ); + $this->dbObj->insert("trackingid", $data); + + return $trackingHash; + } + + /** + * Get the database ID for the given hash + * + * @param $trackingHash + * @return int + */ + private function getTrackingIDbyCode ($trackingHash) { + $filter = array( + "generated_id" => $trackingHash, + ); + + $data = $this->dbObj->select("trackingid", "id", $filter); + + if (is_array($data) && count($data) > 0) { + return $data[0]; + } else { + return -1; + } + } + + /** + * Stores a new log entry to db + * + * @param $trackingHash + * @param $data + */ + public function addLog($trackingHash, $data) { + // first get the id - to add it to our new entry + $dbID = $this->getTrackingIDbyCode($trackingHash); + + // check if trackingHash was valid + if($dbID != -1) { + // build our tracking entry + $data["trackingid_id"] = $dbID; + + $this->dbObj->insert("trackinglog", $data); + } + } + + /** + * Get all Tracking logs for the specified hash. + * + * @param $trackingHash + * @return array|bool + */ + public function getAllLogs($trackingHash) { + // first get the id - to add it to our new entry + $dbID = $this->getTrackingIDbyCode($trackingHash); + + // check if trackingHash was valid + if($dbID != -1) { + $data = $this->dbObj->select("trackinglog", [ + "ip_addr", + "timestamp", + "referrer", + "client", + "tracking_type" + ], [ + "trackingid_id" => $dbID + ]); + + return $data; + } + + return false; + } +} diff --git a/emailtracking/php/lib/database_structure.sql b/emailtracking/php/lib/database_structure.sql new file mode 100644 index 0000000..6cf8bd1 --- /dev/null +++ b/emailtracking/php/lib/database_structure.sql @@ -0,0 +1,48 @@ +-- +-- Database: `emailtracking` +-- + +-- -------------------------------------------------------- + +-- +-- Table structure for table `trackingid` +-- + +CREATE TABLE IF NOT EXISTS `trackingid` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `email_id` varchar(500) NOT NULL, + `destination_addr` varchar(200) NOT NULL, + `source_addr` varchar(200) NOT NULL, + `subject` varchar(1024) NOT NULL, + `current_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `generated_id` varchar(32) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `trackinglog` +-- + +CREATE TABLE IF NOT EXISTS `trackinglog` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `trackingid_id` int(11) NOT NULL, + `ip_addr` varchar(100) NOT NULL, + `referrer` varchar(500) NOT NULL, + `client` varchar(1024) NOT NULL, + `tracking_type` varchar(20) NOT NULL, + `timestamp` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `trackingid_id` (`trackingid_id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=3 ; + +-- +-- Constraints for dumped tables +-- + +-- +-- Constraints for table `trackinglog` +-- +ALTER TABLE `trackinglog` + ADD CONSTRAINT `trackinglog_ibfk_1` FOREIGN KEY (`trackingid_id`) REFERENCES `trackingid` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; \ No newline at end of file diff --git a/emailtracking/php/lib/medoo.php b/emailtracking/php/lib/medoo.php new file mode 100644 index 0000000..f896dd2 --- /dev/null +++ b/emailtracking/php/lib/medoo.php @@ -0,0 +1,936 @@ +database_type) == 'sqlite') + { + $this->database_file = $options; + } + else + { + $this->database_name = $options; + } + } + elseif (is_array($options)) + { + foreach ($options as $option => $value) + { + $this->$option = $value; + } + } + + if ( + isset($this->port) && + is_int($this->port * 1) + ) + { + $port = $this->port; + } + + $type = strtolower($this->database_type); + $is_port = isset($port); + + switch ($type) + { + case 'mariadb': + $type = 'mysql'; + + case 'mysql': + if ($this->socket) + { + $dsn = $type . ':unix_socket=' . $this->socket . ';dbname=' . $this->database_name; + } + else + { + $dsn = $type . ':host=' . $this->server . ($is_port ? ';port=' . $port : '') . ';dbname=' . $this->database_name; + } + + // Make MySQL using standard quoted identifier + $commands[] = 'SET SQL_MODE=ANSI_QUOTES'; + break; + + case 'pgsql': + $dsn = $type . ':host=' . $this->server . ($is_port ? ';port=' . $port : '') . ';dbname=' . $this->database_name; + break; + + case 'sybase': + $dsn = 'dblib:host=' . $this->server . ($is_port ? ':' . $port : '') . ';dbname=' . $this->database_name; + break; + + case 'oracle': + $dbname = $this->server ? + '//' . $this->server . ($is_port ? ':' . $port : ':1521') . '/' . $this->database_name : + $this->database_name; + + $dsn = 'oci:dbname=' . $dbname . ($this->charset ? ';charset=' . $this->charset : ''); + break; + + case 'mssql': + $dsn = strstr(PHP_OS, 'WIN') ? + 'sqlsrv:server=' . $this->server . ($is_port ? ',' . $port : '') . ';database=' . $this->database_name : + 'dblib:host=' . $this->server . ($is_port ? ':' . $port : '') . ';dbname=' . $this->database_name; + + // Keep MSSQL QUOTED_IDENTIFIER is ON for standard quoting + $commands[] = 'SET QUOTED_IDENTIFIER ON'; + break; + + case 'sqlite': + $dsn = $type . ':' . $this->database_file; + $this->username = null; + $this->password = null; + break; + } + + if ( + in_array($type, explode(' ', 'mariadb mysql pgsql sybase mssql')) && + $this->charset + ) + { + $commands[] = "SET NAMES '" . $this->charset . "'"; + } + + $this->pdo = new PDO( + $dsn, + $this->username, + $this->password, + $this->option + ); + + foreach ($commands as $value) + { + $this->pdo->exec($value); + } + } + catch (PDOException $e) { + throw new Exception($e->getMessage()); + } + } + + public function query($query) + { + if ($this->debug_mode) + { + echo $query; + + $this->debug_mode = false; + + return false; + } + + array_push($this->logs, $query); + + return $this->pdo->query($query); + } + + public function exec($query) + { + if ($this->debug_mode) + { + echo $query; + + $this->debug_mode = false; + + return false; + } + + array_push($this->logs, $query); + + return $this->pdo->exec($query); + } + + public function quote($string) + { + return $this->pdo->quote($string); + } + + protected function column_quote($string) + { + return '"' . str_replace('.', '"."', preg_replace('/(^#|\(JSON\))/', '', $string)) . '"'; + } + + protected function column_push($columns) + { + if ($columns == '*') + { + return $columns; + } + + if (is_string($columns)) + { + $columns = array($columns); + } + + $stack = array(); + + foreach ($columns as $key => $value) + { + preg_match('/([a-zA-Z0-9_\-\.]*)\s*\(([a-zA-Z0-9_\-]*)\)/i', $value, $match); + + if (isset($match[1], $match[2])) + { + array_push($stack, $this->column_quote( $match[1] ) . ' AS ' . $this->column_quote( $match[2] )); + } + else + { + array_push($stack, $this->column_quote( $value )); + } + } + + return implode($stack, ','); + } + + protected function array_quote($array) + { + $temp = array(); + + foreach ($array as $value) + { + $temp[] = is_int($value) ? $value : $this->pdo->quote($value); + } + + return implode($temp, ','); + } + + protected function inner_conjunct($data, $conjunctor, $outer_conjunctor) + { + $haystack = array(); + + foreach ($data as $value) + { + $haystack[] = '(' . $this->data_implode($value, $conjunctor) . ')'; + } + + return implode($outer_conjunctor . ' ', $haystack); + } + + protected function fn_quote($column, $string) + { + return (strpos($column, '#') === 0 && preg_match('/^[A-Z0-9\_]*\([^)]*\)$/', $string)) ? + + $string : + + $this->quote($string); + } + + protected function data_implode($data, $conjunctor, $outer_conjunctor = null) + { + $wheres = array(); + + foreach ($data as $key => $value) + { + $type = gettype($value); + + if ( + preg_match("/^(AND|OR)\s*#?/i", $key, $relation_match) && + $type == 'array' + ) + { + $wheres[] = 0 !== count(array_diff_key($value, array_keys(array_keys($value)))) ? + '(' . $this->data_implode($value, ' ' . $relation_match[1]) . ')' : + '(' . $this->inner_conjunct($value, ' ' . $relation_match[1], $conjunctor) . ')'; + } + else + { + preg_match('/(#?)([\w\.]+)(\[(\>|\>\=|\<|\<\=|\!|\<\>|\>\<|\!?~)\])?/i', $key, $match); + $column = $this->column_quote($match[2]); + + if (isset($match[4])) + { + $operator = $match[4]; + + if ($operator == '!') + { + switch ($type) + { + case 'NULL': + $wheres[] = $column . ' IS NOT NULL'; + break; + + case 'array': + $wheres[] = $column . ' NOT IN (' . $this->array_quote($value) . ')'; + break; + + case 'integer': + case 'double': + $wheres[] = $column . ' != ' . $value; + break; + + case 'boolean': + $wheres[] = $column . ' != ' . ($value ? '1' : '0'); + break; + + case 'string': + $wheres[] = $column . ' != ' . $this->fn_quote($key, $value); + break; + } + } + + if ($operator == '<>' || $operator == '><') + { + if ($type == 'array') + { + if ($operator == '><') + { + $column .= ' NOT'; + } + + if (is_numeric($value[0]) && is_numeric($value[1])) + { + $wheres[] = '(' . $column . ' BETWEEN ' . $value[0] . ' AND ' . $value[1] . ')'; + } + else + { + $wheres[] = '(' . $column . ' BETWEEN ' . $this->quote($value[0]) . ' AND ' . $this->quote($value[1]) . ')'; + } + } + } + + if ($operator == '~' || $operator == '!~') + { + if ($type == 'string') + { + $value = array($value); + } + + if (!empty($value)) + { + $like_clauses = array(); + + foreach ($value as $item) + { + if ($operator == '!~') + { + $column .= ' NOT'; + } + + if (preg_match('/^(?!%).+(?fn_quote($key, $item); + } + + $wheres[] = implode(' OR ', $like_clauses); + } + } + + if (in_array($operator, array('>', '>=', '<', '<='))) + { + if (is_numeric($value)) + { + $wheres[] = $column . ' ' . $operator . ' ' . $value; + } + elseif (strpos($key, '#') === 0) + { + $wheres[] = $column . ' ' . $operator . ' ' . $this->fn_quote($key, $value); + } + else + { + $wheres[] = $column . ' ' . $operator . ' ' . $this->quote($value); + } + } + } + else + { + switch ($type) + { + case 'NULL': + $wheres[] = $column . ' IS NULL'; + break; + + case 'array': + $wheres[] = $column . ' IN (' . $this->array_quote($value) . ')'; + break; + + case 'integer': + case 'double': + $wheres[] = $column . ' = ' . $value; + break; + + case 'boolean': + $wheres[] = $column . ' = ' . ($value ? '1' : '0'); + break; + + case 'string': + $wheres[] = $column . ' = ' . $this->fn_quote($key, $value); + break; + } + } + } + } + + return implode($conjunctor . ' ', $wheres); + } + + protected function where_clause($where) + { + $where_clause = ''; + + if (is_array($where)) + { + $where_keys = array_keys($where); + $where_AND = preg_grep("/^AND\s*#?$/i", $where_keys); + $where_OR = preg_grep("/^OR\s*#?$/i", $where_keys); + + $single_condition = array_diff_key($where, array_flip( + explode(' ', 'AND OR GROUP ORDER HAVING LIMIT LIKE MATCH') + )); + + if ($single_condition != array()) + { + $where_clause = ' WHERE ' . $this->data_implode($single_condition, ''); + } + + if (!empty($where_AND)) + { + $value = array_values($where_AND); + $where_clause = ' WHERE ' . $this->data_implode($where[ $value[0] ], ' AND'); + } + + if (!empty($where_OR)) + { + $value = array_values($where_OR); + $where_clause = ' WHERE ' . $this->data_implode($where[ $value[0] ], ' OR'); + } + + if (isset($where['MATCH'])) + { + $MATCH = $where['MATCH']; + + if (is_array($MATCH) && isset($MATCH['columns'], $MATCH['keyword'])) + { + $where_clause .= ($where_clause != '' ? ' AND ' : ' WHERE ') . ' MATCH ("' . str_replace('.', '"."', implode($MATCH['columns'], '", "')) . '") AGAINST (' . $this->quote($MATCH['keyword']) . ')'; + } + } + + if (isset($where['GROUP'])) + { + $where_clause .= ' GROUP BY ' . $this->column_quote($where['GROUP']); + + if (isset($where['HAVING'])) + { + $where_clause .= ' HAVING ' . $this->data_implode($where['HAVING'], ' AND'); + } + } + + if (isset($where['ORDER'])) + { + $rsort = '/(^[a-zA-Z0-9_\-\.]*)(\s*(DESC|ASC))?/'; + $ORDER = $where['ORDER']; + + if (is_array($ORDER)) + { + if ( + isset($ORDER[1]) && + is_array($ORDER[1]) + ) + { + $where_clause .= ' ORDER BY FIELD(' . $this->column_quote($ORDER[0]) . ', ' . $this->array_quote($ORDER[1]) . ')'; + } + else + { + $stack = array(); + + foreach ($ORDER as $column) + { + preg_match($rsort, $column, $order_match); + + array_push($stack, '"' . str_replace('.', '"."', $order_match[1]) . '"' . (isset($order_match[3]) ? ' ' . $order_match[3] : '')); + } + + $where_clause .= ' ORDER BY ' . implode($stack, ','); + } + } + else + { + preg_match($rsort, $ORDER, $order_match); + + $where_clause .= ' ORDER BY "' . str_replace('.', '"."', $order_match[1]) . '"' . (isset($order_match[3]) ? ' ' . $order_match[3] : ''); + } + } + + if (isset($where['LIMIT'])) + { + $LIMIT = $where['LIMIT']; + + if (is_numeric($LIMIT)) + { + $where_clause .= ' LIMIT ' . $LIMIT; + } + + if ( + is_array($LIMIT) && + is_numeric($LIMIT[0]) && + is_numeric($LIMIT[1]) + ) + { + $where_clause .= ' LIMIT ' . $LIMIT[0] . ',' . $LIMIT[1]; + } + } + } + else + { + if ($where != null) + { + $where_clause .= ' ' . $where; + } + } + + return $where_clause; + } + + protected function select_context($table, $join, &$columns = null, $where = null, $column_fn = null) + { + $table = '"' . $table . '"'; + $join_key = is_array($join) ? array_keys($join) : null; + + if ( + isset($join_key[0]) && + strpos($join_key[0], '[') === 0 + ) + { + $table_join = array(); + + $join_array = array( + '>' => 'LEFT', + '<' => 'RIGHT', + '<>' => 'FULL', + '><' => 'INNER' + ); + + foreach($join as $sub_table => $relation) + { + preg_match('/(\[(\<|\>|\>\<|\<\>)\])?([a-zA-Z0-9_\-]*)\s?(\(([a-zA-Z0-9_\-]*)\))?/', $sub_table, $match); + + if ($match[2] != '' && $match[3] != '') + { + if (is_string($relation)) + { + $relation = 'USING ("' . $relation . '")'; + } + + if (is_array($relation)) + { + // For ['column1', 'column2'] + if (isset($relation[0])) + { + $relation = 'USING ("' . implode($relation, '", "') . '")'; + } + // For ['column1' => 'column2'] + else + { + $relation = 'ON ' . $table . '."' . key($relation) . '" = "' . (isset($match[5]) ? $match[5] : $match[3]) . '"."' . current($relation) . '"'; + } + } + + $table_join[] = $join_array[ $match[2] ] . ' JOIN "' . $match[3] . '" ' . (isset($match[5]) ? 'AS "' . $match[5] . '" ' : '') . $relation; + } + } + + $table .= ' ' . implode($table_join, ' '); + } + else + { + if (is_null($columns)) + { + if (is_null($where)) + { + if ( + is_array($join) && + isset($column_fn) + ) + { + $where = $join; + $columns = null; + } + else + { + $where = null; + $columns = $join; + } + } + else + { + $where = $join; + $columns = null; + } + } + else + { + $where = $columns; + $columns = $join; + } + } + + if (isset($column_fn)) + { + if ($column_fn == 1) + { + $column = '1'; + + if (is_null($where)) + { + $where = $columns; + } + } + else + { + if (empty($columns)) + { + $columns = '*'; + $where = $join; + } + + $column = $column_fn . '(' . $this->column_push($columns) . ')'; + } + } + else + { + $column = $this->column_push($columns); + } + + return 'SELECT ' . $column . ' FROM ' . $table . $this->where_clause($where); + } + + public function select($table, $join, $columns = null, $where = null) + { + $query = $this->query($this->select_context($table, $join, $columns, $where)); + + return $query ? $query->fetchAll( + (is_string($columns) && $columns != '*') ? PDO::FETCH_COLUMN : PDO::FETCH_ASSOC + ) : false; + } + + public function insert($table, $datas) + { + $lastId = array(); + + // Check indexed or associative array + if (!isset($datas[0])) + { + $datas = array($datas); + } + + foreach ($datas as $data) + { + $keys = array_keys($data); + $values = array(); + $columns = array(); + + foreach ($data as $key => $value) + { + array_push($columns, $this->column_quote($key)); + + switch (gettype($value)) + { + case 'NULL': + $values[] = 'NULL'; + break; + + case 'array': + preg_match("/\(JSON\)\s*([\w]+)/i", $key, $column_match); + + $values[] = isset($column_match[0]) ? + $this->quote(json_encode($value)) : + $this->quote(serialize($value)); + break; + + case 'boolean': + $values[] = ($value ? '1' : '0'); + break; + + case 'integer': + case 'double': + case 'string': + $values[] = $this->fn_quote($key, $value); + break; + } + } + + $this->exec('INSERT INTO "' . $table . '" (' . implode(', ', $columns) . ') VALUES (' . implode($values, ', ') . ')'); + + $lastId[] = $this->pdo->lastInsertId(); + } + + return count($lastId) > 1 ? $lastId : $lastId[ 0 ]; + } + + public function update($table, $data, $where = null) + { + $fields = array(); + + foreach ($data as $key => $value) + { + preg_match('/([\w]+)(\[(\+|\-|\*|\/)\])?/i', $key, $match); + + if (isset($match[3])) + { + if (is_numeric($value)) + { + $fields[] = $this->column_quote($match[1]) . ' = ' . $this->column_quote($match[1]) . ' ' . $match[3] . ' ' . $value; + } + } + else + { + $column = $this->column_quote($key); + + switch (gettype($value)) + { + case 'NULL': + $fields[] = $column . ' = NULL'; + break; + + case 'array': + preg_match("/\(JSON\)\s*([\w]+)/i", $key, $column_match); + + $fields[] = $column . ' = ' . $this->quote( + isset($column_match[0]) ? json_encode($value) : serialize($value) + ); + break; + + case 'boolean': + $fields[] = $column . ' = ' . ($value ? '1' : '0'); + break; + + case 'integer': + case 'double': + case 'string': + $fields[] = $column . ' = ' . $this->fn_quote($key, $value); + break; + } + } + } + + return $this->exec('UPDATE "' . $table . '" SET ' . implode(', ', $fields) . $this->where_clause($where)); + } + + public function delete($table, $where) + { + return $this->exec('DELETE FROM "' . $table . '"' . $this->where_clause($where)); + } + + public function replace($table, $columns, $search = null, $replace = null, $where = null) + { + if (is_array($columns)) + { + $replace_query = array(); + + foreach ($columns as $column => $replacements) + { + foreach ($replacements as $replace_search => $replace_replacement) + { + $replace_query[] = $column . ' = REPLACE(' . $this->column_quote($column) . ', ' . $this->quote($replace_search) . ', ' . $this->quote($replace_replacement) . ')'; + } + } + + $replace_query = implode(', ', $replace_query); + $where = $search; + } + else + { + if (is_array($search)) + { + $replace_query = array(); + + foreach ($search as $replace_search => $replace_replacement) + { + $replace_query[] = $columns . ' = REPLACE(' . $this->column_quote($columns) . ', ' . $this->quote($replace_search) . ', ' . $this->quote($replace_replacement) . ')'; + } + + $replace_query = implode(', ', $replace_query); + $where = $replace; + } + else + { + $replace_query = $columns . ' = REPLACE(' . $this->column_quote($columns) . ', ' . $this->quote($search) . ', ' . $this->quote($replace) . ')'; + } + } + + return $this->exec('UPDATE "' . $table . '" SET ' . $replace_query . $this->where_clause($where)); + } + + public function get($table, $join = null, $column = null, $where = null) + { + if (!isset($where)) + { + $where = array(); + } + + $where['LIMIT'] = 1; + + $query = $this->query($this->select_context($table, $join, $column, $where)); + + if ($query) + { + $data = $query->fetchAll(PDO::FETCH_ASSOC); + + if (isset($data[0])) + { + $column = $where == null ? $join : $column; + + if (is_string($column) && $column != '*') + { + return $data[ 0 ][ $column ]; + } + + return $data[ 0 ]; + } + else + { + return false; + } + } + else + { + return false; + } + } + + public function has($table, $join, $where = null) + { + $column = null; + + $query = $this->query('SELECT EXISTS(' . $this->select_context($table, $join, $column, $where, 1) . ')'); + + return $query ? $query->fetchColumn() === '1' : false; + } + + public function count($table, $join = null, $column = null, $where = null) + { + $query = $this->query($this->select_context($table, $join, $column, $where, 'COUNT')); + + return $query ? 0 + $query->fetchColumn() : false; + } + + public function max($table, $join, $column = null, $where = null) + { + $query = $this->query($this->select_context($table, $join, $column, $where, 'MAX')); + + if ($query) + { + $max = $query->fetchColumn(); + + return is_numeric($max) ? $max + 0 : $max; + } + else + { + return false; + } + } + + public function min($table, $join, $column = null, $where = null) + { + $query = $this->query($this->select_context($table, $join, $column, $where, 'MIN')); + + if ($query) + { + $min = $query->fetchColumn(); + + return is_numeric($min) ? $min + 0 : $min; + } + else + { + return false; + } + } + + public function avg($table, $join, $column = null, $where = null) + { + $query = $this->query($this->select_context($table, $join, $column, $where, 'AVG')); + + return $query ? 0 + $query->fetchColumn() : false; + } + + public function sum($table, $join, $column = null, $where = null) + { + $query = $this->query($this->select_context($table, $join, $column, $where, 'SUM')); + + return $query ? 0 + $query->fetchColumn() : false; + } + + public function debug() + { + $this->debug_mode = true; + + return $this; + } + + public function error() + { + return $this->pdo->errorInfo(); + } + + public function last_query() + { + return end($this->logs); + } + + public function log() + { + return $this->logs; + } + + public function info() + { + $output = array( + 'server' => 'SERVER_INFO', + 'driver' => 'DRIVER_NAME', + 'client' => 'CLIENT_VERSION', + 'version' => 'SERVER_VERSION', + 'connection' => 'CONNECTION_STATUS' + ); + + foreach ($output as $key => $value) + { + $output[ $key ] = $this->pdo->getAttribute(constant('PDO::ATTR_' . $value)); + } + + return $output; + } +} +?> \ No newline at end of file diff --git a/emailtracking/php/plugin.emailtracking.php b/emailtracking/php/plugin.emailtracking.php new file mode 100644 index 0000000..bd7d906 --- /dev/null +++ b/emailtracking/php/plugin.emailtracking.php @@ -0,0 +1,151 @@ +registerHook('server.core.settings.init.before'); + $this->registerHook('server.core.operations.submitmessage'); + $this->store = $GLOBALS['mapisession']->getDefaultMessageStore(); + $this->emailTracker = new EmailTracker(); + } + + /** + * Process the incoming events that where fired by the client. + * + * @param String $eventID Identifier of the hook + * @param Array $data Reference to the data of the triggered hook + */ + function execute($eventID, &$data) { + switch($eventID){ + // Register plugin + case 'server.core.settings.init.before': + $this->onBeforeSettingsInit($data); + break; + // Add tracking stuff before submitting the message + case 'server.core.operations.submitmessage': + $this->onBeforeSubmit($data); + break; + // Add tracking property, which is send to the client + case 'server.module.itemmodule.open.after': + $this->onAfterOpen($data); + break; + } + } + + /** + * This function handles the 'beforesend' hook which is triggered before sending the email. + * If the PR_MESSAGE_CLASS is set to a signed email (IPM.Note.SMIME.Multipartsigned), this function + * will convert the mapi message to RFC822, sign the eml and attach the signed email to the mapi message. + * + * @param {mixed} $data from php hook + */ + function onBeforeSubmit(&$data) + { + $message = $data['message']; + + // Retrieve message class + $props = mapi_getprops($message, array(PR_MESSAGE_CLASS, PR_SUBJECT, PR_SENDER_EMAIL_ADDRESS)); + if (mapi_is_error(mapi_last_hresult())) { + error_log("error loading props!"); + } + $recipients = mapi_message_getrecipienttable($message); + $messageClass = $props[PR_MESSAGE_CLASS]; + + if(isset($messageClass) && (stripos($messageClass, 'IPM.Note.Tracking') !== false)) { + // reset messageClass + mapi_setprops($message, array(PR_MESSAGE_CLASS => 'IPM.Note')); + + error_log("Number of recipients: " . mapi_table_getrowcount($recipients)); + + // get all recipient email addresses + $filters=array(PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS); + $rows = mapi_table_queryrows($recipients,$filters,0,1); // get only the first recipient + + $recipient = ""; + foreach($rows as $row) { + $recipient = $row[PR_ADDRTYPE] === "SMTP" ? $row[PR_SMTP_ADDRESS] : $row[PR_EMAIL_ADDRESS]; + } + + // get a new tracking id: + $trackingId = $this->emailTracker->getNewTrackingCode(md5($props[PR_SUBJECT] . time()), $recipient, $props[PR_SENDER_EMAIL_ADDRESS], $props[PR_SUBJECT]); + + $trackingURL_Image = PLUGIN_EMAILTRACKING_PUBLIC_WEBAPP_URL . "/plugins/emailtracking/php/track.php?img=" . $trackingId; + $trackingURL_Sound = PLUGIN_EMAILTRACKING_PUBLIC_WEBAPP_URL . "/plugins/emailtracking/php/track.php?bgsound=" . $trackingId; + + // add the tracking html tags to the body + $trackingHTML = ''; + $trackingHTML .= ''; + + // stream the body and add the tracking changes! + $messageStream = mapi_openproperty($message, PR_HTML, IID_IStream, 0, 0); + $stat = mapi_stream_stat($messageStream); + + // create temporary files + $tmpEmail = tempnam(sys_get_temp_dir(),true); + + $fhandle = fopen($tmpEmail,'w'); + $buffer = null; + for($i = 0; $i < $stat["cb"]; $i += BLOCK_SIZE) { + // Write stream + $buffer = mapi_stream_read($messageStream, BLOCK_SIZE); + fwrite($fhandle,$buffer,strlen($buffer)); + } + + // append tracking stuff + fwrite($fhandle, $trackingHTML, strlen($trackingHTML)); + fclose($fhandle); + + // Save the signed message as attachment of the send email + $outStream = mapi_openproperty($message, PR_HTML, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); + $handle = fopen($tmpEmail, 'r'); + while (!feof($handle)) { + $contents = fread($handle, BLOCK_SIZE); + mapi_stream_write($outStream, $contents); + } + fclose($handle); + + // Save stream + mapi_stream_commit($outStream); + + // remove tmp files + unlink($tmpEmail); + + // Save changes + mapi_savechanges($message); + } + } + + /** + * Called when the core Settings class is initialized and ready to accept sysadmin default + * settings. Registers the sysadmin defaults for the example plugin. + * + * @param {mixed} $data Reference to the data of the triggered hook + */ + function onBeforeSettingsInit(&$data){ + $data['settingsObj']->addSysAdminDefaults(Array( + 'zarafa' => Array( + 'v1' => Array( + 'plugins' => Array( + 'emailtracking' => Array( + 'enable' => PLUGIN_EMAILTRACKING_USER_DEFAULT_ENABLE_TRACKING, + 'internals' => PLUGIN_EMAILTRACKING_SHOW_INTERNAL_LOGS + ) + ) + ) + ) + )); + } +} +?> diff --git a/emailtracking/php/track.php b/emailtracking/php/track.php new file mode 100644 index 0000000..8c7718f --- /dev/null +++ b/emailtracking/php/track.php @@ -0,0 +1,32 @@ + $_SERVER["REMOTE_ADDR"], + "referrer" => isset($_SERVER["HTTP_REFERER"]) ? $_SERVER["HTTP_REFERER"] : "", + "client" => isset($_SERVER["HTTP_USER_AGENT"]) ? $_SERVER["HTTP_USER_AGENT"] : "", + "tracking_type" => $type, + "timestamp" => date('Y-m-d H:i:s', time()), + ); + + // log it to db + $dbObj->addLog($trackingID, $data); +} else { + echo "Powerful you have become, the dark side I sense in you."; +} \ No newline at end of file diff --git a/emailtracking/resources/css/emailtracking-styles.css b/emailtracking/resources/css/emailtracking-styles.css new file mode 100644 index 0000000..12ee21f --- /dev/null +++ b/emailtracking/resources/css/emailtracking-styles.css @@ -0,0 +1,11 @@ +.icon_tracking_on { + background-image: url(../icons/ico_tracking_on.png); + background-repeat: no-repeat; + background-position: center; +} + +.icon_tracking_off { + background-image: url(../icons/ico_tracking_off.png); + background-repeat: no-repeat; + background-position: center; +} diff --git a/emailtracking/resources/icons/ico_tracking_off.png b/emailtracking/resources/icons/ico_tracking_off.png new file mode 100644 index 0000000000000000000000000000000000000000..09e5fc5bfd2572430cec28ffc355ee2f6ff6bb8a GIT binary patch literal 465 zcmV;?0WSWDP)FHS!igRKye%6idN6r8qbgoLvO{AM{TUoa&^-MPG1m z@`Gw27^UE1gt{d(MliJ&w9xc%NRXa8-E-h@?zzDL)eCN@7_+ACl}GvSJ|CbuY}O1> zPmn^#oqP^bss)R#1QCfK3KU$={xlXy$>yCVO6|5+(U5f!CbL@OTc}?3l4zfbGe*$0 zsHP2Fd(!N-3g^t2wCcTah`Z3r;xcn64&A6NieXl~7Z5 zP2#7ID4AB|cw!5pU2ZvLg&f)U7*@rssa~CC#FX&21e;WA5!5VgJV&Q7dZ(d*0i@^+wl*&F?Wx5N#+&L>F7 z-8{GBp+Nl`Ku2gt0NFDCz4diXTudis(birLBU<3fx5mwV72! zMXTD?KBBPH5laeOC|RQeIX3n|&WwyTW}>~hEt;28(1E+T=lst3bNC;@KjG=|T0jS| z8`uIY03$#@a4+N!CNt|r2AB=p1v&w=t9W4s8x;MsT|Gl>sQlLVklX{od6h0$Eo zME8Mn9N%?}#PfQBt%e=&rF~ZR>fZG5nWS-=7Qx|Sk1wh~1K78eM zbnZKaVVKN*ixQX^1i-DTINj8R%##4s2LB1IHN|F9eR@d%&4ex_sh2}%n~2}i@+9=OAsk8c6SO=&3& zG)#R7yFSf-Lgq{AD|fQF#Le{VG#Jpi_awe@A1~j%;^x!a1SSU^A-9C5$6F2zXi(AbNg#5u)00SW84-NtcfP^pwsbw3zl1d-c z>Py4-9r({J0L%@?-bqW{1=a}D*horer&dz75F#bAj3r@+5#W5tAH0^S>KAOE1p3Q# RvvB|b002ovPDHLkV1lL;S_A+9 literal 0 HcmV?d00001 diff --git a/emailtracking/resources/icons/ico_trackingxcf.xcf b/emailtracking/resources/icons/ico_trackingxcf.xcf new file mode 100644 index 0000000000000000000000000000000000000000..bc8970a9b90cab3b66d01cd27bf54230fbcb1cdb GIT binary patch literal 2747 zcmeHI&2Jk;6rY)0+nW#5AR35NJ}i;!+ACa|Gnc9YQIRy>L>%CRTz}Qnv7M~5O#s1x z3lbOT-vC!6&X^lj5(#l@a^L_638^6UPytR_?+m{;yK5z?kvJd(5+l90zxR7@-g}-e zH=66++;*j&t2bLUhA|Th;YT5yLp=tuE`7up3C3B73CThpk1@r~BK}EyXBtRbY_69Z zwPItlSp~)5=Pd8{c3QQZ-)S|gg~@!qQR%cgo4LIH6gL}J%ef2ZFX%A_EXJ+^rd}O0 zHSy>E9MIHlzgw;}+l|~}QVZnHI5OsnTkU2q*KAj=)qI-Ysn`8lZ`2^oewd1w(^F|3 z;=w<}5@s07e2_5sJ`xM9W!5YEy;?Q5T;8c|QcM~`i>@G6j45y;#`rK=e2Sql2}$tO z2|RrQ(*`CnXMia$cW94jSN8V)5$@d!PTN|AebAHUpECd(j$&JyQ5^PkWE3_CW@6^rLg ztj}O!tAB9N-&(-xe8!ji;okPUUx)oAY{}Z8x^-#mIcHxTuJO4&rFz>xJU{)8QhRgS zx^cAY-GKE(6dAnr(k*qchMioZEZh6x7q2jb4d#3O+qe7vJc=C~RMucmN4peVe%kt( zlV>mY>>~LEDU5&W7)I5`A459Y@i2E|kUU&ChUT}YIrA5?4KL%QJi{5<3$`O11JJY$ zM|i@KwqrTcb2y>YNNJNh+>WUjWTOCXn*;>>I)f2enu0PCUORQDPJ?_=MH8Jkj;HG{ z*oMu~od7p)qYrfFWdgJt4E^p#AOaA9;Y)&;5lBC<0_g>uP->*K$ph}kR1C6F0QXG- z0)Cyrh%B8HypizQsY7)dJtPz1w`^$j$lTXxa1J1jVPSGXWt zqaX=liYp7QJidf47)?4p`b@=MBk z`7XV2pQ70Bvp0Vn93B7q)mn77k^@W^@NBcvd85^-T-T~rJ>P9N=nRj$e8m6V{{fVf z#<>50