-
-
Save EionRobb/21dd89b05417b247bface6373fb380fb to your computer and use it in GitHub Desktop.
Office 365 Roaming Signature
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| // Create a new roaming signature for a user | |
| // To configure: | |
| // Create a new Enterprise Application at https://portal.azure.com | |
| // Make sure it has the API permissions for Microsoft Graph to be: | |
| // Directory.Read.All | |
| // EWS.AccessAsUser.All | |
| // Files.Read.All | |
| // GroupMember.Read.All | |
| // Mail.ReadWrite | |
| // MailboxSettings.ReadWrite | |
| // offline_access | |
| // openid | |
| // profile | |
| // User.Read.All | |
| // and grant admin consent | |
| // Then get the client ID, client secret, and tenant ID from the application | |
| // and set them as environment variables | |
| // OAUTH_CLIENT_ID | |
| // OAUTH_CLIENT_SECRET | |
| // OAUTH_CLIENT_TENANT | |
| // Based on https://gist.github.com/lart2150/8b4707a933bb70377f1594e4c658df39 | |
| class RoamingSignatureManager { | |
| private $accessToken; | |
| private $refreshToken; | |
| private $outlookAccessToken; | |
| private static $TIMESTAMP_BASE_TICKS = 621355968000000000; // Ticks between 0001-01-01 and 1970-01-01 | |
| public function __construct() { | |
| $this->accessToken = $this->getAccessToken(); | |
| } | |
| private function getTimestampTicks() { | |
| $microtime = microtime(true); | |
| $ticks = (int)($microtime * 10000000) + self::$TIMESTAMP_BASE_TICKS; | |
| return $ticks; | |
| } | |
| /** | |
| * Get OAuth access token using authorization code flow | |
| */ | |
| private function getAccessToken() { | |
| $tenantId = $_ENV['OAUTH_CLIENT_TENANT']; | |
| $clientId = $_ENV['OAUTH_CLIENT_ID']; | |
| $clientSecret = $_ENV['OAUTH_CLIENT_SECRET']; | |
| $redirectUri = 'http://localhost:56523'; | |
| if (empty($tenantId) || empty($clientId) || empty($clientSecret)) { | |
| throw new Exception('Missing OAuth credentials. Please set OAUTH_CLIENT_TENANT, OAUTH_CLIENT_ID, and OAUTH_CLIENT_SECRET environment variables.'); | |
| } | |
| $scope = 'https://graph.microsoft.com/.default openid profile offline_access'; | |
| $authUrl = "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize?" . http_build_query([ | |
| 'client_id' => $clientId, | |
| 'response_type' => 'code', | |
| 'redirect_uri' => $redirectUri, | |
| 'scope' => $scope, | |
| 'client-request-id' => 'e9865a68-7cb4-4fa4-8cab-d964573492c9' | |
| ]); | |
| echo "Starting local server for OAuth callback on {$redirectUri}...\n"; | |
| $server = stream_socket_server("tcp://127.0.0.1:56523", $errno, $errorMessage); | |
| if ($server === false) { | |
| throw new Exception("Could not bind to socket: $errorMessage"); | |
| } | |
| echo "Opening browser to $authUrl\n"; | |
| // Attempt to open the default browser on Windows | |
| pclose(popen('start "" "' . str_replace('&', '&', $authUrl) . '"', 'r')); | |
| echo "Waiting for authentication in browser...\n"; | |
| $client = stream_socket_accept($server, 300); // 5 minute timeout | |
| $code = null; | |
| if ($client) { | |
| $request = fread($client, 2048); | |
| preg_match('/GET \/\?.*?code=([^ &\r\n]+)/', $request, $matches); | |
| $code = $matches[1] ?? null; | |
| $response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n"; | |
| if ($code) { | |
| $response .= "<h1>Authentication Successful</h1><p>You can close this window and return to the application.</p><script>setTimeout(function(){window.close();}, 1500);</script>"; | |
| } else { | |
| $response .= "<h1>Authentication Failed</h1><p>No code parameter found in the request. Please try again.</p>"; | |
| } | |
| fwrite($client, $response); | |
| fclose($client); | |
| } else { | |
| fclose($server); | |
| throw new Exception("Timed out waiting for authentication."); | |
| } | |
| fclose($server); | |
| if (!$code) { | |
| throw new Exception("Did not receive authorization code."); | |
| } | |
| echo "Exchanging authorization code for token...\n"; | |
| $tokenUrl = "https://login.microsoftonline.com/organizations/oauth2/v2.0/token"; | |
| $postData = http_build_query([ | |
| 'client_id' => $clientId, | |
| 'scope' => $scope, | |
| 'grant_type' => 'authorization_code', | |
| 'code' => $code, | |
| 'redirect_uri' => $redirectUri | |
| ]); | |
| $context = stream_context_create([ | |
| 'http' => [ | |
| 'method' => 'POST', | |
| 'header' => 'Content-Type: application/x-www-form-urlencoded', | |
| 'content' => $postData, | |
| 'proxy' => 'localhost:8888', | |
| ], | |
| 'ssl' => [ | |
| 'verify_peer' => false, | |
| 'verify_peer_name' => false, | |
| ] | |
| ]); | |
| $response = file_get_contents($tokenUrl, false, $context); | |
| if ($response === false) { | |
| throw new Exception('Failed to get access token'); | |
| } | |
| $tokenData = json_decode($response, true); | |
| if (!isset($tokenData['access_token'])) { | |
| throw new Exception('Invalid token response: ' . $response); | |
| } | |
| $this->refreshToken = $tokenData['refresh_token']; | |
| $this->accessToken = $tokenData['access_token']; | |
| $scope = 'https://outlook.office.com/.default offline_access openid profile'; | |
| $postData = http_build_query([ | |
| 'client_id' => $clientId, | |
| 'scope' => $scope, | |
| 'grant_type' => 'refresh_token', | |
| 'refresh_token' => $this->refreshToken, | |
| 'client_info' => 1, | |
| ]); | |
| $context = stream_context_create([ | |
| 'http' => [ | |
| 'method' => 'POST', | |
| 'header' => 'Content-Type: application/x-www-form-urlencoded', | |
| 'content' => $postData, | |
| 'proxy' => 'localhost:8888', | |
| ], | |
| 'ssl' => [ | |
| 'verify_peer' => false, | |
| 'verify_peer_name' => false, | |
| ] | |
| ]); | |
| $response = file_get_contents($tokenUrl, false, $context); | |
| if ($response === false) { | |
| throw new Exception('Failed to get access token'); | |
| } | |
| $tokenData = json_decode($response, true); | |
| if (!isset($tokenData['access_token'])) { | |
| throw new Exception('Invalid token response: ' . $response); | |
| } | |
| $this->outlookAccessToken = $tokenData['access_token']; | |
| return $this->accessToken; | |
| } | |
| /** | |
| * Get all users from Microsoft Graph API | |
| */ | |
| public function getUsers() { | |
| $url = "https://graph.microsoft.com/v1.0/users?\$filter=" . rawurlencode('accountEnabled eq true and not(employeeId eq null)'); | |
| $context = stream_context_create([ | |
| 'http' => [ | |
| 'method' => 'GET', | |
| 'header' => [ | |
| "Authorization: Bearer {$this->accessToken}", | |
| 'ConsistencyLevel: eventual' | |
| ], | |
| 'proxy' => 'localhost:8888', | |
| ], | |
| 'ssl' => [ | |
| 'verify_peer' => false, | |
| 'verify_peer_name' => false, | |
| ] | |
| ]); | |
| $response = file_get_contents($url, false, $context); | |
| if ($response === false) { | |
| throw new Exception('Failed to get users from Microsoft Graph'); | |
| } | |
| $data = json_decode($response, true); | |
| if (!isset($data['value'])) { | |
| throw new Exception('Invalid users response: ' . $response); | |
| } | |
| return $data['value']; | |
| } | |
| public function getUser($email) { | |
| $url = "https://graph.microsoft.com/v1.0/users/{$email}?\$select=businessPhones%2Ccity%2CcompanyName%2Ccountry%2Cdepartment%2CdisplayName%2CfaxNumber%2CgivenName%2CjobTitle%2Cmail%2CmailNickname%2CmobilePhone%2CofficeLocation%2ConPremisesExtensionAttributes%2CpostalCode%2CproxyAddresses%2Cstate%2CstreetAddress%2Csurname%2Cid%2ConPremisesDistinguishedName%2ConPremisesDomainName%2ConPremisesImmutableId%2ConPremisesSamAccountName%2ConPremisesSecurityIdentifier%2ConPremisesUserPrincipalName%2CsecurityIdentifier%2CuserPrincipalName&\$expand=manager(\$select=id,displayName)"; | |
| $context = stream_context_create([ | |
| 'http' => [ | |
| 'method' => 'GET', | |
| 'header' => [ | |
| "Authorization: Bearer {$this->accessToken}", | |
| 'ConsistencyLevel: eventual' | |
| ], | |
| 'proxy' => 'localhost:8888', | |
| ], | |
| 'ssl' => [ | |
| 'verify_peer' => false, | |
| 'verify_peer_name' => false, | |
| ] | |
| ]); | |
| $response = file_get_contents($url, false, $context); | |
| if ($response === false) { | |
| throw new Exception('Failed to get user from Microsoft Graph'); | |
| } | |
| $data = json_decode($response, true); | |
| return $data; | |
| } | |
| /** | |
| * Generate signature text format | |
| */ | |
| private function generateSignatureText($user) { | |
| $businessPhone = isset($user['businessPhones'][0]) ? $user['businessPhones'][0] : ''; | |
| return "{$user['displayName']}\n" . | |
| "{$user['jobTitle']}\n" . | |
| "{$businessPhone}\n" . | |
| "{$user['mail']}"; | |
| } | |
| /** | |
| * Generate signature HTML format | |
| */ | |
| private function generateSignatureHtml($user) { | |
| $businessPhone = isset($user['businessPhones'][0]) ? $user['businessPhones'][0] : ''; | |
| return ' | |
| <p style="padding: 5px 10px 0 0; margin: 0; color: #254F59; font-family: Arial; font-size: 17px; font-weight: bold; text-transform: uppercase"><span style="color: #254F59;">' . htmlspecialchars($user['displayName']) . '</span></p> | |
| <p style="padding: 0px 10px 4px 0; margin: 0; color: #262626; font-family: Arial; font-size: 13px; font-weight: normal; line-height: 1.6; text-align: left; vertical-align: top;"><span style="color: #254F59;">' . htmlspecialchars($user['jobTitle']) . '</span></p> | |
| <p style="padding: 6px 0 5px 0; margin: 0; color: #000000; font-family: Arial; font-size: 13px; font-weight: normal;"> | |
| ' . htmlspecialchars($businessPhone) . '<br> | |
| <a href="mailto:' . htmlspecialchars($user['mail']) . '" style="color: rgba(37, 79, 80, 0.82); line-height: 1.6;">' . htmlspecialchars($user['mail']) . '</a><br> | |
| </p> | |
| '; | |
| } | |
| /** | |
| * Create signature list for user | |
| */ | |
| private function createList($user, $name) { | |
| $email = $user['mail']; | |
| $data = json_encode([ | |
| [ | |
| "itemClass" => "RoamingSetting", | |
| "name" => "roaming_signature_list", | |
| "scope" => $email, | |
| "secondaryKey" => "roaming_signature_list", | |
| "type" => "BlobArray", | |
| "value" => $name | |
| ] | |
| ]); | |
| $context = stream_context_create([ | |
| 'http' => [ | |
| 'method' => 'POST', | |
| 'header' => [ | |
| "Authorization: Bearer {$this->outlookAccessToken}", | |
| 'Content-Type: application/json', | |
| 'Content-Length: ' . strlen($data), | |
| 'x-owa-explicitlogonuser: ' . $email, | |
| 'x-anchormailbox: ' . $email, | |
| 'x-islargesetting: false', | |
| 'x-overridetimestamp: false', | |
| ], | |
| 'content' => $data, | |
| 'proxy' => 'localhost:8888', | |
| ], | |
| 'ssl' => [ | |
| 'verify_peer' => false, | |
| 'verify_peer_name' => false, | |
| ] | |
| ]); | |
| $url = "https://outlook.office.com/ows/v1/OutlookCloudSettings/settings/account"; | |
| $response = file_get_contents($url, false, $context); | |
| if ($response !== false) { | |
| echo "Created roaming signature list for {$email}\n"; | |
| return true; | |
| } | |
| return false; | |
| } | |
| private function getExistingSignatureNames($user) { | |
| $email = $user['mail']; | |
| $context = stream_context_create([ | |
| 'http' => [ | |
| 'method' => 'GET', | |
| 'header' => [ | |
| "Authorization: Bearer {$this->outlookAccessToken}", | |
| 'x-owa-explicitlogonuser: ' . $email, | |
| 'x-anchormailbox: ' . $email, | |
| 'x-islargesetting: false', | |
| 'x-overridetimestamp: false', | |
| ], | |
| 'proxy' => 'localhost:8888', | |
| ], | |
| 'ssl' => [ | |
| 'verify_peer' => false, | |
| 'verify_peer_name' => false, | |
| ] | |
| ]); | |
| $url = "https://outlook.office.com/ows/v1/OutlookCloudSettings/settings/account?settingname=roaming_signature_list"; | |
| $response = file_get_contents($url, false, $context); | |
| if ($response === false) { | |
| return false; | |
| } | |
| $signResponse = json_decode($response, true); | |
| foreach ($signResponse as $item) { | |
| if (strtolower($item['name']) === 'roaming_signature_list') { | |
| $roamingSigList = $item; | |
| break; | |
| } | |
| } | |
| if (!$roamingSigList) { | |
| return []; | |
| } | |
| return explode(',', $roamingSigList['value']); | |
| } | |
| /** | |
| * Get signature name for user | |
| */ | |
| private function getSignatureName($user) { | |
| $signatureNames = $this->getExistingSignatureNames($user); | |
| if (count($signatureNames) === 1) { | |
| return $signatureNames[0]; | |
| } | |
| // Check for roaming_new_signature | |
| foreach ($signResponse as $item) { | |
| if (strtolower($item['name']) === 'roaming_new_signature' && !empty($item['value'])) { | |
| return $item['value']; | |
| } | |
| } | |
| // Check for roaming_reply_signature | |
| foreach ($signResponse as $item) { | |
| if (strtolower($item['name']) === 'roaming_reply_signature' && !empty($item['value'])) { | |
| return $item['value']; | |
| } | |
| } | |
| return $signatureNames[0]; | |
| } | |
| /** | |
| * Change user signature | |
| */ | |
| private function changeUserSignature($user, $signatureName) { | |
| $email = $user['mail']; | |
| $data = json_encode([ | |
| [ | |
| "itemClass" => "RoamingSetting", | |
| "metadata" => "encoding:utf-8", | |
| "name" => $signatureName, | |
| "parentSetting" => "roaming_signature_list", | |
| "scope" => $email, | |
| "secondaryKey" => "htm", | |
| "timestamp" => $this->getTimestampTicks(), | |
| "type" => "Blob", | |
| "value" => $this->generateSignatureHtml($user), | |
| "value@is.Large" => true | |
| ], | |
| [ | |
| "itemClass" => "RoamingSetting", | |
| "metadata" => "encoding:utf-8", | |
| "name" => $signatureName, | |
| "parentSetting" => "roaming_signature_list", | |
| "scope" => $email, | |
| "secondaryKey" => "txt", | |
| "timestamp" => $this->getTimestampTicks(), | |
| "type" => "Blob", | |
| "value" => $this->generateSignatureText($user), | |
| "value@is.Large" => true | |
| ] | |
| ]); | |
| $context = stream_context_create([ | |
| 'http' => [ | |
| 'method' => 'PATCH', | |
| 'header' => [ | |
| "Authorization: Bearer {$this->outlookAccessToken}", | |
| 'Content-Type: application/json', | |
| 'x-owa-explicitlogonuser: ' . $email, | |
| 'x-anchormailbox: ' . $email, | |
| 'x-islargesetting: true', | |
| 'x-overridetimestamp: true', | |
| 'Content-Length: ' . strlen($data) | |
| ], | |
| 'content' => $data | |
| ] | |
| ]); | |
| $url = "https://outlook.office.com/ows/v1/OutlookCloudSettings/settings/account"; | |
| $response = file_get_contents($url, false, $context); | |
| if ($response === false) { | |
| // Get error details from HTTP response headers | |
| $error = error_get_last(); | |
| echo "Failed to set settings for {$email}: " . ($error['message'] ?? 'Unknown error') . "\n"; | |
| return false; | |
| } | |
| $signatureNames = $this->getExistingSignatureNames($user); | |
| $newSignatureNames = array_merge($signatureNames, [$signatureName]); | |
| // Update the list of signatures with a PATCH request | |
| $data = json_encode([ | |
| [ | |
| "itemClass" => "RoamingSetting", | |
| "name" => "roaming_signature_list", | |
| "scope" => $email, | |
| "secondaryKey" => "roaming_signature_list", | |
| "type" => "BlobArray", | |
| "value" => implode(',', $newSignatureNames), | |
| ] | |
| ]); | |
| return true; | |
| } | |
| /** | |
| * Run signature update for a single user | |
| */ | |
| public function runForUser($user) { | |
| $signatureName = $this->getSignatureName($user); | |
| $signatureName = 'Signature_1'; | |
| if (!$signatureName) { | |
| return false; | |
| } | |
| echo "User has signature {$user['mail']}: {$signatureName}\n"; | |
| echo "Setting {$signatureName} for {$user['mail']}\n"; | |
| return $this->changeUserSignature($user, $signatureName); | |
| } | |
| /** | |
| * Run signature update for all users | |
| */ | |
| public function runForAllUsers() { | |
| $users = $this->getUsers(); | |
| foreach ($users as $user) { | |
| $this->runForUser($user); | |
| } | |
| } | |
| /** | |
| * Run signature update for a specific user by email | |
| */ | |
| public function runForSpecificUser($targetEmail) { | |
| $user = $this->getUser($targetEmail); | |
| return $this->runForUser($user); | |
| } | |
| } | |
| // Usage example: | |
| try { | |
| $signatureManager = new RoamingSignatureManager(); | |
| // Run for one specific user | |
| $signatureManager->runForSpecificUser('eion@example.com'); | |
| // Uncomment to run for all users: | |
| // $signatureManager->runForAllUsers(); | |
| } catch (Exception $e) { | |
| echo "Error: " . $e->getMessage() . "\n"; | |
| exit(1); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment