<?php
use Bitweaver\Users\RolePermUser;
use Bitweaver\KernelTools;

function board_sync_run($pLog = false) {
	global $gBitUser, $gBitSystem;

	$gBitUser->setPermissionOverride('p_users_bypass_captcha', true);

	$connectionString = '{'.$gBitSystem->getConfig('boards_sync_mail_server','imap').':'.$gBitSystem->getConfig('boards_sync_mail_port','993').'/'.$gBitSystem->getConfig('boards_sync_mail_protocol','imap').'/ssl/novalidate-cert}';

	// Can we open the mailbox?
	if( $mbox = imap_open( $connectionString, $gBitSystem->getConfig( 'boards_sync_user' ), $gBitSystem->getConfig( 'boards_sync_password' ) ) )  {

		$MC = imap_check($mbox);

		// Fetch an overview for all messages in INBOX of mailbox has messages
		if( $MC->Nmsgs ) {
			//	  print($MC->Nmsgs);
			$result = imap_fetch_overview($mbox,"1:{$MC->Nmsgs}",0);
			if( $messageNumbers = imap_sort( $mbox, SORTDATE, 0 ) ) {
				foreach( $messageNumbers as $msgNum ) {
					if ($pLog) print "Processing Msg#: ".$msgNum."\n";
					$deleteMsg = false;
					$header = imap_headerinfo( $mbox, $msgNum );

					// Is this a moderation message?
					if( preg_match('/.*? post from .*? requires approval/', $header->subject) ) {
						if ($pLog) print "Is Moderation Request.\n";
						// moderated messages nest the orginal message in another part
						// php imap functions dont give us easy access to part header info, so...
						// to easily get to the headers of those message we open the part as a new imap stream

						// fetch the original message
						$body = imap_fetchbody( $mbox, $msgNum, 2);
						// add a spoof time marker to the first line to make imap_open happy
						$body = "From dummy@localhost  Sat Jan  1 00:00:00 1970\n".$body;

						// write the org msg to a temp file
						$filename = 'orginal_email.eml';
						srand( time() );
						$filestore = TEMP_PKG_PATH.BOARDS_PKG_NAME.'/boardsync/'.rand( 999, 999999999 ).'/'.$filename;
						KernelTools::mkdir_p( dirname( $filestore ) );
						$fp=fopen( $filestore, "w+" );
						fwrite( $fp, $body );
						fclose( $fp );

						// open the temp file as an imap stream so we can use imap_headerinfo() to parse the org msg header
						$mbox2 = imap_open( $filestore, "", "" );
						$msgHeader = imap_headerinfo( $mbox2, 1 );
						// moderation validation is also in a part, extract it
						$replyBody = imap_fetchbody( $mbox, $msgNum, 3);
						$replyHeaders = board_sync_raw_headers($replyBody);
						$approveSubj = board_sync_get_header('Subject', $replyHeaders);
						$confirmCode = substr($approveSubj, strlen('confirm '));
						if ($pLog) print "Confirm code: ".$confirmCode."\n";

						$deliveredTo = board_sync_delivered_to($replyHeaders);
						$deleteMsg = board_sync_process_message($mbox, $msgNum, $msgHeader, imap_fetchstructure( $mbox, $msgNum, 2), $confirmCode, $pLog, $deliveredTo);
						// Is this a reminder message that we just skip?
					} elseif( preg_match('/[0-9]+ .*? moderator request.* waiting/', $header->subject) ) {
						if ($pLog) print "Deleting reminder.\n";
						$deleteMsg = true;
					} elseif( preg_match('/Welcome to the .* mailing list/', $header->subject) ) {
						if ($pLog) print "Deleting welcome message.\n";
						$deleteMsg = true;
					} else {
						// imap_headerinfo acts retarded on these emails and improperly parses the header unless we call fetchstructure first - so do it.
						// note this problem does not occure above when parsing the temp email in a moderated msg - god damn spooky
						$msgStructure = imap_fetchstructure(  $mbox, $msgNum );
						$msgHeader = imap_headerinfo( $mbox, $msgNum );
						// With multiple To: recipients it is often handy to know who the message is "Delivered-To" by the MTA
						$raw_headers = imap_fetchheader($mbox, $msgNum);
						$deliveredTo = board_sync_delivered_to($raw_headers);
						$deleteMsg = board_sync_process_message( $mbox, $msgNum, $msgHeader, $msgStructure, false, $pLog, $deliveredTo);
						//					vd($deleteMsg);
					}
					if( $deleteMsg && empty( $gDebug ) && empty( $gArgs['test'] ) ) {
						//					vd("DELETE!");
						if ($pLog) print "Deleted msg $msgNum\n";
						imap_delete( $mbox, $msgNum );
					}
				}
			}
		}

		// final cleanup
		imap_expunge( $mbox );
		imap_close( $mbox );
		// clear everything we've written to the temp directory
		$dir = TEMP_PKG_PATH.BOARDS_PKG_NAME.'/boardsync';
		if( is_dir( $dir ) && strpos( $dir, BIT_ROOT_PATH ) === 0 ) {
			if( !KernelTools::unlink_r( $dir ) ) {
				bit_error_log( "Failed to clear directory: ".$dir." in boards package mailinglist synchronization." );
			}
		}

	} else {
		bit_error_log( __FILE__." failed imap_open $connectionString ".imap_last_error() );
	}

}

function board_parse_msg_parts( &$pPartHash, $pMbox, $pMsgId, $pMsgPart, $pPartNum, $pLog ) {

	//fetch part
	$part=imap_fetchbody( $pMbox, $pMsgId, $pPartNum);
	switch( $pMsgPart->encoding ) {
		case '3': // BASE64
			$part = base64_decode($part);
			break;
		case '4': // QUOTED-PRINTABLE
			$part = quoted_printable_decode($part);
			break;
		//0	7BIT
		//1	8BIT
		//2	BINARY
		//4	QUOTED-PRINTABLE
		//5	OTHER
	}

	if ($pLog) print "Msg part ".$pPartNum." type: ".$pMsgPart->subtype."\n";

	switch( $pMsgPart->type ) {
		case '0':
			// make sure text is UTF-8
			if( $pMsgPart->ifparameters ){
				foreach( $pMsgPart->parameters as $params ){
					// we trust the email source to specify the correct charset
					// Note: alternatively one might run a check to make sure the text is really utf-8, regardless of the header
					// use strtolower on the attributes since different php installs do not reconcile casing consistantly
					if( strtolower( $params->attribute ) == 'charset' && strtolower( $params->value ) != 'utf-8' ){
						if ($pLog) print( "Msg part ".$pPartNum." charset: ".$params->value."\n" );
						$part = @iconv($params->value, 'UTF-8', $part );
					}
				}
			}
			// put msg in hash
			$pPartHash[$pPartNum][strtolower($pMsgPart->subtype)] = $part;
			break;
		default:
			// type is not text
			if( !preg_match( '/signature/i', $pMsgPart->subtype ) ) {
				//get filename of attachment if present
				$filename='';
				foreach( [ 'dparameters', 'parameters' ] as $prm ) {
					if( empty( $filename ) ) {
						// if there are any dparameters present in this part
						if( !empty($pMsgPart->$prm) && count( $pMsgPart->$prm ) > 0 ){
							foreach( $pMsgPart->$prm as $param ) {
								if( strtoupper( $param->attribute ) == 'NAME' || strtoupper( $param->attribute ) == 'FILENAME' ) {
									$filename = $param->value;
								}
							}
						}
					}
				}
				//write to disk and set pPartHash variable
				if( !empty( $filename ) ) {
					//where to write file attachments to
					srand( time() );
					$filestore = TEMP_PKG_PATH.BOARDS_PKG_NAME.'/boardsync/'.rand( 999, 999999999 ).'/'.$filename;
					KernelTools::mkdir_p( dirname( $filestore ) );
					$pPartHash[$pPartNum]['attachment'] = $filestore;
					$fp=fopen( $filestore, "w+" );
					fwrite( $fp, $part );
					fclose( $fp );
				}
			}
			break;

	}

	//if subparts... recurse into function and parse them too!
	if( !empty( $pMsgPart->parts ) ){
		foreach ($pMsgPart->parts as $pno=>$parr){
		board_parse_msg_parts( $pPartHash, $pMbox, $pMsgId, $parr, ( $pPartNum.'.'.( $pno + 1 ) ), $pLog);
		}
	}
}

function board_sync_get_user( $pFrom ) {
	global $gBitUser;

	if( preg_match_all('/[^<\s]+@[^>\s]+/', $pFrom, $matches) ) {
		foreach( $matches[0] as $email ) {
			$ret = $gBitUser->getUserInfo( [ 'email'=>$email ] );
			if( !empty($ret) ) {
				return $ret;
			}
		}
	}

	return $gBitUser->getUserInfo( [ 'user_id'=>-1 ] );
}

function cache_check_content_prefs( $pName, $pValue, $pLower = false ) {
	global $gBitDb, $gBitSystem;
	static $prefs;

	if( empty($prefs[$pLower][$pName]) ) {
		$bindVars = [ $pName ];
		$prefs[$pLower][$pName] = $gBitDb->getAssoc( "SELECT " .
							($pLower ? 'LOWER(`pref_value`)' : '`pref_value`').
							", `content_id` FROM `".BIT_DB_PREFIX."liberty_content_prefs` WHERE `pref_name`=?", $bindVars, );
	}

	if( !empty($prefs[$pLower][$pName][$pValue]) ) {
		return $prefs[$pLower][$pName][$pValue];
	}

	return null;
}

/**
 * $pMsgHeader is a imap_headerinfo generated array
 **/
function board_sync_process_message( $pMbox, $pMsgNum, $pMsgHeader, $pMsgStructure, $pModerate = false , $pLog=false, $pDeliveredTo=null) {
	global $gBitSystem, $gBitDb;
	// vd( $pMsgHeader );

	// Collect a bit of header information
	$message_id = board_sync_get_headerinfo( $pMsgHeader, 'message_id' );
	// @TODO comment or clean up, not sure why this is here -wjames5
	if( empty($message_id) ) {
		$message_id = board_sync_get_headerinfo( $pMsgHeader, 'message_id' );
	}
	$subject = board_sync_get_headerinfo( $pMsgHeader, 'Subject' );

	if( empty( $message_id ) ){
		bit_error_log( "Email sync for message: ".$subject." failed: No Message Id in mail header." );
	}else{
		if ($pLog) print("Processing: ".$message_id."\n");
		if ($pLog) print("  Subject: ".$subject."\n");

		$matches = [];
		$toAddresses = [];
		$allRecipients = "";
		if (empty($pDeliveredTo)) {
			if( isset( $pMsgHeader->toaddress ) ){
				$allRecipients .= $pMsgHeader->toaddress;
				if ($pLog) print ("  To addresses: " . $pMsgHeader->toaddress . "\n");
			}
			if( isset( $pMsgHeader->ccaddress ) ){
				$allRecipients .= (( $allRecipients != "" )?",":"") . $pMsgHeader->ccaddress;
				if ($pLog) print ("  CC addresses: " . $pMsgHeader->ccaddress . "\n");
			}

			if ($pLog) print ("  All Recipients: ". $allRecipients ."\n");
			$allSplit = split( ',', $allRecipients );
			foreach( $allSplit as $s ) {
				$s = trim( $s );
				$matches = [];
				if( strpos( $s, '<' ) !== false ) {
					if( preg_match( "/\s*(.*)\s*<\s*(.*)\s*>/", $s, $matches ) ) {
						$toAddresses[] = [ 'name'=>$matches[1], 'email'=>$matches[2] ];
					} elseif( preg_match('/<\s*(.*)\s*>\s*(.*)\s*/', $s, $matches) ) {
						$toAddresses[] = [ 'email'=>$matches[1], 'name'=>$matches[2] ];
					}
				} elseif( KernelTools::validate_email_syntax( $s ) ) {
					$toAddresses[] = [ 'email'=>$s ];
				}
			}
		} else {
			foreach ($pDeliveredTo as $address) {
				$toAddresses[] = ['email' => $address];
			}
		}
		if ($pLog) print_r($toAddresses);

		$date = board_sync_get_headerinfo($pMsgHeader, 'Date');
		$from = board_sync_get_headerinfo($pMsgHeader, 'from');
		$fromaddress = $from[0]->mailbox."@".$from[0]->host;
		// personal is not always defined.
		if (isset($from[0]->personal)) {
			$personal = ucwords($from[0]->personal);
		} else {
			$personal = null;
		}
		$in_reply_to = board_sync_get_headerinfo($pMsgHeader, 'in_reply_to');

		if ($pLog) print( "\n---- ".date( "Y-m-d HH:mm:ss" )." -------------------------\nImporting: ".$message_id."\nDate: ".$date."\nFrom: ".$fromaddress."\nTo: ".$allRecipients."\nSubject: ".$subject."\nIn Reply To: ".$in_reply_to."\nName: ".$personal.(is_array($pDeliveredTo) ? "\nDelivered-To:".implode(", ", $pDeliveredTo) : '')."\n");

		foreach( $toAddresses AS $to ) {
			if ($pLog) print( "  Processing email: " . strtolower($to['email']) . "\n");
			// get a board match for the email address
			if( $boardContentId = cache_check_content_prefs( 'board_sync_list_address', strtolower($to['email']), true ) ) {
				if ($pLog) print "Found Board Content $boardContentId for $to[email]\n";

				// Do we already have this message in this board?
				$contentId = null;
				if( $message_id != null ) {
					$sql = "SELECT `content_id` FROM `".BIT_DB_PREFIX."liberty_comments` WHERE `message_guid`=? AND `root_id`=?";
					$contentId = $gBitDb->getOne( $sql, [ $message_id, $boardContentId ] );
				}
				if( empty($contentId) ) {
					if( !empty( $in_reply_to ) ) {
						if( $parent = $gBitDb->GetRow( "SELECT `content_id`, `root_id` FROM `".BIT_DB_PREFIX."liberty_comments` WHERE `message_guid`=?", [ $in_reply_to ] ) ) {
							$replyId = $parent['content_id'];
							$rootId = $parent['root_id'];
						} else {
							if ($pLog) print ( "WARNING: Reply to unfound message: ".$in_reply_to );
							$replyId = $boardContentId;
							$rootId = $boardContentId;
						}
						// if no reply to message guid then match on title - this looks dangerous as titles could easily be duplicated -wjames
					} elseif( $parent = $gBitDb->GetRow( "SELECT lcom.`content_id`, lcom.`root_id` FROM `".BIT_DB_PREFIX."liberty_comments` lcom INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON(lcom.`content_id`=lc.`content_id`) WHERE lc.`title`=?", [ preg_replace( '/re: /i', '', $subject ) ] ) ) {
						$replyId = $parent['content_id'];
						$rootId = $parent['root_id'];
						// attach to board as first level comment e.g. new topic
					} else {
						$replyId = $boardContentId;
						$rootId = $boardContentId;
					}
					$userInfo = board_sync_get_user( $fromaddress );
					// prep the storage hash
					$storeRow = [];
					$storeRow['created'] = strtotime( $date );
					$storeRow['last_modified'] = $storeRow['created'];
					$storeRow['user_id'] = $userInfo['user_id'];
					$storeRow['modifier_user_id'] = $userInfo['user_id'];
					$storeRow['title'] = $subject;
					$storeRow['message_guid'] = $message_id;
					if( $userInfo['user_id'] == ANONYMOUS_USER_ID && !empty( $personal ) ) {
						$storeRow['anon_name'] = $personal;
					}
					$storeRow['root_id'] = $rootId;
					$storeRow['parent_id'] = $replyId;

					$partHash = [];

					switch( $pMsgStructure->type ) {
					case '0':
						if ($pLog) print( "Structure Type: text\n" );
						board_parse_msg_parts( $partHash, $pMbox, $pMsgNum, $pMsgStructure, 1, $pLog );
						break;
					case '1':
						if ($pLog) print( "Structure Type: multipart\n" );
						if ($pModerate) {
							$prefix = '2.';
						}
						else {
							$prefix = '';
						}
						foreach( $pMsgStructure->parts as $partNum => $part ) {
							board_parse_msg_parts( $partHash, $pMbox, $pMsgNum, $part, $prefix.($partNum+1), $pLog );
						}
						break;
					}
					$plainBody = "";
					$htmlBody = "";

					foreach( array_keys( $partHash ) as $i ) {
						if( !empty( $partHash[$i]['plain'] ) ) {
							$plainBody .= $partHash[$i]['plain'];
						}
						if( !empty( $partHash[$i]['html'] ) ) {
							$htmlBody .= $partHash[$i]['html'];
						}
						if( !empty( $partHash[$i]['attachment'] ) ) {
							$storeRow['_files_override'][] = [
												   'tmp_name'=> $partHash[$i]['attachment'],
												   'type'=>$gBitSystem->verifyMimeType( $partHash[$i]['attachment'] ),
												   'size'=>filesize( $partHash[$i]['attachment'] ),
												   'name'=>basename( $partHash[$i]['attachment'] ),
												   'user_id'=>$userInfo['user_id'], ];
						}
					}

					if( !empty( $htmlBody ) ) {
						$storeRow['edit'] = $htmlBody;
						$storeRow['format_guid'] = 'bithtml';
					} elseif( !empty( $plainBody ) ) {
						$storeRow['edit'] = nl2br( $plainBody );
						$storeRow['format_guid'] = 'bithtml';
					}

					// Nuke all email addresses from the body.
					if( !empty($storeRow['edit']) ) {
						$storeRow['edit'] = ereg_replace(
										 '[-!#$%&\`*+\\./0-9=?A-Z^_`a-z{|}~]+'.'@'.
										 '(localhost|[-!$%&\'*+\\/0-9=?A-Z^_`a-z{|}~]+\.'.
										 '[-!$%&\'*+\\./0-9=?A-Z^_`a-z{|}~]+)', '', $storeRow['edit'], );
					}

					// We trust the user from this source
					// and count on moderation to handle links
					global $gBitUser;
					$gBitUser->setPermissionOverride('p_liberty_trusted_editor', true);

					// Check to add attachments

					// NOTE: we temporarily change the gBitUser here!
					// This is so we can run a proper content permissions check
					// for attachment permission against the parent
					// board object. This is sort of a hack to deal
					// with the fact that LibertyContent does not have a
					// means to check the permissions of any user except gBitUser -wjames5

					// Important store a reference so we can switch back when we are done
					$gBitUserOrg = $gBitUser;

					// Load the message sending user
					if( $userInfo['user_id'] != ANONYMOUS_USER_ID ) {
						$newBitUser = new RolePermUser( $userInfo['user_id'] );
						$newBitUser->load( true );
					}
					if( !empty( $newBitUser ) && $newBitUser->isValid() ){
						// flip gBitUser to our message sender
						$gBitUser = $newBitUser;
					}

					// Load the parent board
					$board = new BitBoard( null, $boardContentId );
					$board->load();

					// Check the permission for the user on the board
					if( $gBitSystem->isFeatureActive( 'comments_allow_attachments' ) && $board->hasUserPermission( 'p_liberty_attach_attachments' ) ){
						// note we grant the permission to the anonymous user which will become gBitUser once again
						$gBitUserOrg->setPermissionOverride('p_liberty_attach_attachments', true);
					};

					// Clear the reference to this board so we dont mistakenly use it later
					unset( $board );

					// Important: switch gBitUser back!
					$gBitUser = $gBitUserOrg;

					// End check to add attachments to comments to the parent board

					// Check for an empty body
					// Duplicate subject if we have it
					if (empty($storeRow['edit'])) {
						if (!empty($storeRow['title'])) {
							$storeRow['edit'] = $storeRow['title'];
						}
						else {
							$storeRow['edit'] = ".";
						}
					}

					$storeComment = new LibertyComment( null );
					$gBitDb->StartTrans();
					if( $storeComment->storeComment($storeRow) ) {
						// undo the attachment permission
						$gBitUser->setPermissionOverride('p_liberty_attach_attachments', false);

						// set moderation approval
						if( !$pModerate && $gBitSystem->isPackageActive('moderation') && $gBitSystem->isPackageActive('modcomments') ) {
							global $gModerationSystem, $gBitUser;
							$moderation = $gModerationSystem->getModeration(null, $storeComment->mContentId);
							if( !empty($moderation) ) {
								// Allow to moderate
								$gBitUser->setPermissionOverride('p_admin', true);
								$gModerationSystem->setModerationReply($moderation['moderation_id'], MODERATION_APPROVED);
								$gBitUser->setPermissionOverride('p_admin', false);
							}
						}

						if( !empty( $storeRow['message_guid'] ) ){
							// map the message guid to the comment
							$storeComment->mDb->query( "UPDATE `".BIT_DB_PREFIX."liberty_comments` SET `message_guid`=? WHERE `content_id`=?", [ $storeRow['message_guid'], $storeComment->mContentId ] );

							// Store the confirm code
							if( $pModerate ) {
								$storeComment->storePreference('board_confirm_code', $pModerate);
							}

							// done
							$gBitDb->CompleteTrans();
							return true;
						}
							bit_error_log( "Email sync error: Message Id not set. You shouldn't have even gotten this far." );
							$gBitDb->RollbackTrans();
							return false;

					}
						if( count( $storeComment->mErrors ) == 1 && !empty( $storeComment->mErrors['store'] ) && $storeComment->mErrors['store'] == 'Duplicate comment.' ) {
							return true;
						}
							foreach( $storeComment->mErrors as $error ){
								bit_error_log( $error );
							}
							$gBitDb->RollbackTrans();
							return false;

				}
					if ($pLog) print "Message Exists: $contentId : $boardContentId : $message_id : $pModerate\n";
					// If this isn't a moderation message
					if( $pModerate === false ) {
						// If the message exists it must have been approved via some
						// moderation mechanism, so make sure it is available
						if( $gBitSystem->isPackageActive('moderation') && $gBitSystem->isPackageActive('modcomments') ) {
							global $gModerationSystem, $gBitUser;
							$storeComment = new LibertyComment( null, $contentId );
							$storeComment->loadComment();
							if ($storeComment->mInfo['content_status_id'] > 0) {
								if ($pLog) print "Already approved: $contentId\n";
							} else {
								$moderation = $gModerationSystem->getModeration(null, $contentId);
								//				vd($moderation);
								if( !empty($moderation) ) {
									$gBitUser->setPermissionOverride('p_admin', true);
									if ($pLog) print( "Setting approved: $contentId\n" );
									$gModerationSystem->setModerationReply($moderation['moderation_id'], MODERATION_APPROVED);
									$gBitUser->setPermissionOverride('p_admin', false);
									if ($pLog) print "Done";
								} else {
									if ($pLog) print "ERROR: Unable to find moderation to approve for: $contentId";
								}
							}
						}
					} else {
						// Store the approve code;
						if ($pLog) print "Storing approval code: " . $contentId . ":" . $pModerate . "\n";
						$storeComment = new LibertyComment( null, $contentId );
						$storeComment->storePreference('board_confirm_code', $pModerate);
					}
					return true;

			}
				if ($pLog) print "No Board match found for $to[email]\n";

		}
	}
	return false;
}

function board_sync_raw_headers($body) {
	$matches = preg_split('/^\s*$/ms', $body, 2);
	return $matches[0];
}

function board_sync_get_header($header, $body) {
	$ret = null;
	preg_match( '/^'.$header.':\s*(.*?)\s*$/m', $body, $matches);
	if (!empty($matches[1])) {
		$ret = $matches[1];
	}
	return $ret;
}

// $header is imap_headerinfo array
function board_sync_get_headerinfo( $header, $key ){
	$ret = null;
	if( isset( $header->$key ) ){
		$ret = $header->$key;
	}
	return $ret;
}

function board_sync_delivered_to( $raw_headers ) {
	$ret = null;
	if (isset($raw_headers) &&
		preg_match_all("/Delivered-To:\s*(.*)\s*/", $raw_headers, $deliveredTo) > 0) {
		$ret = [];
		foreach ($deliveredTo[1] as $address) {
			// Make sure the Delivered-To: address is valid.
			if (KernelTools::validate_email_syntax( $address ) ) {
				$ret[] = strtolower(trim($address));
			}
		}
	}
	return $ret;
}

function is_utf8($string) {
	// From http://w3.org/International/questions/qa-forms-utf-8.html
	return preg_match('%^(?:
          [\x09\x0A\x0D\x20-\x7E]            # ASCII
        | [\xC2-\xDF][\x80-\xBF]             # non-overlong 2-byte
        |  \xE0[\xA0-\xBF][\x80-\xBF]        # excluding overlongs
        | [\xE1-\xEC\xEE\xEF][\x80-\xBF][2]  # straight 3-byte
        |  \xED[\x80-\x9F][\x80-\xBF]        # excluding surrogates
        |  \xF0[\x90-\xBF][\x80-\xBF][2]     # planes 1-3
        | [\xF1-\xF3][\x80-\xBF][3]          # planes 4-15
        |  \xF4[\x80-\x8F][\x80-\xBF][2]     # plane 16
    )*$%xs', $string);
}
