<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0">
  <classes>
    <class id="MailInboxStandard" _delta="define">
      <parent>MailInboxBase</parent>
      <properties>
        <category>grant_by_profile,application</category>
        <abstract>false</abstract>
        <key_type>autoincrement</key_type>
        <db_table>mailinbox_standard</db_table>
        <db_key_field>id</db_key_field>
        <db_final_class_field>realclass</db_final_class_field>
        <naming>
          <format>%1$s</format>
          <attributes>
            <attribute id="login"/>
          </attributes>
        </naming>
        <display_template/>
        <icon>images/mailbox.png</icon>
        <reconciliation>
          <attributes>
            <attribute id="server"/>
            <attribute id="login"/>
            <attribute id="protocol"/>
            <attribute id="mailbox"/>
            <attribute id="port"/>
          </attributes>
        </reconciliation>
      </properties>
      <fields>
        <field id="behavior" xsi:type="AttributeEnum">
          <values>
            <value>create_only</value>
            <value>update_only</value>
            <value>both</value>
          </values>
          <sql>behavior</sql>
          <default_value>both</default_value>
          <is_null_allowed>false</is_null_allowed>
        </field>
        <field id="target_class" xsi:type="AttributeEnum">
          <values>
            <value>Incident</value>
            <value>UserRequest</value>
            <value>Change</value>
            <value>RoutineChange</value>
            <value>NormalChange</value>
            <value>EmergencyChange</value>
            <value>Problem</value>
          </values>
          <sql>target_class</sql>
          <default_value>UserRequest</default_value>
          <is_null_allowed>false</is_null_allowed>
        </field>
        <field id="ticket_default_values" xsi:type="AttributeText">
          <sql>ticket_default_values</sql>
          <default_value/>
          <is_null_allowed>true</is_null_allowed>
        </field>
        <field id="ticket_default_title" xsi:type="AttributeString">
          <sql>ticket_default_title</sql>
          <default_value/>
          <is_null_allowed>true</is_null_allowed>
        </field>
        <field id="title_pattern" xsi:type="AttributeString">
          <sql>title_pattern</sql>
          <default_value/>
          <is_null_allowed>true</is_null_allowed>
        </field>
        <field id="unknown_caller_behavior" xsi:type="AttributeEnum">
          <values>
            <value>create_contact</value>
            <value>reject_email</value>
          </values>
          <sql>unknown_caller_behavior</sql>
          <default_value>reject_email</default_value>
          <is_null_allowed>false</is_null_allowed>
        </field>
        <field id="unknown_caller_rejection_reply" xsi:type="AttributeText">
          <sql>unknown_caller_rejection_reply</sql>
          <default_value/>
          <is_null_allowed>true</is_null_allowed>
        </field>
        <field id="caller_default_values" xsi:type="AttributeText">
          <sql>caller_default_values</sql>
          <default_value/>
          <is_null_allowed>true</is_null_allowed>
        </field>
        <field id="error_behavior" xsi:type="AttributeEnum">
          <values>
            <value>delete</value>
            <value>mark_as_error</value>
          </values>
          <sql>error_behavior</sql>
          <default_value>mark_as_error</default_value>
          <is_null_allowed>false</is_null_allowed>
        </field>
        <field id="notify_errors_to" xsi:type="AttributeEmailAddress">
          <sql>notify_errors_to</sql>
          <default_value/>
          <is_null_allowed>true</is_null_allowed>
        </field>
        <field id="notify_errors_from" xsi:type="AttributeEmailAddress">
          <sql>notify_errors_from</sql>
          <default_value/>
          <is_null_allowed>true</is_null_allowed>
        </field>
        <field id="trace" xsi:type="AttributeEnum">
          <values>
            <value>yes</value>
            <value>no</value>
          </values>
          <sql>trace</sql>
          <default_value>no</default_value>
          <is_null_allowed>false</is_null_allowed>
        </field>
        <field id="debug_trace" xsi:type="AttributeLongText">
          <sql>debug_trace</sql>
          <default_value/>
          <is_null_allowed>true</is_null_allowed>
        </field>
        <field id="email_storage" xsi:type="AttributeEnum">
          <values>
            <value>keep</value>
            <value>delete</value>
            <value>move</value>
          </values>
          <sql>email_storage</sql>
          <default_value>keep</default_value>
          <is_null_allowed>false</is_null_allowed>
        </field>
        <field id="target_folder" xsi:type="AttributeString">
          <sql>target_folder</sql>
          <default_value/>
          <is_null_allowed>true</is_null_allowed>
        </field>
        <field id="import_additional_contacts" xsi:type="AttributeEnum">
          <values>
            <value>never</value>
            <value>only_on_creation</value>
            <value>only_on_update</value>
            <value>always</value>
          </values>
          <sql>import_additional_contacts</sql>
          <default_value>never</default_value>
          <is_null_allowed>false</is_null_allowed>
        </field>
        <field id="stimuli" xsi:type="AttributeText">
          <sql>stimuli</sql>
          <default_value/>
          <is_null_allowed>true</is_null_allowed>
        </field>
      </fields>
      <methods>
        <method id="DisplayBareRelations">
          <comment>/**
	 * Add an extra tab showing the debug trace
	 * @see cmdbAbstractObject::DisplayBareRelations()
	 */</comment>
          <static>false</static>
          <access>public</access>
          <type>Overload-cmdbAbstractObject</type>
          <code><![CDATA[	function DisplayBareRelations(WebPage $oPage, $bEditMode = false)
	{
		parent::DisplayBareRelations($oPage, $bEditMode);
		if (!$bEditMode)
		{
			$oPage->SetCurrentTab(Dict::S('MailInboxStandard:DebugTrace'));
			$sAjaxUrl = addslashes(utils::GetAbsoluteUrlModulesRoot().basename(dirname(__FILE__)).'/ajax.php');
			$iId = $this->GetKey();
			if ($this->Get('trace') == 'yes')
			{
				$oPage->add('<p><button type="button" class="ibo-button ibo-is-regular ibo-is-neutral" id="debug_trace_refresh">'.Dict::S('UI:Button:Refresh').'</button></p>');
				$oPage->add('<div id="debug_trace_output"></div>');
				$oPage->add_ready_script(
<<<EOF
$('#debug_trace_refresh').click(function() {
	$('#debug_trace_output').html('<img src="../images/indicator.gif"/>');
	$.post('$sAjaxUrl', {operation: 'debug_trace', id: $iId }, function(data) {
		$('#debug_trace_output').html(data);
	});
});
$('#debug_trace_refresh').trigger('click');
EOF
				);
			}
			else
			{
				$oPage->add('<div id="debug_trace_output"><p>'.Dict::S('MailInboxStandard:DebugTraceNotActive').'</p></div>');
			}
		}
	}]]></code>
        </method>
        <method id="Trace">
          <comment>/**
	 * Debug trace: activated/disabled by the configuration flag set for the base module...
	 * @param string $sText
	 */</comment>
          <static>false</static>
          <access>public</access>
          <type>Overload-cmdbAbstractObject</type>
          <code><![CDATA[	public function Trace($sText)
	{
		parent::Trace($sText);
		$iMaxTraceLength = 500*1024; // Maximum size of the Trace to keep in the database...
		
		if ($this->Get('trace') == 'yes')
		{
			$sStampedText = date('Y-m-d H:i:s').' - '.$sText."\n";
			$this->Set('debug_trace', mb_substr($this->Get('debug_trace').$sStampedText, -$iMaxTraceLength));
			$this->DBUpdate();
		}
	}]]></code>
        </method>
        <method id="RecordAttChanges">
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	protected function RecordAttChanges(array $aValues, array $aOrigValues)
	{
		// Do NOT record the changes on the 'debug trace' attribute
		unset($aValues['debug_trace']);
		parent::RecordAttChanges($aValues, $aOrigValues);
	}]]></code>
        </method>
        <method id="DispatchEmail">
          <comment>/**
	 * Initial dispatching of an incoming email: determines what to do with the email
	 * @param EmailReplica $oEmailReplica The EmailReplica associated with the email. A new replica (i.e. not yet in DB) one for new emails
	 * @return int An action code from EmailProcessor
	 */</comment>
          <static>false</static>
          <access>public</access>
          <type>Overload-DBObject</type>
          <code>	public function DispatchEmail(EmailReplica $oEmailReplica)
	{
		return parent::DispatchEmail($oEmailReplica);
	}</code>
        </method>
        <method id="ProcessNewEmail">
          <comment>/**
	 * Process an new (unread) incoming email
	 * @param EmailSource $oSource The source from which this email was read
	 * @param int $index The index of the message in the source
	 * @param EmailMessage $oEmail The decoded email
	 * @return Ticket The ticket created or updated in response to the email
	 */</comment>
          <static>false</static>
          <access>public</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	public function ProcessNewEmail(EmailSource $oSource, $index, EmailMessage $oEmail)
	{
	    $this->sLastError = null;
		$this->Trace("Processing new eMail (index = $index)");
		$oTicket = null;
		if ($this->IsUndesired($oEmail))
		{
			$this->HandleError($oEmail, 'undesired_message', $oEmail->oRawEmail);
			return null;
		}

		try
		{
			// Search if the caller email is an existing contact in iTop
			$oCaller = $this->FindCaller($oEmail);
			if ($oCaller === null)
			{
				// Cannot create/update a ticket if the caller is unknown
				return null;
			}

			// Check whether we need to create a new ticket or to update an existing one
			$oTicket = $this->GetRelatedTicket($oEmail);

			switch($this->Get('behavior'))
			{
				case 'create_only':
				$oTicket = $this->CreateTicketFromEmail($oEmail, $oCaller);
				break;

				case 'update_only':
				if (!is_object($oTicket))
				{
					// No ticket associated with the incoming email, nothing to update, reject the email
					$this->HandleError($oEmail, 'nothing_to_update', $oEmail->oRawEmail);
				}
				else
				{
					// Update the ticket with the incoming eMail
					$this->UpdateTicketFromEmail($oTicket, $oEmail, $oCaller);
				}
				break;

				default: // both: update or create as needed
				if (!is_object($oTicket))
				{
					// Let's create a new ticket
					$oTicket = $this->CreateTicketFromEmail($oEmail, $oCaller);
				}
				else
				{
					// Update the ticket with the incoming eMail
					$this->UpdateTicketFromEmail($oTicket, $oEmail, $oCaller);
				}
				break;
			}

			return $oTicket;
		}
		catch(Exception $e)
		{
			$sExceptionMessage = $e->getMessage();
			$sExceptionStack = $e->getTraceAsString();
			$this->Trace("ERROR occurred during mail processing : $sExceptionMessage\n$sExceptionStack");
			$this->HandleError($oEmail, 'error during processing', $oEmail->oRawEmail);
		}
	}]]></code>
        </method>
        <method id="FindCaller">
          <comment>/**
	 * Search if the caller email is an existing contact in iTop, if not may create it
	 * depending on the mailinbox setting.
	 * {@inheritDoc}
	 * @see MailInboxBase::FindCaller()
	 */</comment>
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	protected function FindCaller(EmailMessage $oEmail)
	{
		$oCaller = null;
		$sContactQuery = 'SELECT Person WHERE email = :email';
		$oSet = new DBObjectSet(DBObjectSearch::FromOQL($sContactQuery), array(), array('email' => $oEmail->sCallerEmail));
		$sAdditionalDescription = '';
		switch($oSet->Count())
		{
			case 1:
			// Ok, the caller was found in iTop
			$oCaller = $oSet->Fetch();
			break;
			
			case 0:
			switch($this->Get('unknown_caller_behavior'))
			{
				case 'reject_email':
				$this->Trace('No contact found for the email address "'.$oEmail->sCallerEmail.'", the email will not be processed');
				$this->HandleError($oEmail, 'unknown_contact', $oEmail->oRawEmail);
				return null;
				break;
				
				case 'create_contact':
				default:
				$this->Trace("Creating a new Person for the email: {$oEmail->sCallerEmail}");
				$oCaller = new Person();
				$oCaller->Set('email', $oEmail->sCallerEmail);
				$sDefaultValues = $this->Get('caller_default_values');
				$aDefaults = explode("\n", $sDefaultValues);
				$aDefaultValues = array();
				foreach($aDefaults as $sLine)
				{
					if (preg_match('/^([^:]+):(.*)$/', $sLine, $aMatches))
					{
						$sAttCode = trim($aMatches[1]);
						$sValue = trim($aMatches[2]);
						$aDefaultValues[$sAttCode] = $sValue;
					}
				}
				$this->InitObjectFromDefaultValues($oCaller, $aDefaultValues);
				try
				{
					$oCaller->DBInsert();
				}
				catch(Exception $e)
				{
					$this->Trace('Failed to create a Person for the email address "'.$oEmail->sCallerEmail.'".');
					$this->Trace($e->getMessage());
					$this->HandleError($oEmail, 'failed_to_create_contact', $oEmail->oRawEmail);
					return null;
				}
				
			}			
			break;
			
			default:
			$this->Trace('Found '.$oSet->Count().' callers with the same email address "'.$oEmail->sCallerEmail.'", the first one will be used...');
			// Multiple callers with the same email address !!!
			$oCaller = $oSet->Fetch();
		}
		
		return $oCaller;
	}]]></code>
        </method>
        <method id="GetRelatedTicket">
          <comment>/**
	 * 
	 * {@inheritDoc}
	 * @see MailInboxBase::GetRelatedTicket()
	 */</comment>
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	protected function GetRelatedTicket(EmailMessage $oEmail)
	{
		// First check if there is any iTop object mentioned in the headers of the eMail
		$oTicket = parent::GetRelatedTicket($oEmail);

		if (!is_null($oTicket)){
		  return $oTicket;
		}

		if (MetaModel::GetModuleSetting('itop-standard-email-synchro', 'aggregate_replies', true) === true) {
			if (array_key_exists('in-reply-to', $oEmail->aHeaders)) {
				$sInReplyToMsgId = $oEmail->aHeaders['in-reply-to'];
				$sOQL = "SELECT EmailReplica WHERE message_id = :in_reply_to";
				$oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array(), array('in_reply_to' => $sInReplyToMsgId));
				$oSet->OptimizeColumnLoad(['EmailReplica'=>['ticket_id']]);
				$iEmailReplicaCount = $oSet->Count();
				if ($iEmailReplicaCount > 0){
					/** @var EmailReplica $oEmailReplica */
					$oEmailReplica = $oSet->Fetch();
					if ($iEmailReplicaCount > 1) {
						$this->Trace("iTop Simple Email Synchro: WARNING the message ({$oEmail->sUIDL}) replies to the message_id=".$sInReplyToMsgId."
						This message_id corresponds to $iEmailReplicaCount email replica in iTop.");
					}
					$oTicket = MetaModel::GetObject('Ticket', $oEmailReplica->Get('ticket_id'), false);
					if (is_null($oTicket)) {
						$this->Trace("iTop Simple Email Synchro: WARNING the message ({$oEmail->sUIDL}) got a EmailReplica#{$oEmailReplica->GetKey()}
						with the ticket_id={$oEmailReplica->Get('ticket_id')} but the ticket does not exist.");
					} elseif (MetaModel::IsValidAttCode(get_class($oTicket), "operational_status") && $oTicket->Get("operational_status") === "closed") {
						$this->Trace("iTop Simple Email Synchro: WARNING the message ({$oEmail->sUIDL}) is related to the CLOSED (operational_status) ticket {$oTicket->Get('id')}");
						return null;
					} elseif (MetaModel::IsValidAttCode(get_class($oTicket), "status") && $oTicket->Get("status") === "closed") {
						$this->Trace("iTop Simple Email Synchro: WARNING the message ({$oEmail->sUIDL}) is related to the CLOSED (status) ticket {$oTicket->Get('id')}");
						return null;
					}
					else {
					  return $oTicket;
					}
				}
			}
		}

			// No associated ticket found by parsing the headers, check
			// if the subject does not match a specific pattern
			$sPattern = $this->FixPattern($this->Get('title_pattern'));
			if(($sPattern != '') && (preg_match($sPattern, $oEmail->sSubject, $aMatches)))
			{
				$oFilter = DBSearch::FromOQL('SELECT Ticket WHERE ref = :ref');
				foreach ($aMatches as $sMatch)
				{
					if (!empty($sMatch))
					{
						$this->Trace("iTop Simple Email Synchro: Retrieving ticket $sMatch (match by subject pattern)...");
						$oSet = new DBObjectSet($oFilter, array(), array('ref' => $sMatch));
						$oTicket = $oSet->Fetch();
						if ($oTicket)
						{
							break;
						}
				    }
                }
			}

		return $oTicket;
	}]]></code>
        </method>
        <method id="CreateTicketFromEmail">
          <comment><![CDATA[/**
	 * Actual creation of the ticket from the incoming email. Overload this method
	 * to implement your own behavior, if needed
	 *
	 * @param EmailMessage $oEmail The decoded incoming email
	 * @param Contact $oCaller The contact corresponding to the "From" email address
	 *
	 * @return Ticket the created ticket or null in case of failure
	 * @throws \Exception if invalid target_class
	 */]]></comment>
          <static>false</static>
          <access>public</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	public function CreateTicketFromEmail(EmailMessage $oEmail, Contact $oCaller)
	{
		// In case of error (exception...) set the behavior
		if ($this->Get('error_behavior') == 'delete')
		{
			$this->SetNextAction(EmailProcessor::DELETE_MESSAGE); // Remove the message from the mailbox
		}
		else
		{
			$this->SetNextAction(EmailProcessor::MARK_MESSAGE_AS_ERROR); // Keep the message in the mailbox, but marked as error
		}
		$this->Trace("Creating a new Ticket from eMail '".$oEmail->sSubject."'");
		if (!MetaModel::IsValidClass($this->Get('target_class')))
		{
			throw new Exception('Invalid "ticket_class" configured: "'.$this->Get('target_class').'" is not a valid class. Cannot create such an object.');
		}
		
		$iCurrentUserId = UserRights::GetUserId();
    $bInsertChangeUserId = false;
    // Set changes author to mail author and create a new Change
    if(method_exists('UserRights', 'GetUserFromPerson') && $oCaller !== null){
      $oUser = UserRights::GetUserFromPerson($oCaller, true);
      if($oUser !== null){
            CMDBObject::SetTrackUserId($oUser->GetKey());
            $oCMDBChange = CMDBObject::GetCurrentChange();
            CMDBObject::SetCurrentChange(null);
            $bInsertChangeUserId = true;
      }
    }
		
		$oTicket = MetaModel::NewObject($this->Get('target_class'));
		$oTicket->Set('org_id', $oCaller->Get('org_id'));
		if (MetaModel::IsValidAttCode(get_class($oTicket), 'caller_id'))
		{
			$oTicket->Set('caller_id', $oCaller->GetKey());
		}
		if (MetaModel::IsValidAttCode(get_class($oTicket), 'origin'))
		{
			$oTicket->Set('origin', 'mail');
		}
		if ($oEmail->sSubject == '')
		{
			$sDefaultSubject = ($this->Get('ticket_default_title') == '') ? Dict::S('MailInbox:NoSubject') : $this->Get('ticket_default_title');
			$this->Trace("The incoming email has no subject, the ticket's title will be set to: '$sDefaultSubject'");
			$oTicket->Set('title', $sDefaultSubject);
		}
		else
		{
			$oTicketTitleAttDef = MetaModel::GetAttributeDef(get_class($oTicket), 'title');
			$iTitleMaxSize = $oTicketTitleAttDef->GetMaxSize();
			$oTicket->Set('title', substr($oEmail->sSubject, 0, $iTitleMaxSize));
		}

		$aIgnoredAttachments = array();

		// Insert the remaining attachments so that we know their ID and can reference them in the message's body
		$aAddedAttachments = $this->AddAttachments($oTicket, $oEmail, true, $aIgnoredAttachments, $oCaller, $oUser);  // Cannot insert them for real since the Ticket is not saved yet (we don't know its ID)
																									// we'll have to call UpdateAttachments once the ticket is properly saved
		$oTicketDescriptionAttDef = MetaModel::GetAttributeDef(get_class($oTicket), 'description');
		$bForPlainText = true; // Target format is plain text (by default)
		if ($oTicketDescriptionAttDef instanceof AttributeHTML)
		{
			// Target format is HTML
			$bForPlainText = false;
		}
		else if ($oTicketDescriptionAttDef instanceof AttributeText)
		{
			$aParams = $oTicketDescriptionAttDef->GetParams();
			if (array_key_exists('format', $aParams) && ($aParams['format'] == 'html'))
			{
				// Target format is HTML
				$bForPlainText = false;
			}
		}
		$this->Trace("Target format for 'description': ".($bForPlainText ? 'text/plain' : 'text/html'));
		$this->Trace("Email body format: ".$oEmail->sBodyFormat);

		$sTicketDescription = $this->BuildDescription($oEmail, $aAddedAttachments, $aIgnoredAttachments, $bForPlainText);

		$iDescriptionMaxSize = $oTicketDescriptionAttDef->GetMaxSize() - 1000; // Keep some room just in case...
	  if (mb_strlen($sTicketDescription) > $iDescriptionMaxSize)
		{
      $sMsg = "CreateTicketFromEmail: Truncated description for [{$oTicket->Get('title')}] actual length: ".mb_strlen($sTicketDescription)." maximum: $iDescriptionMaxSize";
      IssueLog::Error($sMsg);
      $this->Trace($sMsg);
		  // This has no effect, attachments have already been processed
			$oEmail->aAttachments[] = array('content' => $sTicketDescription, 'filename' => ($bForPlainText ? 'original message.txt' : 'original message.html'), 'mimeType' => ($bForPlainText ? 'text/plain' : 'text/html'));
		}

		$oTicket->Set('description', $this->FitTextIn($sTicketDescription, $iDescriptionMaxSize));

		// Default values
		$sDefaultValues = $this->Get('ticket_default_values');
		$aDefaults = explode("\n", $sDefaultValues);
		$aDefaultValues = array();
		foreach($aDefaults as $sLine)
		{
			if (preg_match('/^([^:]+):(.*)$/', $sLine, $aMatches))
			{
				$sAttCode = trim($aMatches[1]);
				$sValue = trim($aMatches[2]);
				$aDefaultValues[$sAttCode] = $sValue;
			}
		}
		$this->InitObjectFromDefaultValues($oTicket, $aDefaultValues);

		if (($this->Get('import_additional_contacts') == 'always') || ($this->Get('import_additional_contacts') == 'only_on_creation'))
		{
			$this->AddAdditionalContacts($oTicket, $oEmail);
		}

		$this->BeforeInsertTicket($oTicket, $oEmail, $oCaller);
		$oTicket->DBInsert();
		$this->Trace("Ticket ".$oTicket->GetName()." created.");
		$this->AfterInsertTicket($oTicket, $oEmail, $oCaller, $aAddedAttachments);

    // Restore previous change as current change
    if($bInsertChangeUserId === true){
      CMDBObject::SetTrackUserId($iCurrentUserId);
      CMDBObject::SetCurrentChange($oCMDBChange);
    }

		return $oTicket;
	}]]></code>
        </method>
        <method id="BuildDescription">
          <comment><![CDATA[/**
	 * Build the 'description' of the ticket when creating a new ticket
	 * @param EmailMessage $oEmail The incoming Email
	 * @param bool $bForPlainText True if the desired output format is plain text, false if HTML
	 * @return string
	 */]]></comment>
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	protected function BuildDescription(EmailMessage $oEmail, $aAddedAttachments, $aIgnoredAttachments, $bForPlainText)
	{
		$sTicketDescription = '';
		if ($oEmail->sBodyFormat == 'text/html')
		{
			// Original message is in HTML
			$this->Trace("Managing inline images...");
			$sTicketDescription = $this->ManageInlineImages($oEmail->sBodyText, $aAddedAttachments, $aIgnoredAttachments, $bForPlainText);
			if ($bForPlainText)
			{
				$this->Trace("Converting HTML to text using utils::HtmlToText...");
				$sTicketDescription = utils::HtmlToText($oEmail->sBodyText);
			}
		}
		else
		{
			// Original message is in plain text
			$sTicketDescription = utils::TextToHtml($oEmail->sBodyText);
			if (!$bForPlainText)
			{
				$this->Trace("Converting text to HTML using utils::TextToHtml...");
				$sTicketDescription = utils::TextToHtml($oEmail->sBodyText);
			}
		}

		if (empty($sTicketDescription))
		{
			$sTicketDescription = 'No description provided.';
		}
		
		return $sTicketDescription;
	}]]></code>
        </method>
        <method id="AddAdditionalContacts">
          <comment>/**
	 * Add the contacts in To: or CC: as additional contacts to the ticket (if they exist in the DB)
	 * @param Ticket $oTicket
	 * @param EmailMessage $oEmail
	 */</comment>
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	protected function AddAdditionalContacts(Ticket $oTicket, EmailMessage $oEmail)
	{
		$oContactsSet = $oTicket->Get('contacts_list');
		$aExistingContacts = array();
		while($oLnk = $oContactsSet->Fetch())
		{
			$aExistingContacts[$oLnk->Get('contact_id')] = true;
		}
		$aAdditionalContacts = array_merge($oEmail->aTos, $oEmail->aCCs); // Take both the To: and CC:, no actual merge since the keys of the arrays are numeric (i.e. 0, 1, 2...)
		foreach($aAdditionalContacts as $aInfo)
		{
			$sCallerEmail = $oTicket->Get('caller_id->email');
			// Exclude the caller from the additional contacts
			if ($aInfo['email'] != $sCallerEmail && $aInfo['email'] != $this->Get('login'))
			{
				$oContact = $this->FindAdditionalContact($aInfo['email']);
				if (($oContact != null) && !array_key_exists($oContact->GetKey(), $aExistingContacts))
				{
					$oLnk = new lnkContactToTicket();
					$oLnk->Set('contact_id', $oContact->GetKey());
					$oContactsSet->AddItem($oLnk);
				}
			}
			else
			{
				$this->Trace('Skipping "'.$aInfo['email'].'" from the email address in To/CC since it is the same as the caller\'s email or mailbox\'s email.');
			}
		}
		$oTicket->Set('contacts_list', $oContactsSet);
	}]]></code>
        </method>
        <method id="FindAdditionalContact">
          <comment>/**
	 * Search if the CC email is an existing contact in iTop, if so return it, otherwise ignore it
	 * @param $sEmail string The email address to seach
	 * @return Contact | null
	 */</comment>
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	protected function FindAdditionalContact($sEmail)
	{
		$oContact = null;
		$sContactQuery = 'SELECT Contact WHERE email = :email';
		$oSet = new DBObjectSet(DBObjectSearch::FromOQL($sContactQuery), array(), array('email' => $sEmail));
		switch($oSet->Count())
		{
			case 1:
			// Ok, the caller was found in iTop
			$oContact = $oSet->Fetch();
			$this->Trace('Found Contact::'.$oContact->GetKey().' ('.$oContact->GetName().') from the email address in To/CC "'.$sEmail.'".');
			break;
			
			case 0:
			$this->Trace('No contact found with the email address in To/CC "'.$sEmail.'", email address ignored.');
			break;
			
			default:
			$this->Trace('Found '.$oSet->Count().' contacts with the same email address in To/CC "'.$sEmail.'", the first one will be used...');
			// Multiple contacts with the same email address !!!
			$oContact = $oSet->Fetch();
		}
		return $oContact;
	}]]></code>
        </method>
        <method id="BeforeInsertTicket">
          <comment>/**
	 * Handler called just before inserting the ticket into the database
	 * Overload this method to adjust the values of the ticket at your will
	 * @param Ticket $oTicket
	 * @param EmailMessage $oEmail
	 * @param Contact $oCaller
	 */</comment>
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code>	protected function BeforeInsertTicket(Ticket $oTicket, EmailMessage $oEmail, Contact $oCaller)
	{
		// Default implementation: do nothing
	}</code>
        </method>
        <method id="AfterInsertTicket">
          <comment>/**
	 * Finalize the processing after the insertion of the ticket in the database
	 * @param Ticket $oTicket The ticket being written
	 * @param EmailMessage $oEmail The source email
	 * @param Contact $oCaller The caller for this ticket, as passed to CreateTicket
	 * @param array $aAddedAttachments The array of attachments added to the ticket
	 */</comment>
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	protected function AfterInsertTicket(Ticket $oTicket, EmailMessage $oEmail, Contact $oCaller, $aAddedAttachments)
	{
		// Process attachments
		$this->UpdateAttachments($aAddedAttachments, $oTicket); // Now update the attachments since we know the ID of the ticket
		
		// Shall we delete the source email immediately?
		if ($this->Get('email_storage') == 'delete')
		{
			// Remove the processed message from the mailbox
			$this->Trace("Ticket created, deleting the source eMail '".$oEmail->sSubject."'");
			$this->SetNextAction(EmailProcessor::DELETE_MESSAGE);		
		}
		else	if ($this->Get('email_storage') == 'move')
		{
			// Remove the processed message from the mailbox
			$this->Trace("Ticket created, move the source eMail '".$oEmail->sSubject."'");
			$this->SetNextAction(EmailProcessor::MOVE_MESSAGE);
		}
		else
		{
			// Keep the message in the mailbox
			$this->SetNextAction(EmailProcessor::NO_ACTION);		
		}		
	}]]></code>
        </method>
        <method id="UpdateTicketFromEmail">
          <comment><![CDATA[/**
	 * Actual update of a ticket from the incoming email. Overload this method
	 * to implement your own behavior, if needed
	 * @param Ticket $oTicket The ticket to update
	 * @param EmailMessage $oEmail The decoded incoming email
	 * @param Contact $oCaller The contact corresponding to the "From" email address
	 * @return void
	 */]]></comment>
          <static>false</static>
          <access>public</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	public function UpdateTicketFromEmail(Ticket $oTicket, EmailMessage $oEmail, Contact $oCaller)
	{
		// In case of error (exception...) set the behavior
		if ($this->Get('error_behavior') == 'delete')
		{
			$this->SetNextAction(EmailProcessor::DELETE_MESSAGE); // Remove the message from the mailbox
		}
		else
		{
			$this->SetNextAction(EmailProcessor::MARK_MESSAGE_AS_ERROR); // Keep the message in the mailbox, but marked as error
		}		
		
		// Check that the ticket is of the expected class
		if (!is_a($oTicket, $this->Get('target_class')))
		{
			$this->Trace("iTop Simple Email Synchro: Error: the incoming email refers to the ticket ".$oTicket->GetName()." of class ".get_class($oTicket).", but this mailbox is configured to process only tickets of class ".$this->Get('target_class'));
			$this->SetNextAction(EmailProcessor::MARK_MESSAGE_AS_ERROR); // Keep the message in the mailbox, but marked as error
			return;
		}
		
		// Try to extract what's new from the message's body
		$this->Trace("iTop Simple Email Synchro: Updating the iTop ticket ".$oTicket->GetName()." from eMail '".$oEmail->sSubject."'");

    $iCurrentUserId = UserRights::GetUserId();
    $bInsertChangeUserId = false;
    // Set changes author to mail author and create a new Change
    if(method_exists('UserRights', 'GetUserFromPerson') && $oCaller !== null){
      $oUser = UserRights::GetUserFromPerson($oCaller, true);
      if($oUser !== null){
            CMDBObject::SetTrackUserId($oUser->GetKey());
            $oCMDBChange = CMDBObject::GetCurrentChange();
            CMDBObject::SetCurrentChange(null);
            $bInsertChangeUserId = true;
      }
    }
		
		// Process attachments
		$aIgnoredAttachments = array();
		$aAddedAttachments = $this->AddAttachments($oTicket, $oEmail, true, $aIgnoredAttachments, $oCaller, $oUser);
		
		$sCaseLogEntry = $this->BuildCaseLogEntry($oEmail, $aAddedAttachments, $aIgnoredAttachments);
		
		$this->Trace($oEmail->sTrace);
		// Write the log on behalf of the caller
		$sCallerName = $oEmail->sCallerName;
		if (empty($sCallerName))
		{
			$sCallerName = $oEmail->sCallerEmail;
		}
					
		// Determine which field to update
		$sAttCode = 'public_log';
		$aAttCodes = MetaModel::GetModuleSetting('itop-standard-email-synchro', 'ticket_log', array('UserRequest' => 'public_log', 'Incident' => 'public_log'));
		if (array_key_exists(get_class($oTicket), $aAttCodes))
		{
			$sAttCode = $aAttCodes[get_class($oTicket)];
		}
		
		$oLog = $oTicket->Get($sAttCode);
		if($bInsertChangeUserId === true){
		  $oLog->AddLogEntry($sCaseLogEntry, $sCallerName, $oUser->GetKey());
		}
		else{
			$oLog->AddLogEntry($sCaseLogEntry, $sCallerName);
    }
		$oTicket->Set($sAttCode, $oLog);
		
		if (($this->Get('import_additional_contacts') == 'always') || ($this->Get('import_additional_contacts') == 'only_on_update'))
		{
			$this->AddAdditionalContacts($oTicket, $oEmail);
		}
		$this->BeforeUpdateTicket($oTicket, $oEmail, $oCaller);
		$oTicket->DBUpdate();			
		$this->Trace("Ticket ".$oTicket->GetName()." updated.");
		$this->AfterUpdateTicket($oTicket, $oEmail, $oCaller);

    // Restore previous change as current change
    if($bInsertChangeUserId === true){
      CMDBObject::SetTrackUserId($iCurrentUserId);
      CMDBObject::SetCurrentChange($oCMDBChange);
    }

		return $oTicket;		
	}]]></code>
        </method>
        <method id="BuildCaseLogEntry">
          <comment>/**
	 * Build the text/html to be inserted in the case log when the ticket is updated
	 * Starting with iTop 2.3.0, the format is always HTML
	 * @param EmailMessage $oEmail
	 * @return string The HTML text to be inserted in the case log
	 */</comment>
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	protected function BuildCaseLogEntry(EmailMessage $oEmail, $aAddedAttachments, $aIgnoredAttachments)
	{
		$sCaseLogEntry = '';
		$this->Trace("Email body format: ".$oEmail->sBodyFormat);
		if ($oEmail->sBodyFormat == 'text/html')
		{
			$this->Trace("Extracting the new part using GetNewPartHTML...");
			$sCaseLogEntry = $oEmail->GetNewPartHTML($oEmail->sBodyText);
			if (strip_tags($sCaseLogEntry) == '')
			{
				// No new part (only blank tags)... we'd better use the whole text of the message
				$sCaseLogEntry = $oEmail->sBodyText;
			}
			$this->Trace("Managing inline images...");
			$sCaseLogEntry = $this->ManageInlineImages($sCaseLogEntry, $aAddedAttachments, $aIgnoredAttachments, false /* $bForPlainText */);
		}
		else
		{
			$this->Trace("Extracting the new part using GetNewPart...");
			$sCaseLogEntry = $oEmail->GetNewPart($oEmail->sBodyText, $oEmail->sBodyFormat); // GetNewPart always returns a plain text version of the message
			$sCaseLogEntry = utils::TextToHtml($sCaseLogEntry);
		}
		return $sCaseLogEntry;
	}]]></code>
        </method>
        <method id="BeforeUpdateTicket">
          <comment>/**
	 * Handler called before updating a ticket in the database
	 * Overload this method to alter the ticket at your will
	 * @param Ticket $oTicket
	 * @param EmailMessage $oEmail
	 * @param Contact $oCaller
	 */</comment>
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code>	protected function BeforeUpdateTicket(Ticket $oTicket, EmailMessage $oEmail, Contact $oCaller)
	{
		// Default implementation: do nothing
	}</code>
        </method>
        <method id="ApplyConfiguredStimulus">
          <comment><![CDATA[/**
	 * Read the configuration in the 'stimuli' field (format: <state_code>:<stimulus_code>, one per line)
	 * and apply the corresponding stimulus according to the current state of the ticket
	 * @param ticket $oTicket
	 */]]></comment>
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	protected function ApplyConfiguredStimulus(ticket $oTicket)
	{
		$sConf = $this->Get('stimuli');
		$aConf = explode("\n", $sConf);
		$aStateToStimulus = array();
		foreach($aConf as $sLine)
		{
			if (preg_match('/^([^:]+):(.*)$/', $sLine, $aMatches))
			{
				$sState = trim($aMatches[1]);
				$sStimulus = trim($aMatches[2]);
				$aStateToStimulus[$sState] = $sStimulus;
			}
			elseif (!empty($sLine))
			{
				$this->Trace('Invalid line in the "stimuli" configuration: "'.$sLine.'". The expected format for each line is <state_code>:<stimulus_code>');
			}
		}
		if (array_key_exists($oTicket->GetState(), $aStateToStimulus))
		{
			$sStimulusCode = $aStateToStimulus[$oTicket->GetState()];
			$this->Trace('About to apply the stimulus: '.$sStimulusCode.' for the ticket in state: '.$oTicket->GetState());

			// Check that applying the stimulus will not break the data integrity constaints (mandatory, must change)
			$aTransitions = $oTicket->EnumTransitions();
			$bCanApplyStimulus = true;
			if (!isset($aTransitions[$sStimulusCode]))
			{
				$bCanApplyStimulus = false;
				$this->Trace('The Stimulus '.$sStimulusCode.' for '.get_class($oTicket).' in state '.$oTicket->GetState().' has no effect (no transition). Ignored.');
			}
			else
			{
				
				$aTransitionDef = $aTransitions[$sStimulusCode];
				$sTargetState = $aTransitionDef['target_state'];
				$aTargetStates = MetaModel::EnumStates(get_class($oTicket));
				$aTargetStateDef = $aTargetStates[$sTargetState];
				$aExpectedAttributes = $aTargetStateDef['attribute_list'];
				foreach($aExpectedAttributes as $sAttCode => $iExpectCode)
				{
					$oAttDef = MetaModel::GetAttributeDef(get_class($oTicket), $sAttCode);
					if (($iExpectCode & OPT_ATT_MANDATORY) && ($oAttDef->IsNull($oTicket->Get($sAttCode))))
					{
						// Check if there is just one possible value, in which case, use it				
						$aArgs = array('this' => $oTicket);
						// If the field is mandatory, set it to the only possible value
						if ($oAttDef->IsExternalKey())
						{
							$oAllowedValues = MetaModel::GetAllowedValuesAsObjectSet(get_class($oTicket), $sAttCode, $aArgs);
							if ($oAllowedValues->Count() == 1)
							{
								$oRemoteObj = $oAllowedValues->Fetch();
								$oTicket->Set($sAttCode, $oRemoteObj->GetKey());
								$this->Trace('Setting the mandatory External Key '.$sAttCode.' to the only allowed value: '.$oRemoteObj->GetKey());
							}
							else
							{
								$this->Trace('Cannot apply the stimulus since the attribute '.$sAttCode.' is mandatory in the target state '.$sTargetState.' and is neither currently set nor has just one allowed value.');
								$bCanApplyStimulus = false;
							}
						}
						else
						{
							$aAllowedValues = MetaModel::GetAllowedValues_att(get_class($oTicket), $sAttCode, $aArgs);
							if (count($aAllowedValues) == 1)
							{
								$aValues = array_keys($aAllowedValues);
								$oTicket->Set($sAttCode, $aValues[0]);
								$this->Trace('Setting the mandatory attribute '.$sAttCode.' to the only allowed value: '.(string)$aValues[0]);
							}
							else
							{
								$this->Trace('Cannot apply the stimulus since the attribute '.$sAttCode.' is mandatory in the target state '.$sTargetState.' and is neither currently set nor has just one allowed value.');
								$bCanApplyStimulus = false;
							}
						}
					}
					if ($iExpectCode & OPT_ATT_MUSTCHANGE)
					{
						$this->Trace('Cannot apply the stimulus since the value of the attribute '.$sAttCode.' must be modified (manually) during the transition to the target state '.$sTargetState.'.');
						$bCanApplyStimulus = false;
					}
				}
			}
			
			if ($bCanApplyStimulus)
			{
				try
				{
					$this->Trace('Actually applying the stimulus: '.$sStimulusCode.' for the ticket in state: '.$oTicket->GetState());
					$oTicket->ApplyStimulus($sStimulusCode);
				}
				catch(Exception $e)
				{
					$this->Trace('ApplyStimulus failed: '.$e->getMessage());
				}
			}
			else
			{
				$this->Trace('ApplyStimulus ignored.');
			}
		}
	}]]></code>
        </method>
        <method id="AfterUpdateTicket">
          <comment>/**
	 * Finalize the processing after the update of the ticket in the database
	 * @param Ticket $oTicket The ticket being written
	 * @param EmailMessage $oEmail The source email
	 * @param Contact $oCaller The caller for this ticket, as passed to UpdateTicket
	 */</comment>
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	protected function AfterUpdateTicket(Ticket $oTicket, EmailMessage $oEmail, Contact $oCaller)
	{
		// If there are any TriggerOnMailUpdate defined, let's activate them
		$aClasses = MetaModel::EnumParentClasses(get_class($oTicket), ENUM_PARENT_CLASSES_ALL);
		$sClassList = implode(", ", CMDBSource::Quote($aClasses));
		$oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnMailUpdate AS t WHERE t.target_class IN ($sClassList)"));
		while ($oTrigger = $oSet->Fetch())
		{
			$oTrigger->DoActivate($oTicket->ToArgs('this'));
		}

		// Apply a stimulus if needed, will write the ticket to the database, may launch triggers, etc...
		$this->ApplyConfiguredStimulus($oTicket);
		
		// Shall we keep the email or delete it immediately?
		if ($this->Get('email_storage') == 'delete')	{
			// Remove the processed message from the mailbox
			$this->Trace("Ticket updated, deleting the source eMail '".$oEmail->sSubject."'");
			$this->SetNextAction(EmailProcessor::DELETE_MESSAGE);		
		} else	if ($this->Get('email_storage') == 'move')	{
			// Remove the processed message from the mailbox
			$this->Trace("Ticket created, move the source eMail '".$oEmail->sSubject."'");
			$this->SetNextAction(EmailProcessor::MOVE_MESSAGE);
		} else {
			// Keep the message in the mailbox
			$this->SetNextAction(EmailProcessor::NO_ACTION);		
		}
	}]]></code>
        </method>
        <method id="ManageInlineImages">
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	protected function ManageInlineImages($sBodyText, $aAddedAttachments, $aIgnoredAttachments, $bForPlainText = true)
	{
		// Search for inline images: i.e. <img tags containing an src="cid:...." or without double quotes e.g. src=cid:xyzxyzx
		// Note: (?: ... ) is used for grouping the alternative without creating a "matching group"
		if (preg_match_all('/<img[^>]+src=(?:"cid:([^"]+)"|cid:([^ >]+))[^>]*>/i', $sBodyText, $aMatches, PREG_OFFSET_CAPTURE))
		{
			$aInlineImages = array();
			foreach ($aMatches[0] as $idx => $aInfo)
			{
				$aInlineImages[$idx] = array(
					'position' => $aInfo[1]
				);
			}
			foreach ($aMatches[1] as $idx => $aInfo)
			{
				$sCID = $aInfo[0];
				if (!array_key_exists($sCID, $aAddedAttachments) && !array_key_exists($sCID, $aIgnoredAttachments))
				{
					$this->Trace("Info: inline image: $sCID not found as an attachment. Ignored.");
				}
				else if (array_key_exists($sCID, $aAddedAttachments))
				{
					$aInlineImages[$idx]['cid'] = $sCID;
					$this->Trace("Inline image cid:$sCID stored as ".get_class($aAddedAttachments[$sCID])."::".$aAddedAttachments[$sCID]->GetKey());
				}
			}
			if (!defined('ATTACHMENT_DOWNLOAD_URL'))
			{
				define('ATTACHMENT_DOWNLOAD_URL', 'pages/ajax.render.php?operation=download_document&class=Attachment&field=contents&id=');
			}
			if ($bForPlainText)
			{
				// The target form is text/plain, so the HTML tags will be stripped
				// Insert the URLs to the attachments, just before the <img tag so that the hyperlink remains (as plain text) at the right position
				// when the HTML tags will be stripped
				// Start from the end of the text to preserve the positions of the <img tags AFTER the insertion
				$sWholeText = $sBodyText;
				$idx = count($aInlineImages);
				while ($idx > 0)
				{
					$idx --;
					if (array_key_exists('cid', $aInlineImages[$idx]))
					{
						$sBefore = substr($sWholeText, 0, $aInlineImages[$idx]['position']);
						$sAfter = substr($sWholeText, $aInlineImages[$idx]['position']);
						$oAttachment = $aAddedAttachments[$aInlineImages[$idx]['cid']];
						$sUrl = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL.$oAttachment->GetKey();
						$sWholeText = $sBefore.' '.$sUrl.' '. $sAfter;
					}
				}
			}
			else
			{
				// The target format is text/html, keep the formatting, but just change the URLs
				$aSearches = array();
				$aReplacements = array();
				foreach($aAddedAttachments as $sCID => $oAttachment)
				{
					$aSearches[] = 'src="cid:'.$sCID.'"';
					if (class_exists('InlineImage') && ($oAttachment instanceof InlineImage))
					{
						// Inline images have a special download URL requiring the 'secret' token
						$aReplacements[] = 'src="'.utils::GetAbsoluteUrlAppRoot().INLINEIMAGE_DOWNLOAD_URL.$oAttachment->GetKey().'&s='.$oAttachment->Get('secret').'"';
					}
					else
					{
						$aReplacements[] = 'src="'.utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL.$oAttachment->GetKey().'"';
					}
					$aSearches[] = 'src=cid:'.$sCID; // Same without quotes
					if (class_exists('InlineImage') && ($oAttachment instanceof InlineImage))
					{
						// Inline images have a special download URL requiring the 'secret' token
						$aReplacements[] = 'src="'.utils::GetAbsoluteUrlAppRoot().INLINEIMAGE_DOWNLOAD_URL.$oAttachment->GetKey().'&s='.$oAttachment->Get('secret').'" '; // Beware we need to add a space at the end
					}
					else
					{
						$aReplacements[] = 'src="'.utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL.$oAttachment->GetKey().'" '; // Beware we need to add a space at the end
					}
				}
				$sWholeText = str_replace($aSearches, $aReplacements, $sBodyText);
			}
			$sBodyText = $sWholeText;
		}
		else
		{
			$this->Trace("Inline Images: no inline-image found in the message");
		}
		return $sBodyText;
	}]]></code>
        </method>
        <method id="IsUndesired">
          <comment><![CDATA[/**
	 * Check (based on a set of patterns tested against the subject of the email) if the email is considered as "undesired"
	 * @param EmailMessage $oEmail The message to check
	 * @return boolean
	 */]]></comment>
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	protected function IsUndesired(EmailMessage $oEmail)
	{
		$bRet = false; 
		$aUndesiredSubjectPatterns = MetaModel::GetModuleSetting('combodo-email-synchro', 'undesired-subject-patterns', array());
		foreach($aUndesiredSubjectPatterns as $sPattern)
		{
			if (preg_match($sPattern, $oEmail->sSubject))
			{
				$this->Trace("The message '{$oEmail->sSubject}' IS considered as undesired, since it matches '$sPattern'.");
				return true;
			}
		}
		$this->Trace("The message '{$oEmail->sSubject}' is NOT considered as undesired.");
		return false; // No match, the message is NOT undesired
	}]]></code>
        </method>
        <method id="HandleError">
          <comment>/**
	 * Error handler... what to do in case of error ??
	 * @param EmailMessage $oEmail can be null in case of decoding error (like message too big)
	 * @param string $sErrorCode
	 * @return void
	 */</comment>
          <static>false</static>
          <access>public</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	public function HandleError($oEmail, $sErrorCode, $oRawEmail = null, $sAdditionalErrorMessage = '')
	{
		$sTo = $this->Get('notify_errors_to');
		$sFrom = $this->Get('notify_errors_from');
		// The behavior is overriden in case of undesired_message
		if ($this->Get('error_behavior') == 'delete')
		{
			$this->SetNextAction(EmailProcessor::DELETE_MESSAGE); // Remove the message from the mailbox
			$sLastAction = "<p>The eMail was deleted from the mailbox.</p>\n";
		}
		else
		{
			$this->SetNextAction(EmailProcessor::MARK_MESSAGE_AS_ERROR); // Keep the message in the mailbox, but marked as error
			$sLastAction = "<p>The eMail is marked as error and will be ignored in further processing of the mailbox.</p>\n";
		}
		$sBody = "";
		switch($sErrorCode)
		{
			case 'unknown_contact':
			// Reject the message because of an unknown caller
			$this->sLastError = "Unknown caller (".$oEmail->sCallerEmail.")";

			// if rejection reply
			$sRejectionReply = $this->Get('unknown_caller_rejection_reply');
			if (!empty($sRejectionReply) && $oRawEmail)
			{
			    $sReplySubject = '[iTop] '.$oEmail->sSubject.' - '.$this->sLastError;
			    $sReplyBody = str_replace("\n", "<br/>", $sRejectionReply);
			    $sReplyTo = $oEmail->sCallerEmail;
			    $aTo = $oRawEmail->GetTo();
			    $sReplyFrom = $aTo[0]['email'];
			    $this->Trace("From: ".$sReplyFrom."\nTo: ".$sReplyTo."\n".$sReplySubject."\n\n".$sReplyBody);
			    $oRawEmail->SendAsAttachment($sReplyTo, $sReplyFrom, $sReplySubject, $sReplyBody);

			    $this->sLastError .= " - Replied to sender on ".date('r');
			}

			// Send to contact
			$sSubject = '[iTop] Unknown contact in incoming eMail - '.$oEmail->sSubject;
			$sBody = "<p>The following email (see attachment) comes from an unknown caller (".$oEmail->sCallerEmail.").<br/>\n";
			$sBody .= "<p>Check the configuration of the Mail Inbox '".$this->GetName()."', since the current configuration does not allow to create new contacts for unknown callers.</p>\n";
			$sBody .= $sLastAction;
			break;
			
			case 'decode_failed':
			$sSubject = '[iTop] Failed to decode an incoming eMail';
			if ($oRawEmail && ($oRawEmail->GetSize() > EmailBackgroundProcess::$iMaxEmailSize))
			{
				$sBody = "<p>The incoming eMail is bigger (".$oRawEmail->GetSize()." bytes) than the maximum configured size (maximum_email_size = ".EmailBackgroundProcess::$iMaxEmailSize.").</p>\n";
				$this->sLastError = "eMail is bigger (".$oRawEmail->GetSize()." bytes) than the maximum configured size (maximum_email_size = ".EmailBackgroundProcess::$iMaxEmailSize.")";
				
				if ($this->Get('error_behavior') == 'delete')
				{
					if ($this->sBigFilesDir == '')
					{
						$sBody .= "<p>The email was deleted. In the future you can:\n<ul>\n";
						$sBody .= "<li>either increase the 'maximum_email_size' parameter in the iTop configuration file, so that the message gets processed</li>\n";
						$sBody .= "<li>or configure the parameter 'big_files_dir' in the iTop configuration file, so that such emails are kept on the web server for further inspection.</li>\n</ul>";
					}
					else if (!is_writable($this->sBigFilesDir))
					{
						$sBody .= "<p>The email was deleted, since the directory where to save such files on the web server ($this->sBigFilesDir) is NOT writable to iTop.</p>\n";
					}
					else
					{
						$idx = 1;
						$sFileName = 'email_'.(date('Y-m-d')).'_';
						$sExtension = '.eml';
						$hFile = false;
						while(($hFile = fopen($this->sBigFilesDir.'/'.$sFileName.$idx.$sExtension, 'x')) === false)
						{
							$idx++;
						}
						fwrite($hFile, $oRawEmail->GetRawContent());
						fclose($hFile);
						$sBody .= "<p>The message was saved as '{$sFileName}{$idx}{$sExtension}' on the web server, in the directory '{$this->sBigFilesDir}'.</p>\n";
						$sBody .= "<p>In order process such messages, increase the value of the 'maximum_email_size' parameter in the iTop configuration file.</p>\n";
					}
				}
				else
				{
					$sBody .= $sLastAction;
				}
								
				$oRawEmail = null; // Do not attach the original message to the mail sent to the admin since it's already big, send the message now
				$this->Trace($sSubject."\n\n".$sBody);
				// Send the email now...
				if(($sTo != '') && ($sFrom != ''))
				{
					$oEmailToSend = new Email();
			  		$oEmailToSend->SetRecipientTO($sTo);
			  		$oEmailToSend->SetSubject($sSubject);
			  		$oEmailToSend->SetBody($sBody, 'text/html');	
			  		$oEmailToSend->SetRecipientFrom($sFrom);
			  		$oEmailToSend->Send($aIssues, true /* bForceSynchronous */, null /* $oLog */);
				}
			}
			else
			{
				$sBody = "<p>The following eMail (see attachment) was not decoded properly and therefore was not processed at all.</p>\n";
				$sBody .= $sLastAction;
			}
			break;
			
			case 'nothing_to_update':
			$sSubject = '[iTop] Unable to update a ticket from the eMail - '.$oEmail->sSubject;
			$sBody = "<p>The following email (see attachment) does not seem to correspond to a ticket in iTop.<br/>\n";
			$sBody .= "The Mail Inbox ".$this->GetName()." is configured to only update existing tickets, therefore the eMail has been rejected.</p>\n";
			$sBody .= $sLastAction;
			$this->sLastError = "No corresponding iTop ticket to update (mode=update only)";
			break;
			
			case 'failed_to_create_contact':
			$sSubject = '[iTop] Failed to create a contact for the incoming eMail - '.$oEmail->sSubject;
			$sBody = "<p>The following email (see attachment) comes from an unknown caller (".$oEmail->sCallerEmail.").<br/>\n";
			$sBody .= "The configuration of the Mail Inbox ".$this->GetName()." instructs to create a new contact based on some default values, but this creation was not successful.<br/>\n";
			$sBody .= "Check the contact's default values configured in the Mail Inbox.</p>\n";
			$sBody .= $sLastAction;
			$this->sLastError = "Failed to create a contact from the incoming eMail. Caller = ".$oEmail->sCallerEmail;
			break;
			
			case 'rejected_attachments':
			$sSubject = '[iTop] Failed to process attachment(s) for the incoming eMail - '.$oEmail->sSubject;
			$sBody = "<p>Some attachments to the eMail were not processed because they are too big:</p>\n";
			$this->sLastError = $sAdditionalErrorMessage;
			$sBody .= "<pre>".$sAdditionalErrorMessage."</pre>\n";

			$oRawEmail = null; // No original message in attachment
			$this->Trace($sSubject."\n\n".$sBody);
			// Send the email now...
			if(($sTo != '') && ($sFrom != ''))
			{
				$oEmailToSend = new Email();
		  		$oEmailToSend->SetRecipientTO($sTo);
		  		$oEmailToSend->SetSubject($sSubject);
		  		$oEmailToSend->SetBody($sBody, 'text/html');	
		  		$oEmailToSend->SetRecipientFrom($sFrom);
		  		$oEmailToSend->Send($aIssues, true /* bForceSynchronous */, null /* $oLog */);
			}
			break;
				
			case 'undesired_message':
			if (MetaModel::GetModuleSetting('combodo-email-synchro', 'undesired-purge-delay', 7) == 0)
			{
			    $this->SetNextAction(EmailProcessor::DELETE_MESSAGE); // immediate delete when delay is 0
			}
			else
			{
                $this->SetNextAction(EmailProcessor::MARK_MESSAGE_AS_UNDESIRED); // Keep the message in the mailbox, but marked as undesired
            }
            $this->sLastError = 'Undesired email';
            $oRawEmail = null; // No feedback on undesired emails
			break;
			
			default:
			$sSubject = '[iTop] handle error';
			$sBody = '<p>Unexpected error: '.$sErrorCode."</p>\n";
			$sBody .= $sLastAction;
			$this->sLastError = 'Unexpected error: '.$sErrorCode;
		}

        $sBody .= "<p>&nbsp;</p><p>Mail Inbox Configuration: ".$this->GetHyperlink()."</p>\n";

        if(($sTo == '') || ($sFrom == ''))
        {
            $this->Trace("HandleError($sErrorCode): No forward configured for forwarding the email...(To: '$sTo', From: '$sFrom'), skipping.");
        }
        else if($oRawEmail)
        {
            $this->Trace("To: ".$sTo."\n".$sSubject."\n\n".$sBody);
            $oRawEmail->SendAsAttachment($sTo, $sFrom, $sSubject, $sBody);
        }
	}]]></code>
        </method>
        <method id="FixPattern">
          <comment>/**
	 * Make sure that the given string is a proper PCRE pattern by surrounding
	 * it with slashes, if needed
	 * @param string $sPattern The pattern to check (can be an empty string)
	 * @return string The valid pattern (or an empty string)
	 */</comment>
          <static>false</static>
          <access>protected</access>
          <type>Overload-DBObject</type>
          <code><![CDATA[	protected function FixPattern($sPattern)
	{
		$sReturn = $sPattern;
		if ($sPattern != '')
		{
			$sFirstChar = substr($sPattern, 0, 1);
			$sLastChar = substr($sPattern, -1, 1);
			if (($sFirstChar != $sLastChar) || preg_match('/[0-9A-Z-a-z]/', $sFirstChar) || preg_match('/[0-9A-Z-a-z]/', $sLastChar))
			{
				// Missing delimiter patterns
				$sReturn = '/'.$sPattern.'/';
			}
		}
		return $sReturn;
	}]]></code>
        </method>
      </methods>
      <presentation>
        <details>
          <items>
            <item id="col:col0">
              <rank>10</rank>
              <items>
                <item id="fieldset:MailInbox:Server">
                  <rank>10</rank>
                  <items>
                    <item id="server">
                      <rank>10</rank>
                    </item>
                    <item id="login">
                      <rank>20</rank>
                    </item>
                    <item id="password">
                      <rank>30</rank>
                    </item>
                    <item id="protocol">
                      <rank>40</rank>
                    </item>
                    <item id="port">
                      <rank>50</rank>
                    </item>
                    <item id="mailbox">
                      <rank>60</rank>
                    </item>
                    <item id="active">
                      <rank>70</rank>
                    </item>
                    <item id="trace">
                      <rank>80</rank>
                    </item>
                  </items>
                </item>
                <item id="fieldset:MailInbox:Errors">
                  <rank>20</rank>
                  <items>
                    <item id="error_behavior">
                      <rank>10</rank>
                    </item>
                    <item id="notify_errors_to">
                      <rank>20</rank>
                    </item>
                    <item id="notify_errors_from">
                      <rank>30</rank>
                    </item>
                  </items>
                </item>
              </items>
            </item>
            <item id="col:col1">
              <rank>20</rank>
              <items>
                <item id="fieldset:MailInbox:Behavior">
                  <rank>10</rank>
                  <items>
                    <item id="behavior">
                      <rank>10</rank>
                    </item>
                    <item id="email_storage">
                      <rank>20</rank>
                    </item>
                    <item id="target_folder">
                      <rank>30</rank>
                    </item>
                    <item id="target_class">
                      <rank>40</rank>
                    </item>
                    <item id="ticket_default_values">
                      <rank>50</rank>
                    </item>
                    <item id="ticket_default_title">
                      <rank>60</rank>
                    </item>
                    <item id="title_pattern">
                      <rank>70</rank>
                    </item>
                    <item id="stimuli">
                      <rank>80</rank>
                    </item>
                  </items>
                </item>
                <item id="fieldset:MailInbox:Caller">
                  <rank>20</rank>
                  <items>
                    <item id="unknown_caller_behavior">
                      <rank>10</rank>
                    </item>
                    <item id="unknown_caller_rejection_reply">
                      <rank>15</rank>
                    </item>
                    <item id="caller_default_values">
                      <rank>20</rank>
                    </item>
                  </items>
                </item>
                <item id="fieldset:MailInbox:OtherContacts">
                  <rank>30</rank>
                  <items>
                    <item id="import_additional_contacts">
                      <rank>10</rank>
                    </item>
                  </items>
                </item>
              </items>
            </item>
          </items>
        </details>
        <search>
          <items>
            <item id="server">
              <rank>10</rank>
            </item>
            <item id="login">
              <rank>20</rank>
            </item>
            <item id="mailbox">
              <rank>30</rank>
            </item>
            <item id="protocol">
              <rank>40</rank>
            </item>
            <item id="active">
              <rank>50</rank>
            </item>
          </items>
        </search>
        <list>
          <items>
            <item id="server">
              <rank>10</rank>
            </item>
            <item id="mailbox">
              <rank>20</rank>
            </item>
            <item id="protocol">
              <rank>30</rank>
            </item>
            <item id="active">
              <rank>40</rank>
            </item>
          </items>
        </list>
      </presentation>
    </class>
  </classes>
  <menus>
    <menu id="ConfigurationTools" xsi:type="MenuGroup" _delta="define_if_not_exists">
      <rank>90</rank>
    </menu>
    <menu id="MailInboxes" xsi:type="OQLMenuNode" _delta="define">
      <rank>30</rank>
      <parent>ConfigurationTools</parent>
      <oql><![CDATA[SELECT MailInboxStandard]]></oql>
      <do_search>1</do_search>
      <enable_admin_only>0</enable_admin_only>
      <enable_class>MailInboxStandard</enable_class>
      <enable_action>UR_ACTION_READ</enable_action>
    </menu>
  </menus>
</itop_design>
