├── .gitignore ├── docs ├── logo.png ├── glpi_network.png └── screenshots │ ├── config.png │ └── config_oauth_mailcollector.png ├── locales ├── en_GB.mo ├── es_AR.mo ├── es_EC.mo ├── es_MX.mo ├── fr_FR.mo ├── pt_BR.mo ├── sk_SK.mo ├── sv_SE.mo ├── oauthimap.pot ├── en_GB.po ├── sv_SE.po ├── fr_FR.po ├── sk_SK.po ├── es_AR.po ├── es_EC.po ├── es_MX.po └── pt_BR.po ├── .tx └── config ├── .github ├── workflows │ ├── release.yml │ ├── locales-update-source.yml │ ├── locales-sync.yml │ ├── auto-tag-new-version.yml │ ├── label-commenter.yml │ ├── continuous-integration.yml │ └── close_stale_issue.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.yml ├── dependabot.yml ├── pull_request_template.md └── label-commenter-config.yml ├── .twig_cs.dist.php ├── CHANGELOG.md ├── .php-cs-fixer.php ├── composer.json ├── phpstan.neon ├── psalm.xml ├── SECURITY.md ├── tools └── HEADER ├── README.md ├── inc ├── oauth │ └── ownerdetails.class.php ├── hook.class.php ├── provider │ ├── google.class.php │ ├── providerinterface.class.php │ └── azure.class.php ├── imap │ ├── imapoauthstorage.class.php │ └── imapoauthprotocol.class.php ├── mailcollectorfeature.class.php ├── application.class.php └── authorization.class.php ├── front ├── authorization.callback.php ├── authorization.process.php └── authorization.form.php ├── ajax └── dropdownAuthorization.php ├── templates └── application_form_extra.html.twig ├── hook.php ├── setup.php ├── rector.php ├── oauthimap.xml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | vendor/ 3 | .gh_token 4 | *.min.* 5 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluginsGLPI/oauthimap/HEAD/docs/logo.png -------------------------------------------------------------------------------- /locales/en_GB.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluginsGLPI/oauthimap/HEAD/locales/en_GB.mo -------------------------------------------------------------------------------- /locales/es_AR.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluginsGLPI/oauthimap/HEAD/locales/es_AR.mo -------------------------------------------------------------------------------- /locales/es_EC.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluginsGLPI/oauthimap/HEAD/locales/es_EC.mo -------------------------------------------------------------------------------- /locales/es_MX.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluginsGLPI/oauthimap/HEAD/locales/es_MX.mo -------------------------------------------------------------------------------- /locales/fr_FR.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluginsGLPI/oauthimap/HEAD/locales/fr_FR.mo -------------------------------------------------------------------------------- /locales/pt_BR.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluginsGLPI/oauthimap/HEAD/locales/pt_BR.mo -------------------------------------------------------------------------------- /locales/sk_SK.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluginsGLPI/oauthimap/HEAD/locales/sk_SK.mo -------------------------------------------------------------------------------- /locales/sv_SE.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluginsGLPI/oauthimap/HEAD/locales/sv_SE.mo -------------------------------------------------------------------------------- /docs/glpi_network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluginsGLPI/oauthimap/HEAD/docs/glpi_network.png -------------------------------------------------------------------------------- /docs/screenshots/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluginsGLPI/oauthimap/HEAD/docs/screenshots/config.png -------------------------------------------------------------------------------- /docs/screenshots/config_oauth_mailcollector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluginsGLPI/oauthimap/HEAD/docs/screenshots/config_oauth_mailcollector.png -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [o:teclib:p:glpi-project-plugin-oauthimap:r:oauthimap-pot] 5 | file_filter = locales/.po 6 | source_file = locales/oauthimap.pot 7 | source_lang = en 8 | type = PO 9 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Publish release" 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish-release: 10 | permissions: 11 | contents: "write" 12 | name: "Publish release" 13 | uses: "glpi-project/plugin-release-workflows/.github/workflows/publish-release.yml@v1" 14 | -------------------------------------------------------------------------------- /.github/workflows/locales-update-source.yml: -------------------------------------------------------------------------------- 1 | name: "Update locales sources" 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | 8 | jobs: 9 | push-on-transifex: 10 | name: "Push locales sources" 11 | uses: "glpi-project/plugin-translation-workflows/.github/workflows/transifex-push-sources.yml@v1" 12 | secrets: 13 | transifex-token: "${{ secrets.TRANSIFEX_TOKEN }}" 14 | -------------------------------------------------------------------------------- /.github/workflows/locales-sync.yml: -------------------------------------------------------------------------------- 1 | name: "Synchronize locales" 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 1-5" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | sync-with-transifex: 10 | name: "Sync with transifex" 11 | uses: "glpi-project/plugin-translation-workflows/.github/workflows/transifex-sync.yml@v1" 12 | secrets: 13 | github-token: "${{ secrets.LOCALES_SYNC_TOKEN }}" 14 | transifex-token: "${{ secrets.TRANSIFEX_TOKEN }}" 15 | -------------------------------------------------------------------------------- /.twig_cs.dist.php: -------------------------------------------------------------------------------- 1 | in(__DIR__ . '/templates') 11 | ->name('*.html.twig') 12 | ->ignoreVCSIgnored(true); 13 | 14 | return Config::create() 15 | ->setFinder($finder) 16 | ->setRuleSet(GlpiTwigRuleset::class) 17 | ; 18 | -------------------------------------------------------------------------------- /.github/workflows/auto-tag-new-version.yml: -------------------------------------------------------------------------------- 1 | name: "Automatically tag new version" 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | - "**/bugfixes" 8 | paths: 9 | - "setup.php" 10 | 11 | jobs: 12 | auto-tag-new-version: 13 | name: "Automatically tag new version" 14 | uses: "glpi-project/plugin-release-workflows/.github/workflows/auto-tag-new-version.yml@v1" 15 | secrets: 16 | github-token: "${{ secrets.AUTOTAG_TOKEN }}" 17 | -------------------------------------------------------------------------------- /.github/workflows/label-commenter.yml: -------------------------------------------------------------------------------- 1 | name: "Label commenter" 2 | 3 | on: 4 | issues: 5 | types: 6 | - "labeled" 7 | - "unlabeled" 8 | 9 | jobs: 10 | comment: 11 | permissions: 12 | contents: "read" 13 | issues: "write" 14 | runs-on: "ubuntu-latest" 15 | steps: 16 | - name: "Checkout" 17 | uses: "actions/checkout@v6" 18 | 19 | - name: "Label commenter" 20 | uses: "peaceiris/actions-label-commenter@v1" 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [1.5.0] - 2025-09-29 9 | 10 | ### Added 11 | 12 | - GLPI 11 compatibility 13 | 14 | ## [1.4.4] - 2025-09-19 15 | 16 | ## Fix 17 | 18 | - Fix UI for the copy to clipboard button 19 | - Remove useless params for proxy configuration 20 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__) 8 | ->name('*.php'); 9 | 10 | $config = new Config(); 11 | 12 | $rules = [ 13 | '@PER-CS2.0' => true, 14 | 'trailing_comma_in_multiline' => ['elements' => ['arguments', 'array_destructuring', 'arrays']], // For PHP 7.4 compatibility 15 | ]; 16 | 17 | return $config 18 | ->setRules($rules) 19 | ->setFinder($finder) 20 | ->setUsingCache(false); 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "php": ">=8.2" 4 | }, 5 | "require-dev": { 6 | "glpi-project/tools": "^0.8" 7 | }, 8 | "provide": { 9 | "guzzlehttp/guzzle": "*" 10 | }, 11 | "config": { 12 | "optimize-autoloader": true, 13 | "platform": { 14 | "php": "8.2.99" 15 | }, 16 | "sort-packages": true 17 | }, 18 | "autoload-dev": { 19 | "psr-4": { 20 | "Glpi\\Tools\\": "../../tools/src/" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: "GLPI professional support" 4 | url: "https://services.glpi-network.com" 5 | about: "Get professional support from the editor and a network of local partners." 6 | - name: "Find an official partner" 7 | url: "https://glpi-project.org/partners/" 8 | about: "Get support to deploy GLPI in a professional manner." 9 | - name: "GLPI Community Forum" 10 | url: "https://forum.glpi-project.org" 11 | about: "Ask questions and get help from the community." 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Ensure GitHub Actions are used in their latest version 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | 9 | # Strategy for composer dependencies 10 | - package-ecosystem: "composer" 11 | directory: "/" 12 | schedule: 13 | interval: "monthly" 14 | allow: 15 | - dependency-type: "direct" 16 | open-pull-requests-limit: 100 17 | versioning-strategy: "increase" 18 | groups: 19 | dev-dependencies: 20 | dependency-type: "development" 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Checklist before requesting a review 2 | 3 | *Please delete options that are not relevant.* 4 | 5 | - [ ] I have performed a self-review of my code. 6 | - [ ] I have added tests (when available) that prove my fix is effective or that my feature works. 7 | - [ ] I have updated the CHANGELOG with a short functional description of the fix or new feature. 8 | - [ ] This change requires a documentation update. 9 | 10 | ## Description 11 | 12 | - It fixes # (issue number, if applicable) 13 | - Here is a brief description of what this PR does 14 | 15 | ## Screenshots (if appropriate): 16 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - ../../vendor/glpi-project/phpstan-glpi/extension.neon 3 | - ../../vendor/phpstan/phpstan-deprecation-rules/rules.neon 4 | - ../../vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon 5 | 6 | parameters: 7 | level: 5 8 | paths: 9 | - ajax 10 | - front 11 | - inc 12 | - hook.php 13 | - setup.php 14 | scanDirectories: 15 | - ../../src 16 | bootstrapFiles: 17 | - ../../stubs/glpi_constants.php 18 | - ../../vendor/autoload.php 19 | - setup.php 20 | treatPhpDocTypesAsCertain: false 21 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | **⚠️ Please never use standard issues to report security problems; vulnerabilities are published once a fix release is available. ⚠️** 4 | 5 | ## Reporting a Vulnerability 6 | 7 | If you found a security issue, please contact us by: 8 | 9 | - [our huntr page](https://huntr.dev/repos/pluginsGLPI/oauthimap/) 10 | - a mail to \[glpi-security AT ow2.org\] 11 | 12 | You should provide us all details about the issue and the way to reproduce it. 13 | You may also provide a script that can be used to check the issue exists. 14 | 15 | Once the report will be handled, and if the issue is not yet fixed (or in progress) 16 | we'll add it to the GitHub security tab, and add you as observer. Meanwhile, 17 | you will reserve a CVE for the issue. 18 | 19 | Thank you for improving the security of GLPI and its plugins. 20 | 21 | ## Supported Versions 22 | 23 | We follow the same version support policy as GLPI. 24 | This means that we provide security patches to versions of the plugin that target a version of GLPI itself maintained from a security point of view. 25 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: "Continuous integration" 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | tags: 8 | - "*" 9 | pull_request: 10 | schedule: 11 | - cron: "0 0 * * *" 12 | workflow_dispatch: 13 | 14 | concurrency: 15 | group: "${{ github.workflow }}-${{ github.ref }}" 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | generate-ci-matrix: 20 | name: "Generate CI matrix" 21 | uses: "glpi-project/plugin-ci-workflows/.github/workflows/generate-ci-matrix.yml@v1" 22 | with: 23 | glpi-version: "11.0.x" 24 | ci: 25 | name: "GLPI ${{ matrix.glpi-version }} - php:${{ matrix.php-version }} - ${{ matrix.db-image }}" 26 | needs: "generate-ci-matrix" 27 | strategy: 28 | fail-fast: false 29 | matrix: ${{ fromJson(needs.generate-ci-matrix.outputs.matrix) }} 30 | uses: "glpi-project/plugin-ci-workflows/.github/workflows/continuous-integration.yml@v1" 31 | with: 32 | plugin-key: "oauthimap" 33 | glpi-version: "${{ matrix.glpi-version }}" 34 | php-version: "${{ matrix.php-version }}" 35 | db-image: "${{ matrix.db-image }}" 36 | -------------------------------------------------------------------------------- /tools/HEADER: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------- 2 | oauthimap plugin for GLPI 3 | ------------------------------------------------------------------------- 4 | 5 | LICENSE 6 | 7 | This file is part of oauthimap plugin. 8 | 9 | This plugin is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | This plugin is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this plugin. If not, see . 21 | ------------------------------------------------------------------------- 22 | @copyright Copyright (C) 2020-2025 by Teclib' 23 | @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 24 | @link https://services.glpi-network.com 25 | ------------------------------------------------------------------------- 26 | -------------------------------------------------------------------------------- /.github/label-commenter-config.yml: -------------------------------------------------------------------------------- 1 | labels: 2 | - name: "invalid" 3 | labeled: 4 | issue: 5 | body: | 6 | This issue has been closed because you did not provide the requested information. 7 | action: "close" 8 | - name: "support" 9 | labeled: 10 | issue: 11 | body: | 12 | This issue has been closed as we only track bugs here. 13 | 14 | You can get community support on [forums](https://forum.glpi-project.org/) or you can consider [taking a subscription](https://glpi-project.org/subscriptions/) to get professional support. 15 | You can also [contact GLPI editor team](https://portal.glpi-network.com/contact-us) directly. 16 | action: close 17 | - name: "feature suggestion" 18 | labeled: 19 | issue: 20 | body: | 21 | This issue has been closed as we only track bugs here. 22 | 23 | You can open a topic to discuss with community about this enhancement on [suggestion website](https://glpi.userecho.com/). 24 | You can also [contact GLPI editor team](https://portal.glpi-network.com/contact-us) directly if you are willing to sponsor this feature. 25 | action: close 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Oauth IMAP client for GLPI 2 | 3 | This plugins provides IMAP SASL XOAUTH2 authentication mechanism for mail receivers. 4 | 5 | Currently implemented for: 6 | 7 | * [Google (G Suite and Gmail)](https://developers.google.com/gmail/imap/xoauth2-protocol) 8 | * [Microsoft (Office 365 via Azure AD)](https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth) 9 | 10 | ![Configuration page](docs/screenshots/config.png) 11 | ![Mail receiver setup](docs/screenshots/config_oauth_mailcollector.png) 12 | 13 | ## Documentation 14 | 15 | We maintain a detailed documentation here -> [Documentation](https://glpi-plugins.readthedocs.io/en/latest/oauthimap/index.html) 16 | 17 | ## Professional Services 18 | 19 | ![GLPI Network](docs/glpi_network.png "GLPI network") 20 | 21 | The GLPI Network services are available through our [Partner's Network](http://www.teclib-edition.com/en/partners/). We provide special training, bug fixes with editor subscription, contributions for new features, and more. 22 | 23 | Obtain a personalized service experience, associated with benefits and opportunities. 24 | 25 | ## Copying 26 | 27 | * **Code**: you can redistribute it and/or modify 28 | it under the terms of the GNU General Public License ([GPL-2.0](https://www.gnu.org/licenses/gpl-2.0.en.html)). 29 | -------------------------------------------------------------------------------- /inc/oauth/ownerdetails.class.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | namespace GlpiPlugin\Oauthimap\Oauth; 32 | 33 | class OwnerDetails 34 | { 35 | public $email; 36 | 37 | public $firstname; 38 | 39 | public $lastname; 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/close_stale_issue.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues' 2 | on: 3 | schedule: 4 | - cron: '0 8 * * *' 5 | 6 | jobs: 7 | stale: 8 | if: github.repository == 'pluginsGLPI/oauthimap' 9 | permissions: 10 | issues: write # for actions/stale to close stale issues 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/stale@v10 14 | with: 15 | stale-issue-message: >- 16 | There has been no activity on this issue for some time and therefore it is considered stale 17 | and will be closed automatically in 10 days. 18 | 19 | 20 | If this issue is related to a bug, please try to reproduce on latest release. If the problem persist, 21 | feel free to add a comment to revive this issue. 22 | 23 | If it is related to a new feature, please open a topic to discuss with community about this enhancement 24 | on [suggestion website](https://glpi.userecho.com/). 25 | 26 | 27 | You may also consider taking a [subscription](https://glpi-project.org/subscriptions/) to get professionnal 28 | support or [contact GLPI editor team](https://portal.glpi-network.com/contact-us) directly. 29 | days-before-issue-stale: 15 30 | days-before-pr-stale: -1 # PR will be marked as stale manually. 31 | days-before-close: 5 32 | exempt-issue-labels: "bug,enhancement,question,security" # Issues with "bug", "enhancement", "question" or "security" labels will not be marked as stale 33 | exempt-all-milestones: true # Do not check issues/PR with defined milestone. 34 | ascending: true # First check older issues/PR. 35 | operations-per-run: 750 # Max API calls per run. 36 | -------------------------------------------------------------------------------- /front/authorization.callback.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | // Session cookie will not be accessible when user will be redirected from provider website 32 | // if `session.cookie_samesite` configuration value is `strict`. 33 | // Redirecting on self using `http-equiv="refresh"` will get around this limitation. 34 | $url = htmlspecialchars( 35 | str_replace('authorization.callback.php', 'authorization.process.php', $_SERVER['REQUEST_URI']), 36 | ); 37 | 38 | echo << 40 | 41 | 42 | 43 | 44 | 45 | HTML; 46 | -------------------------------------------------------------------------------- /inc/hook.class.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | use GlpiPlugin\Oauthimap\MailCollectorFeature; 32 | 33 | class PluginOauthimapHook 34 | { 35 | /** 36 | * Handle post_item_form hook. 37 | * 38 | * @param array $params 39 | * 40 | * @return void 41 | */ 42 | public static function postItemForm(array $params): void 43 | { 44 | $item = $params['item']; 45 | 46 | if (!is_object($item)) { 47 | return; 48 | } 49 | 50 | switch (get_class($item)) { 51 | case MailCollector::class: 52 | MailCollectorFeature::alterMailCollectorForm(); 53 | break; 54 | case PluginOauthimapApplication::class: 55 | PluginOauthimapApplication::showFormExtra((int) $item->fields[PluginOauthimapApplication::getIndexName()]); 56 | break; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ajax/dropdownAuthorization.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | header('Content-Type: text/html; charset=UTF-8'); 32 | Html::header_nocache(); 33 | 34 | Session::checkLoginUser(); 35 | 36 | /** @var DBmysql $DB */ 37 | global $DB; 38 | 39 | $iterator = $DB->request( 40 | [ 41 | 'FROM' => PluginOauthimapAuthorization::getTable(), 42 | 'WHERE' => [ 43 | PluginOauthimapApplication::getForeignKeyField() => $_POST['application_id'] ?? null, 44 | ], 45 | ], 46 | ); 47 | $authorizations = [ 48 | '-1' => __s('Create authorization for another user', 'oauthimap'), 49 | ]; 50 | $value = -1; 51 | foreach ($iterator as $row) { 52 | $authorizations[$row['id']] = $row['email']; 53 | if (array_key_exists('selected', $_POST) && $row['email'] == $_POST['selected']) { 54 | $value = $row['id']; 55 | } 56 | } 57 | 58 | Dropdown::showFromArray( 59 | PluginOauthimapAuthorization::getForeignKeyField(), 60 | $authorizations, 61 | [ 62 | 'display_emptychoice' => false, 63 | 'value' => $value, 64 | ], 65 | ); 66 | -------------------------------------------------------------------------------- /inc/provider/google.class.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | namespace GlpiPlugin\Oauthimap\Provider; 32 | 33 | use GlpiPlugin\Oauthimap\Oauth\OwnerDetails; 34 | use League\OAuth2\Client\Provider\GoogleUser; 35 | use League\OAuth2\Client\Token\AccessToken; 36 | 37 | class Google extends \League\OAuth2\Client\Provider\Google implements ProviderInterface 38 | { 39 | public static function getName(): string 40 | { 41 | return 'Google'; 42 | } 43 | 44 | public static function getIcon(): string 45 | { 46 | return 'fa-google'; 47 | } 48 | 49 | public function getOwnerDetails(AccessToken $token): ?OwnerDetails 50 | { 51 | /** @var GoogleUser $owner */ 52 | $owner = $this->getResourceOwner($token); 53 | 54 | $owner_details = new OwnerDetails(); 55 | $owner_details->email = $owner->getEmail(); 56 | $owner_details->firstname = $owner->getFirstName(); 57 | $owner_details->lastname = $owner->getLastName(); 58 | 59 | return $owner_details; 60 | } 61 | 62 | public function getDefaultHost(): string 63 | { 64 | return 'imap.gmail.com'; 65 | } 66 | 67 | public function getDefaultPort(): ?int 68 | { 69 | return 993; 70 | } 71 | 72 | public function getDefaultSslFlag(): ?string 73 | { 74 | return 'SSL'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /inc/provider/providerinterface.class.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | namespace GlpiPlugin\Oauthimap\Provider; 32 | 33 | use GlpiPlugin\Oauthimap\Oauth\OwnerDetails; 34 | use League\OAuth2\Client\Token\AccessToken; 35 | 36 | interface ProviderInterface 37 | { 38 | /** 39 | * Return provider name. 40 | * 41 | * @return string 42 | */ 43 | public static function getName(): string; 44 | 45 | /** 46 | * Return provider icon (Font-Awesome identifier). 47 | * 48 | * @return string 49 | */ 50 | public static function getIcon(): string; 51 | 52 | /** 53 | * Return token owner details. 54 | * 55 | * @param AccessToken $token 56 | * 57 | * @return OwnerDetails|null 58 | */ 59 | public function getOwnerDetails(AccessToken $token): ?OwnerDetails; 60 | 61 | /** 62 | * Returns default host for IMAP connection. 63 | * 64 | * @return string 65 | */ 66 | public function getDefaultHost(): string; 67 | 68 | /** 69 | * Returns default port for IMAP connection. 70 | * 71 | * @return int|null 72 | */ 73 | public function getDefaultPort(): ?int; 74 | 75 | /** 76 | * Returns default SSL flag ('SSL', 'TLS' or null) for IMAP connection. 77 | * 78 | * @return string|null 79 | */ 80 | public function getDefaultSslFlag(): ?string; 81 | } 82 | -------------------------------------------------------------------------------- /templates/application_form_extra.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | # ------------------------------------------------------------------------- 3 | # oauthimap plugin for GLPI 4 | # ------------------------------------------------------------------------- 5 | # 6 | # LICENSE 7 | # 8 | # This file is part of oauthimap plugin. 9 | # 10 | # This plugin is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This plugin is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this plugin. If not, see . 22 | # ------------------------------------------------------------------------- 23 | # @copyright Copyright (C) 2020-2025 by Teclib' 24 | # @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 25 | # @link https://services.glpi-network.com 26 | # ------------------------------------------------------------------------- 27 | #} 28 | 29 | {% import 'components/form/fields_macros.html.twig' as fields %} 30 | 31 | {{ fields.textField( 32 | 'callback_url', 33 | callback_url, 34 | __('Callback url', 'oauthimap'), { 35 | copyable: true 36 | } 37 | ) }} 38 | 39 | 65 | -------------------------------------------------------------------------------- /inc/provider/azure.class.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | namespace GlpiPlugin\Oauthimap\Provider; 32 | 33 | use GlpiPlugin\Oauthimap\Oauth\OwnerDetails; 34 | use League\OAuth2\Client\Token\AccessToken; 35 | use TheNetworg\OAuth2\Client\Provider\AzureResourceOwner; 36 | 37 | class Azure extends \TheNetworg\OAuth2\Client\Provider\Azure implements ProviderInterface 38 | { 39 | public static function getName(): string 40 | { 41 | return 'Azure'; 42 | } 43 | 44 | public static function getIcon(): string 45 | { 46 | return 'fa-windows'; 47 | } 48 | 49 | public function getOwnerDetails(AccessToken $token): ?OwnerDetails 50 | { 51 | /** @var AzureResourceOwner $owner */ 52 | $owner = $this->getResourceOwner($token); 53 | 54 | $owner_details = new OwnerDetails(); 55 | if (($email = $owner->claim('email')) !== null) { 56 | $owner_details->email = $email; 57 | } elseif (($upn = $owner->claim('upn')) !== null) { 58 | $owner_details->email = $upn; 59 | } 60 | $owner_details->firstname = $owner->getFirstName(); 61 | $owner_details->lastname = $owner->getLastName(); 62 | 63 | return $owner_details; 64 | } 65 | 66 | public function getDefaultHost(): string 67 | { 68 | return 'outlook.office365.com'; 69 | } 70 | 71 | public function getDefaultPort(): ?int 72 | { 73 | return 993; 74 | } 75 | 76 | public function getDefaultSslFlag(): ?string 77 | { 78 | return 'SSL'; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /front/authorization.process.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | $application = new PluginOauthimapApplication(); 32 | $authorization = new PluginOauthimapAuthorization(); 33 | 34 | $application_id = $_SESSION[PluginOauthimapApplication::getForeignKeyField()] ?? null; 35 | 36 | $success = false; 37 | if ( 38 | array_key_exists('error', $_GET) && !empty($_GET['error']) 39 | || array_key_exists('error_description', $_GET) && !empty($_GET['error_description']) 40 | ) { 41 | // Got an error, probably user denied access 42 | Session::addMessageAfterRedirect( 43 | sprintf(__s('Authorization failed with error: %s', 'oauthimap'), htmlspecialchars($_GET['error_description'] ?? $_GET['error'])), 44 | false, 45 | ERROR, 46 | ); 47 | } elseif ( 48 | $application_id === null 49 | || !array_key_exists('state', $_GET) 50 | || !array_key_exists('oauth2state', $_SESSION) 51 | || $_GET['state'] !== $_SESSION['oauth2state'] 52 | ) { 53 | Session::addMessageAfterRedirect(__s('Unable to verify authorization code', 'oauthimap'), false, ERROR); 54 | } elseif (!array_key_exists('code', $_GET)) { 55 | Session::addMessageAfterRedirect(__s('Unable to get authorization code', 'oauthimap'), false, ERROR); 56 | } elseif (!$authorization->createFromCode($application_id, $_GET['code'])) { 57 | Session::addMessageAfterRedirect(__s('Unable to save authorization code', 'oauthimap'), false, ERROR); 58 | } else { 59 | $success = true; 60 | } 61 | 62 | $callback_callable = $_SESSION['plugin_oauthimap_callback_callable'] ?? null; 63 | 64 | if (is_callable($callback_callable)) { 65 | $callback_params = $_SESSION['plugin_oauthimap_callback_params'] ?? []; 66 | call_user_func_array($callback_callable, [$success, $authorization, $callback_params]); 67 | } 68 | 69 | $url = $application->getFromDB($application_id) ? $application->getLinkURL() : $application->getSearchURL(true); 70 | 71 | Html::redirect($url); 72 | -------------------------------------------------------------------------------- /inc/imap/imapoauthstorage.class.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | namespace GlpiPlugin\Oauthimap\Imap; 32 | 33 | use Laminas\Mail\Storage\Exception\ExceptionInterface; 34 | use Laminas\Mail\Storage\Exception\InvalidArgumentException; 35 | use Laminas\Mail\Storage\Exception\RuntimeException; 36 | use Laminas\Mail\Storage\Imap; 37 | 38 | class ImapOauthStorage extends Imap 39 | { 40 | public function __construct($params) 41 | { 42 | if (is_array($params)) { 43 | $params = (object) $params; 44 | } 45 | 46 | $this->has['flags'] = true; 47 | 48 | if ($params instanceof ImapOauthProtocol) { 49 | $this->protocol = $params; 50 | try { 51 | $this->selectFolder('INBOX'); 52 | } catch (ExceptionInterface $e) { 53 | throw new RuntimeException('cannot select INBOX, is this a valid transport?', 0, $e); 54 | } 55 | 56 | return; 57 | } 58 | 59 | if (!isset($params->application_id)) { 60 | throw new InvalidArgumentException('Oauth credentials must be defined'); 61 | } 62 | 63 | if (!isset($params->user)) { 64 | throw new InvalidArgumentException('need at least user in params'); 65 | } 66 | 67 | $host = $params->host ?? 'localhost'; 68 | $password = ''; // No password used in Oauth process 69 | $port = $params->port ?? null; 70 | $ssl = $params->ssl ?? false; 71 | 72 | $this->protocol = new ImapOauthProtocol($params->application_id); 73 | 74 | if (isset($params->novalidatecert)) { 75 | $this->protocol->setNoValidateCert((bool) $params->novalidatecert); 76 | } 77 | 78 | $this->protocol->connect($host, $port, $ssl); 79 | if (!$this->protocol->login($params->user, $password)) { 80 | throw new RuntimeException('cannot login, user or password wrong'); 81 | } 82 | $this->selectFolder($params->folder ?? 'INBOX'); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve oauthimap 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | 8 | Dear GLPI plugin user. 9 | 10 | **⚠️ Please never use standard issues to report security problems. See [security policy](https://github.com/pluginsGLPI/oauthimap/security/policy) for more details. ⚠️** 11 | 12 | BEFORE SUBMITTING YOUR ISSUE, please make sure to read and follow these steps: 13 | 14 | * We do not track feature requests nor enhancements here. Propose them on the [suggest dedicated site](https://suggest.glpi-project.org). 15 | * Keep this tracker in ENGLISH. If you want support in your language, the [community forum](https://forum.glpi-project.org) is the best place. 16 | * Always try to reproduce your issue at least on latest stable release. 17 | 18 | The GLPI team. 19 | - type: markdown 20 | attributes: 21 | value: | 22 | ## Professional Support 23 | 24 | We do not guarantee any processing / resolution time for community issues. 25 | 26 | If you need a quick fix or any guarantee, you should consider to buy a GLPI Network Subscription. 27 | 28 | More information here: https://glpi-project.org/subscriptions/ 29 | - type: checkboxes 30 | id: terms 31 | attributes: 32 | label: Code of Conduct 33 | description: By submitting this issue, you agree to follow hereinabove rules and [Contribution guide](https://github.com/glpi-project/glpi/blob/main/CONTRIBUTING.md) 34 | options: 35 | - label: I agree to follow this project's Code of Conduct 36 | validations: 37 | required: true 38 | - type: checkboxes 39 | attributes: 40 | label: Is there an existing issue for this? 41 | description: Please search to see if an issue already exists for the bug you encountered. 42 | options: 43 | - label: I have searched the existing issues 44 | validations: 45 | required: true 46 | - type: input 47 | id: glpi-version 48 | attributes: 49 | label: GLPI Version 50 | description: What version of our GLPI are you running? 51 | validations: 52 | required: true 53 | - type: input 54 | id: plugin-version 55 | attributes: 56 | label: Plugin version 57 | description: What version of `oauthimap` are you running? 58 | validations: 59 | required: true 60 | - type: textarea 61 | attributes: 62 | label: Bug description 63 | description: A concise description of the problem you are experiencing and what you expected to happen. 64 | validations: 65 | required: false 66 | - type: textarea 67 | id: logs 68 | attributes: 69 | label: Relevant log output 70 | description: | 71 | Please copy and paste any relevant log output. Find them in `*-error.log` files under `glpi/files/_log/`. 72 | 73 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 74 | render: shell 75 | - type: input 76 | id: url 77 | attributes: 78 | label: Page URL 79 | description: If applicable, page URL where the bug happens. 80 | validations: 81 | required: false 82 | - type: textarea 83 | attributes: 84 | label: Steps To reproduce 85 | description: Steps to reproduce the behavior. 86 | placeholder: | 87 | 1. With this config... 88 | 2. Go to... 89 | 3. Scroll down to... 90 | 4. See error... 91 | validations: 92 | required: false 93 | - type: textarea 94 | attributes: 95 | label: Your GLPI setup information 96 | description: Please copy and paste information you will find in GLPI in `Setup > General` menu, `System` tab. 97 | validations: 98 | required: false 99 | - type: textarea 100 | attributes: 101 | label: Anything else? 102 | description: Add any other context about the problem here. 103 | validations: 104 | required: false 105 | -------------------------------------------------------------------------------- /front/authorization.form.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | use Glpi\Exception\Http\BadRequestHttpException; 32 | 33 | /** 34 | * ------------------------------------------------------------------------- 35 | * oauthimap plugin for GLPI 36 | * ------------------------------------------------------------------------- 37 | * 38 | * LICENSE 39 | * 40 | * This file is part of oauthimap plugin. 41 | * 42 | * This plugin is free software: you can redistribute it and/or modify 43 | * it under the terms of the GNU General Public License as published by 44 | * the Free Software Foundation, either version 3 of the License, or 45 | * (at your option) any later version. 46 | * 47 | * This plugin is distributed in the hope that it will be useful, 48 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 49 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 50 | * GNU General Public License for more details. 51 | * 52 | * You should have received a copy of the GNU General Public License 53 | * along with this plugin. If not, see . 54 | * ------------------------------------------------------------------------- 55 | * @copyright Copyright (C) 2020-2025 by Teclib' 56 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 57 | * @link https://services.glpi-network.com 58 | * ------------------------------------------------------------------------- 59 | */ 60 | 61 | $authorization = new PluginOauthimapAuthorization(); 62 | $application = new PluginOauthimapApplication(); 63 | 64 | if (isset($_POST['id']) && isset($_POST['delete'])) { 65 | $authorization->check($_POST['id'], DELETE); 66 | $authorization->delete($_POST); 67 | 68 | Html::back(); 69 | } elseif (isset($_POST['id']) && isset($_POST['update'])) { 70 | $authorization->check($_POST['id'], UPDATE); 71 | if ( 72 | $authorization->update($_POST) 73 | && $application->getFromDB($authorization->fields[$application->getForeignKeyField()]) 74 | ) { 75 | Html::redirect($application->getLinkURL()); 76 | } 77 | 78 | Html::back(); 79 | } elseif (isset($_POST['id']) && isset($_POST['request_authorization'])) { 80 | $application->check($_POST['id'], UPDATE); 81 | $application->redirectToAuthorizationUrl(); 82 | } elseif (isset($_REQUEST['id']) && isset($_REQUEST['diagnose'])) { 83 | $authorization->check($_REQUEST['id'], READ); 84 | 85 | $authorization = new PluginOauthimapAuthorization(); 86 | $application = new PluginOauthimapApplication(); 87 | 88 | Html::popHeader($application::getTypeName(Session::getPluralNumber())); 89 | $authorization->check($_REQUEST['id'], READ); 90 | 91 | $authorization->showDiagnosticForm($_POST); 92 | 93 | Html::popFooter(); 94 | } elseif (isset($_GET['id'])) { 95 | $application = new PluginOauthimapApplication(); 96 | $application->displayCentralHeader(); 97 | $authorization->display( 98 | [ 99 | 'id' => $_GET['id'], 100 | ], 101 | ); 102 | Html::footer(); 103 | } else { 104 | throw new BadRequestHttpException(); 105 | } 106 | -------------------------------------------------------------------------------- /hook.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | use function Safe\glob; 32 | use function Safe\preg_match; 33 | 34 | /** 35 | * ------------------------------------------------------------------------- 36 | * oauthimap plugin for GLPI 37 | * ------------------------------------------------------------------------- 38 | * 39 | * LICENSE 40 | * 41 | * This file is part of oauthimap plugin. 42 | * 43 | * This plugin is free software: you can redistribute it and/or modify 44 | * it under the terms of the GNU General Public License as published by 45 | * the Free Software Foundation, either version 3 of the License, or 46 | * (at your option) any later version. 47 | * 48 | * This plugin is distributed in the hope that it will be useful, 49 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 50 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 51 | * GNU General Public License for more details. 52 | * 53 | * You should have received a copy of the GNU General Public License 54 | * along with this plugin. If not, see . 55 | * ------------------------------------------------------------------------- 56 | * @copyright Copyright (C) 2020-2025 by Teclib' 57 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 58 | * @link https://services.glpi-network.com 59 | * ------------------------------------------------------------------------- 60 | */ 61 | 62 | function plugin_oauthimap_install() 63 | { 64 | $version = plugin_version_oauthimap(); 65 | $migration = new Migration($version['version']); 66 | 67 | // Parse inc directory 68 | foreach (glob(__DIR__ . '/inc/*') as $filepath) { 69 | // Load *.class.php files and get the class name 70 | if (preg_match("/inc.(.+)\.class.php$/", $filepath, $matches) !== 0) { 71 | $classname = 'PluginOauthimap' . ucfirst($matches[1]); 72 | include_once($filepath); 73 | // If the install method exists, load it 74 | if (method_exists($classname, 'install')) { 75 | $classname::install($migration); 76 | } 77 | } 78 | } 79 | $migration->executeMigration(); 80 | 81 | return true; 82 | } 83 | 84 | function plugin_oauthimap_uninstall() 85 | { 86 | $migration = new Migration(PLUGIN_OAUTHIMAP_VERSION); 87 | 88 | // Parse inc directory 89 | foreach (glob(__DIR__ . '/inc/*') as $filepath) { 90 | // Load *.class.php files and get the class name 91 | if (preg_match("/inc.(.+)\.class.php/", $filepath, $matches) !== 0) { 92 | $classname = 'PluginOauthimap' . ucfirst($matches[1]); 93 | include_once($filepath); 94 | // If the install method exists, load it 95 | if (method_exists($classname, 'uninstall')) { 96 | $classname::uninstall($migration); 97 | } 98 | } 99 | } 100 | 101 | return true; 102 | } 103 | 104 | function plugin_oauthimap_getDropdown() 105 | { 106 | $plugin = new Plugin(); 107 | 108 | if ($plugin->isActivated('oauthimap')) { 109 | return [ 110 | 'PluginOauthimapApplication' => PluginOauthimapApplication::getTypeName(Session::getPluralNumber()), 111 | ]; 112 | } 113 | 114 | return []; 115 | } 116 | -------------------------------------------------------------------------------- /locales/oauthimap.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2025-11-26 01:41+0000\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 20 | 21 | #: templates/application_form_extra.html.twig 22 | msgid "Callback url" 23 | msgstr "" 24 | 25 | #: setup.php:102 inc/application.class.php:45 26 | msgid "OAuth IMAP" 27 | msgid_plural "OAuth IMAP" 28 | msgstr[0] "" 29 | msgstr[1] "" 30 | 31 | #: inc/application.class.php:88 inc/application.class.php:122 32 | msgid "Oauth provider" 33 | msgstr "" 34 | 35 | #: inc/application.class.php:94 inc/application.class.php:131 36 | msgid "Client ID" 37 | msgstr "" 38 | 39 | #: inc/application.class.php:100 40 | msgid "Client secret" 41 | msgstr "" 42 | 43 | #: inc/application.class.php:106 inc/application.class.php:139 44 | msgid "Tenant ID" 45 | msgstr "" 46 | 47 | #: inc/application.class.php:182 48 | msgid "Developer help for this provider" 49 | msgstr "" 50 | 51 | #: inc/application.class.php:346 52 | msgid "Name cannot be empty" 53 | msgstr "" 54 | 55 | #: inc/application.class.php:355 56 | msgid "Invalid provider" 57 | msgstr "" 58 | 59 | #: inc/mailcollectorfeature.class.php:367 60 | #, php-format 61 | msgid "Mail receiver \"%s\" has been updated." 62 | msgstr "" 63 | 64 | #: inc/mailcollectorfeature.class.php:397 65 | #, php-format 66 | msgid "Mail receiver \"%s\" has been deactivated." 67 | msgstr "" 68 | 69 | #: inc/mailcollectorfeature.class.php:462 70 | msgid "No associated receivers." 71 | msgstr "" 72 | 73 | #: inc/mailcollectorfeature.class.php:465 74 | msgid "Name" 75 | msgstr "" 76 | 77 | #: inc/mailcollectorfeature.class.php:466 78 | msgid "Connection string" 79 | msgstr "" 80 | 81 | #: inc/mailcollectorfeature.class.php:467 82 | msgid "Login" 83 | msgstr "" 84 | 85 | #: inc/mailcollectorfeature.class.php:468 86 | msgid "Is active ?" 87 | msgstr "" 88 | 89 | #: inc/authorization.class.php:59 90 | msgid "Oauth authorization" 91 | msgid_plural "Oauth authorizations" 92 | msgstr[0] "" 93 | msgstr[1] "" 94 | 95 | #: inc/authorization.class.php:111 96 | msgid "Create an authorization" 97 | msgstr "" 98 | 99 | #: inc/authorization.class.php:121 100 | msgid "No authorizations." 101 | msgstr "" 102 | 103 | #: inc/authorization.class.php:125 inc/authorization.class.php:182 104 | #: inc/authorization.class.php:238 105 | msgid "Email" 106 | msgstr "" 107 | 108 | #: inc/authorization.class.php:142 109 | msgid "Connection diagnostic" 110 | msgstr "" 111 | 112 | #: inc/authorization.class.php:147 113 | msgid "Diagnose" 114 | msgstr "" 115 | 116 | #: inc/authorization.class.php:151 117 | msgid "Update" 118 | msgstr "" 119 | 120 | #: inc/authorization.class.php:159 121 | msgid "Delete" 122 | msgstr "" 123 | 124 | #: inc/authorization.class.php:253 125 | msgid "Server host" 126 | msgstr "" 127 | 128 | #: inc/authorization.class.php:264 129 | msgid "Server port" 130 | msgstr "" 131 | 132 | #: inc/authorization.class.php:281 133 | msgid "Security level" 134 | msgstr "" 135 | 136 | #: inc/authorization.class.php:288 137 | msgid "SSL" 138 | msgstr "" 139 | 140 | #: inc/authorization.class.php:289 141 | msgid "SSL + TLS" 142 | msgstr "" 143 | 144 | #: inc/authorization.class.php:298 145 | msgid "Timeout" 146 | msgstr "" 147 | 148 | #: inc/authorization.class.php:317 149 | msgid "Refresh connection diagnostic" 150 | msgstr "" 151 | 152 | #: inc/authorization.class.php:330 153 | msgid "" 154 | "Diagnostic log contains sensitive information, such as the access token." 155 | msgstr "" 156 | 157 | #: inc/authorization.class.php:349 158 | #, php-format 159 | msgid "Unexpected error: %s" 160 | msgstr "" 161 | 162 | #: front/authorization.process.php:43 163 | #, php-format 164 | msgid "Authorization failed with error: %s" 165 | msgstr "" 166 | 167 | #: front/authorization.process.php:53 168 | msgid "Unable to verify authorization code" 169 | msgstr "" 170 | 171 | #: front/authorization.process.php:55 172 | msgid "Unable to get authorization code" 173 | msgstr "" 174 | 175 | #: front/authorization.process.php:57 176 | msgid "Unable to save authorization code" 177 | msgstr "" 178 | 179 | #: ajax/dropdownAuthorization.php:48 180 | msgid "Create authorization for another user" 181 | msgstr "" 182 | -------------------------------------------------------------------------------- /setup.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | use Glpi\Http\SessionManager; 32 | 33 | use function Safe\define; 34 | 35 | define('PLUGIN_OAUTHIMAP_VERSION', '1.5.0'); 36 | 37 | // Minimal GLPI version, inclusive 38 | define('PLUGIN_OAUTHIMAP_MIN_GLPI', '11.0.0'); 39 | // Maximum GLPI version, exclusive 40 | define('PLUGIN_OAUTHIMAP_MAX_GLPI', '11.0.99'); 41 | 42 | define('PLUGIN_OAUTHIMAP_ROOT', Plugin::getPhpDir('oauthimap')); 43 | 44 | use Glpi\Http\Firewall; 45 | use GlpiPlugin\Oauthimap\MailCollectorFeature; 46 | 47 | function plugin_init_oauthimap() 48 | { 49 | /** @var array $PLUGIN_HOOKS */ 50 | global $PLUGIN_HOOKS; 51 | 52 | $PLUGIN_HOOKS['csrf_compliant']['oauthimap'] = true; 53 | 54 | Firewall::addPluginStrategyForLegacyScripts( 55 | 'oauthimap', 56 | '#^/front/authorization.callback.php$#', 57 | Firewall::STRATEGY_NO_CHECK, 58 | ); 59 | 60 | if (Plugin::isPluginActive('oauthimap')) { 61 | // Config page: redirect to dropdown page 62 | $PLUGIN_HOOKS['config_page']['oauthimap'] = 'front/application.php'; 63 | 64 | // Menu link 65 | $PLUGIN_HOOKS['menu_toadd']['oauthimap'] = [ 66 | 'config' => 'PluginOauthimapApplication', 67 | ]; 68 | 69 | // Secured fields that are encrypted 70 | $PLUGIN_HOOKS['secured_fields']['oauthimap'] = [ 71 | PluginOauthimapApplication::getTableField('client_secret'), 72 | PluginOauthimapAuthorization::getTableField('code'), 73 | PluginOauthimapAuthorization::getTableField('token'), 74 | PluginOauthimapAuthorization::getTableField('refresh_token'), 75 | ]; 76 | 77 | // Plugin hooks 78 | $PLUGIN_HOOKS['post_item_form']['oauthimap'] = [PluginOauthimapHook::class, 'postItemForm']; 79 | 80 | // MailCollector hooks 81 | $PLUGIN_HOOKS['mail_server_protocols']['oauthimap'] = (fn(array $additionnal_protocols) => array_merge($additionnal_protocols, MailCollectorFeature::getMailProtocols())); 82 | $PLUGIN_HOOKS['pre_item_update']['oauthimap'] = [ 83 | 'MailCollector' => [MailCollectorFeature::class, 'forceMailCollectorUpdate'], 84 | ]; 85 | $PLUGIN_HOOKS['item_add']['oauthimap'] = [ 86 | 'MailCollector' => [MailCollectorFeature::class, 'handleMailCollectorSaving'], 87 | ]; 88 | $PLUGIN_HOOKS['item_update']['oauthimap'] = [ 89 | 'MailCollector' => [MailCollectorFeature::class, 'handleMailCollectorSaving'], 90 | ]; 91 | } 92 | } 93 | 94 | function plugin_oauthimap_boot() 95 | { 96 | SessionManager::registerPluginStatelessPath('oauthimap', '#/front/authorization\.callback\.php#'); 97 | } 98 | 99 | function plugin_version_oauthimap() 100 | { 101 | return [ 102 | 'name' => __s('OAuth IMAP', 'oauthimap'), 103 | 'version' => PLUGIN_OAUTHIMAP_VERSION, 104 | 'author' => 'Teclib\'', 105 | 'license' => 'GPL v3+', 106 | 'homepage' => 'https://www.teclib-edition.com', 107 | 'requirements' => [ 108 | 'glpi' => [ 109 | 'min' => PLUGIN_OAUTHIMAP_MIN_GLPI, 110 | 'max' => PLUGIN_OAUTHIMAP_MAX_GLPI, 111 | ], 112 | 'php' => [ 113 | 'exts' => [ 114 | 'openssl' => [ 115 | 'required' => true, 116 | 'function' => 'openssl_x509_read', 117 | ], 118 | ], 119 | ], 120 | ], 121 | ]; 122 | } 123 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | require_once __DIR__ . '/../../src/Plugin.php'; 32 | 33 | use Rector\Caching\ValueObject\Storage\FileCacheStorage; 34 | use Rector\CodeQuality\Rector as CodeQuality; 35 | use Rector\Config\RectorConfig; 36 | use Rector\DeadCode\Rector as DeadCode; 37 | use Rector\ValueObject\PhpVersion; 38 | 39 | return RectorConfig::configure() 40 | ->withPaths([ 41 | __DIR__ . '/ajax', 42 | __DIR__ . '/front', 43 | __DIR__ . '/inc', 44 | ]) 45 | ->withPhpVersion(PhpVersion::PHP_82) 46 | ->withCache( 47 | cacheClass: FileCacheStorage::class, 48 | cacheDirectory: sys_get_temp_dir() . '/oauthimap-rector', 49 | ) 50 | ->withRootFiles() 51 | ->withParallel(timeoutSeconds: 300) 52 | ->withImportNames(removeUnusedImports: true) 53 | ->withRules([ 54 | CodeQuality\Assign\CombinedAssignRector::class, 55 | CodeQuality\BooleanAnd\RemoveUselessIsObjectCheckRector::class, 56 | CodeQuality\BooleanAnd\SimplifyEmptyArrayCheckRector::class, 57 | CodeQuality\BooleanNot\ReplaceMultipleBooleanNotRector::class, 58 | CodeQuality\Catch_\ThrowWithPreviousExceptionRector::class, 59 | CodeQuality\Empty_\SimplifyEmptyCheckOnEmptyArrayRector::class, 60 | CodeQuality\Expression\InlineIfToExplicitIfRector::class, 61 | CodeQuality\Expression\TernaryFalseExpressionToIfRector::class, 62 | CodeQuality\For_\ForRepeatedCountToOwnVariableRector::class, 63 | CodeQuality\Foreach_\ForeachItemsAssignToEmptyArrayToAssignRector::class, 64 | CodeQuality\Foreach_\ForeachToInArrayRector::class, 65 | CodeQuality\Foreach_\SimplifyForeachToCoalescingRector::class, 66 | CodeQuality\Foreach_\UnusedForeachValueToArrayKeysRector::class, 67 | CodeQuality\FuncCall\ChangeArrayPushToArrayAssignRector::class, 68 | CodeQuality\FuncCall\CompactToVariablesRector::class, 69 | CodeQuality\FuncCall\InlineIsAInstanceOfRector::class, 70 | CodeQuality\FuncCall\IsAWithStringWithThirdArgumentRector::class, 71 | CodeQuality\FuncCall\RemoveSoleValueSprintfRector::class, 72 | CodeQuality\FuncCall\SetTypeToCastRector::class, 73 | CodeQuality\FuncCall\SimplifyFuncGetArgsCountRector::class, 74 | CodeQuality\FuncCall\SimplifyInArrayValuesRector::class, 75 | CodeQuality\FuncCall\SimplifyStrposLowerRector::class, 76 | CodeQuality\FuncCall\UnwrapSprintfOneArgumentRector::class, 77 | CodeQuality\Identical\BooleanNotIdenticalToNotIdenticalRector::class, 78 | CodeQuality\Identical\SimplifyArraySearchRector::class, 79 | CodeQuality\Identical\SimplifyConditionsRector::class, 80 | CodeQuality\Identical\StrlenZeroToIdenticalEmptyStringRector::class, 81 | CodeQuality\If_\CombineIfRector::class, 82 | CodeQuality\If_\CompleteMissingIfElseBracketRector::class, 83 | CodeQuality\If_\ConsecutiveNullCompareReturnsToNullCoalesceQueueRector::class, 84 | CodeQuality\If_\ExplicitBoolCompareRector::class, 85 | CodeQuality\If_\ShortenElseIfRector::class, 86 | CodeQuality\If_\SimplifyIfElseToTernaryRector::class, 87 | CodeQuality\If_\SimplifyIfNotNullReturnRector::class, 88 | CodeQuality\If_\SimplifyIfNullableReturnRector::class, 89 | CodeQuality\If_\SimplifyIfReturnBoolRector::class, 90 | CodeQuality\Include_\AbsolutizeRequireAndIncludePathRector::class, 91 | CodeQuality\LogicalAnd\AndAssignsToSeparateLinesRector::class, 92 | CodeQuality\LogicalAnd\LogicalToBooleanRector::class, 93 | CodeQuality\NotEqual\CommonNotEqualRector::class, 94 | CodeQuality\Ternary\UnnecessaryTernaryExpressionRector::class, 95 | DeadCode\Assign\RemoveUnusedVariableAssignRector::class, 96 | ]) 97 | ->withPhpSets(php74: true) // apply PHP sets up to PHP 7.4 98 | ; 99 | -------------------------------------------------------------------------------- /locales/en_GB.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Cédric Anne, 2021 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2025-11-10 01:44+0000\n" 15 | "PO-Revision-Date: 2020-10-15 13:49+0000\n" 16 | "Last-Translator: Cédric Anne, 2021\n" 17 | "Language-Team: English (United Kingdom) (https://app.transifex.com/teclib/teams/28042/en_GB/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: en_GB\n" 22 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 23 | 24 | #: templates/application_form_extra.html.twig 25 | msgid "Callback url" 26 | msgstr "Callback url" 27 | 28 | #: setup.php:102 inc/application.class.php:45 29 | msgid "OAuth IMAP" 30 | msgid_plural "OAuth IMAP" 31 | msgstr[0] "" 32 | msgstr[1] "" 33 | 34 | #: inc/application.class.php:88 inc/application.class.php:122 35 | msgid "Oauth provider" 36 | msgstr "Oauth provider" 37 | 38 | #: inc/application.class.php:94 inc/application.class.php:131 39 | msgid "Client ID" 40 | msgstr "Client ID" 41 | 42 | #: inc/application.class.php:100 43 | msgid "Client secret" 44 | msgstr "Client secret" 45 | 46 | #: inc/application.class.php:106 inc/application.class.php:139 47 | msgid "Tenant ID" 48 | msgstr "Tenant ID" 49 | 50 | #: inc/application.class.php:182 51 | msgid "Developer help for this provider" 52 | msgstr "Developer help for this provider" 53 | 54 | #: inc/application.class.php:346 55 | msgid "Name cannot be empty" 56 | msgstr "Name cannot be empty" 57 | 58 | #: inc/application.class.php:355 59 | msgid "Invalid provider" 60 | msgstr "Invalid provider" 61 | 62 | #: inc/mailcollectorfeature.class.php:367 63 | #, php-format 64 | msgid "Mail receiver \"%s\" has been updated." 65 | msgstr "Mail receiver \"%s\" has been updated." 66 | 67 | #: inc/mailcollectorfeature.class.php:397 68 | #, php-format 69 | msgid "Mail receiver \"%s\" has been deactivated." 70 | msgstr "Mail receiver \"%s\" has been deactivated." 71 | 72 | #: inc/mailcollectorfeature.class.php:462 73 | msgid "No associated receivers." 74 | msgstr "No associated receivers." 75 | 76 | #: inc/mailcollectorfeature.class.php:465 77 | msgid "Name" 78 | msgstr "Name" 79 | 80 | #: inc/mailcollectorfeature.class.php:466 81 | msgid "Connection string" 82 | msgstr "Connection string" 83 | 84 | #: inc/mailcollectorfeature.class.php:467 85 | msgid "Login" 86 | msgstr "Login" 87 | 88 | #: inc/mailcollectorfeature.class.php:468 89 | msgid "Is active ?" 90 | msgstr "Is active ?" 91 | 92 | #: inc/authorization.class.php:59 93 | msgid "Oauth authorization" 94 | msgid_plural "Oauth authorizations" 95 | msgstr[0] "Oauth authorization" 96 | msgstr[1] "Oauth authorizations" 97 | 98 | #: inc/authorization.class.php:111 99 | msgid "Create an authorization" 100 | msgstr "Create an authorization" 101 | 102 | #: inc/authorization.class.php:121 103 | msgid "No authorizations." 104 | msgstr "No authorizations." 105 | 106 | #: inc/authorization.class.php:125 inc/authorization.class.php:182 107 | #: inc/authorization.class.php:238 108 | msgid "Email" 109 | msgstr "Email" 110 | 111 | #: inc/authorization.class.php:142 112 | msgid "Connection diagnostic" 113 | msgstr "Connection diagnostic" 114 | 115 | #: inc/authorization.class.php:147 116 | msgid "Diagnose" 117 | msgstr "Diagnose" 118 | 119 | #: inc/authorization.class.php:151 120 | msgid "Update" 121 | msgstr "Update" 122 | 123 | #: inc/authorization.class.php:159 124 | msgid "Delete" 125 | msgstr "Delete" 126 | 127 | #: inc/authorization.class.php:253 128 | msgid "Server host" 129 | msgstr "Server host" 130 | 131 | #: inc/authorization.class.php:264 132 | msgid "Server port" 133 | msgstr "Server port" 134 | 135 | #: inc/authorization.class.php:281 136 | msgid "Security level" 137 | msgstr "Security level" 138 | 139 | #: inc/authorization.class.php:288 140 | msgid "SSL" 141 | msgstr "SSL" 142 | 143 | #: inc/authorization.class.php:289 144 | msgid "SSL + TLS" 145 | msgstr "SSL + TLS" 146 | 147 | #: inc/authorization.class.php:298 148 | msgid "Timeout" 149 | msgstr "Timeout" 150 | 151 | #: inc/authorization.class.php:317 152 | msgid "Refresh connection diagnostic" 153 | msgstr "Refresh connection diagnostic" 154 | 155 | #: inc/authorization.class.php:330 156 | msgid "" 157 | "Diagnostic log contains sensitive information, such as the access token." 158 | msgstr "" 159 | "Diagnostic log contains sensitive information, such as the access token." 160 | 161 | #: inc/authorization.class.php:349 162 | #, php-format 163 | msgid "Unexpected error: %s" 164 | msgstr "Unexpected error: %s" 165 | 166 | #: front/authorization.process.php:43 167 | #, php-format 168 | msgid "Authorization failed with error: %s" 169 | msgstr "Authorization failed with error: %s" 170 | 171 | #: front/authorization.process.php:53 172 | msgid "Unable to verify authorization code" 173 | msgstr "Unable to verify authorization code" 174 | 175 | #: front/authorization.process.php:55 176 | msgid "Unable to get authorization code" 177 | msgstr "Unable to get authorization code" 178 | 179 | #: front/authorization.process.php:57 180 | msgid "Unable to save authorization code" 181 | msgstr "Unable to save authorization code" 182 | 183 | #: ajax/dropdownAuthorization.php:48 184 | msgid "Create authorization for another user" 185 | msgstr "Create authorization for another user" 186 | -------------------------------------------------------------------------------- /locales/sv_SE.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Peter Rundqvist, 2025 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2025-11-13 01:41+0000\n" 15 | "PO-Revision-Date: 2020-10-15 13:49+0000\n" 16 | "Last-Translator: Peter Rundqvist, 2025\n" 17 | "Language-Team: Swedish (Sweden) (https://app.transifex.com/teclib/teams/28042/sv_SE/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: sv_SE\n" 22 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 23 | 24 | #: templates/application_form_extra.html.twig 25 | msgid "Callback url" 26 | msgstr "Callback-URL" 27 | 28 | #: setup.php:102 inc/application.class.php:45 29 | msgid "OAuth IMAP" 30 | msgid_plural "OAuth IMAP" 31 | msgstr[0] "OAuth IMAP" 32 | msgstr[1] "OAuth IMAP" 33 | 34 | #: inc/application.class.php:88 inc/application.class.php:122 35 | msgid "Oauth provider" 36 | msgstr "OAuth-leverantör" 37 | 38 | #: inc/application.class.php:94 inc/application.class.php:131 39 | msgid "Client ID" 40 | msgstr "Klient-ID" 41 | 42 | #: inc/application.class.php:100 43 | msgid "Client secret" 44 | msgstr "Klienthemlighet" 45 | 46 | #: inc/application.class.php:106 inc/application.class.php:139 47 | msgid "Tenant ID" 48 | msgstr "Tenant-ID" 49 | 50 | #: inc/application.class.php:182 51 | msgid "Developer help for this provider" 52 | msgstr "Hjälp för utvecklare för denna leverantör" 53 | 54 | #: inc/application.class.php:346 55 | msgid "Name cannot be empty" 56 | msgstr "Namnet får inte vara tomt" 57 | 58 | #: inc/application.class.php:355 59 | msgid "Invalid provider" 60 | msgstr "Ogiltig leverantör" 61 | 62 | #: inc/mailcollectorfeature.class.php:367 63 | #, php-format 64 | msgid "Mail receiver \"%s\" has been updated." 65 | msgstr "Ärendemottagaren \"%s\" uppdaterades." 66 | 67 | #: inc/mailcollectorfeature.class.php:397 68 | #, php-format 69 | msgid "Mail receiver \"%s\" has been deactivated." 70 | msgstr "Ärendemottagaren \"%s\" inaktiverades." 71 | 72 | #: inc/mailcollectorfeature.class.php:462 73 | msgid "No associated receivers." 74 | msgstr "Hittade inga mottagare." 75 | 76 | #: inc/mailcollectorfeature.class.php:465 77 | msgid "Name" 78 | msgstr "Namn" 79 | 80 | #: inc/mailcollectorfeature.class.php:466 81 | msgid "Connection string" 82 | msgstr "Anslutningssträng" 83 | 84 | #: inc/mailcollectorfeature.class.php:467 85 | msgid "Login" 86 | msgstr "Inloggning" 87 | 88 | #: inc/mailcollectorfeature.class.php:468 89 | msgid "Is active ?" 90 | msgstr "Aktiv?" 91 | 92 | #: inc/authorization.class.php:59 93 | msgid "Oauth authorization" 94 | msgid_plural "Oauth authorizations" 95 | msgstr[0] "OAuth-auktorisering" 96 | msgstr[1] "OAuth-auktoriseringar" 97 | 98 | #: inc/authorization.class.php:111 99 | msgid "Create an authorization" 100 | msgstr "Skapa auktorisation" 101 | 102 | #: inc/authorization.class.php:121 103 | msgid "No authorizations." 104 | msgstr "Inga auktorisationer." 105 | 106 | #: inc/authorization.class.php:125 inc/authorization.class.php:182 107 | #: inc/authorization.class.php:238 108 | msgid "Email" 109 | msgstr "E-post" 110 | 111 | #: inc/authorization.class.php:142 112 | msgid "Connection diagnostic" 113 | msgstr "Anslutningsdiagnostik" 114 | 115 | #: inc/authorization.class.php:147 116 | msgid "Diagnose" 117 | msgstr "Diagnostisera" 118 | 119 | #: inc/authorization.class.php:151 120 | msgid "Update" 121 | msgstr "Uppdatera" 122 | 123 | #: inc/authorization.class.php:159 124 | msgid "Delete" 125 | msgstr "Ta bort" 126 | 127 | #: inc/authorization.class.php:253 128 | msgid "Server host" 129 | msgstr "Värd" 130 | 131 | #: inc/authorization.class.php:264 132 | msgid "Server port" 133 | msgstr "Port" 134 | 135 | #: inc/authorization.class.php:281 136 | msgid "Security level" 137 | msgstr "Säkerhet" 138 | 139 | #: inc/authorization.class.php:288 140 | msgid "SSL" 141 | msgstr "SSL" 142 | 143 | #: inc/authorization.class.php:289 144 | msgid "SSL + TLS" 145 | msgstr "SSL + TLS" 146 | 147 | #: inc/authorization.class.php:298 148 | msgid "Timeout" 149 | msgstr "Timeout" 150 | 151 | #: inc/authorization.class.php:317 152 | msgid "Refresh connection diagnostic" 153 | msgstr "Uppdatera" 154 | 155 | #: inc/authorization.class.php:330 156 | msgid "" 157 | "Diagnostic log contains sensitive information, such as the access token." 158 | msgstr "" 159 | "Diagnostikloggen innehåller känslig information som till exempel " 160 | "åtkomsttoken." 161 | 162 | #: inc/authorization.class.php:349 163 | #, php-format 164 | msgid "Unexpected error: %s" 165 | msgstr "Oväntat fel: %s" 166 | 167 | #: front/authorization.process.php:43 168 | #, php-format 169 | msgid "Authorization failed with error: %s" 170 | msgstr "Auktorisering misslyckades. Felbeskrivning: %s" 171 | 172 | #: front/authorization.process.php:53 173 | msgid "Unable to verify authorization code" 174 | msgstr "Det gick inte att verifiera auktoriseringskoden" 175 | 176 | #: front/authorization.process.php:55 177 | msgid "Unable to get authorization code" 178 | msgstr "Det gick inte att hämta auktoriseringskoden" 179 | 180 | #: front/authorization.process.php:57 181 | msgid "Unable to save authorization code" 182 | msgstr "Det gick inte att spara auktoriseringskoden" 183 | 184 | #: ajax/dropdownAuthorization.php:48 185 | msgid "Create authorization for another user" 186 | msgstr "Skapa auktorisering för en annan användare" 187 | -------------------------------------------------------------------------------- /locales/fr_FR.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Cédric Anne, 2021 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2025-11-10 01:44+0000\n" 15 | "PO-Revision-Date: 2020-10-15 13:49+0000\n" 16 | "Last-Translator: Cédric Anne, 2021\n" 17 | "Language-Team: French (France) (https://app.transifex.com/teclib/teams/28042/fr_FR/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: fr_FR\n" 22 | "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 23 | 24 | #: templates/application_form_extra.html.twig 25 | msgid "Callback url" 26 | msgstr "Url de retour" 27 | 28 | #: setup.php:102 inc/application.class.php:45 29 | msgid "OAuth IMAP" 30 | msgid_plural "OAuth IMAP" 31 | msgstr[0] "" 32 | msgstr[1] "" 33 | msgstr[2] "" 34 | 35 | #: inc/application.class.php:88 inc/application.class.php:122 36 | msgid "Oauth provider" 37 | msgstr "Fournisseur Oauth" 38 | 39 | #: inc/application.class.php:94 inc/application.class.php:131 40 | msgid "Client ID" 41 | msgstr "ID d'application (client)" 42 | 43 | #: inc/application.class.php:100 44 | msgid "Client secret" 45 | msgstr "Secret client" 46 | 47 | #: inc/application.class.php:106 inc/application.class.php:139 48 | msgid "Tenant ID" 49 | msgstr "ID de l'annuaire (locataire)" 50 | 51 | #: inc/application.class.php:182 52 | msgid "Developer help for this provider" 53 | msgstr "Aide développeur pour ce fournisseur" 54 | 55 | #: inc/application.class.php:346 56 | msgid "Name cannot be empty" 57 | msgstr "Le nom ne peut être vide" 58 | 59 | #: inc/application.class.php:355 60 | msgid "Invalid provider" 61 | msgstr "Fournisseur invalide" 62 | 63 | #: inc/mailcollectorfeature.class.php:367 64 | #, php-format 65 | msgid "Mail receiver \"%s\" has been updated." 66 | msgstr "Le collecteur mail \"%s\" a été mis à jour." 67 | 68 | #: inc/mailcollectorfeature.class.php:397 69 | #, php-format 70 | msgid "Mail receiver \"%s\" has been deactivated." 71 | msgstr "Le collecteur mail \"%s\" a été désactivé." 72 | 73 | #: inc/mailcollectorfeature.class.php:462 74 | msgid "No associated receivers." 75 | msgstr "Aucun collecteur associé." 76 | 77 | #: inc/mailcollectorfeature.class.php:465 78 | msgid "Name" 79 | msgstr "Nom" 80 | 81 | #: inc/mailcollectorfeature.class.php:466 82 | msgid "Connection string" 83 | msgstr "Chaîne de connexion" 84 | 85 | #: inc/mailcollectorfeature.class.php:467 86 | msgid "Login" 87 | msgstr "Identifiant" 88 | 89 | #: inc/mailcollectorfeature.class.php:468 90 | msgid "Is active ?" 91 | msgstr "Est actif ?" 92 | 93 | #: inc/authorization.class.php:59 94 | msgid "Oauth authorization" 95 | msgid_plural "Oauth authorizations" 96 | msgstr[0] "Autorisation Oauth" 97 | msgstr[1] "Autorisations Oauth" 98 | msgstr[2] "Autorisations Oauth" 99 | 100 | #: inc/authorization.class.php:111 101 | msgid "Create an authorization" 102 | msgstr "Créer une autorisation" 103 | 104 | #: inc/authorization.class.php:121 105 | msgid "No authorizations." 106 | msgstr "Aucune autorisation." 107 | 108 | #: inc/authorization.class.php:125 inc/authorization.class.php:182 109 | #: inc/authorization.class.php:238 110 | msgid "Email" 111 | msgstr "E-mail" 112 | 113 | #: inc/authorization.class.php:142 114 | msgid "Connection diagnostic" 115 | msgstr "Diagnostic de la connexion" 116 | 117 | #: inc/authorization.class.php:147 118 | msgid "Diagnose" 119 | msgstr "Diagnostiquer" 120 | 121 | #: inc/authorization.class.php:151 122 | msgid "Update" 123 | msgstr "Mettre à jour" 124 | 125 | #: inc/authorization.class.php:159 126 | msgid "Delete" 127 | msgstr "Supprimer" 128 | 129 | #: inc/authorization.class.php:253 130 | msgid "Server host" 131 | msgstr "Hôte du serveur" 132 | 133 | #: inc/authorization.class.php:264 134 | msgid "Server port" 135 | msgstr "Port du serveur" 136 | 137 | #: inc/authorization.class.php:281 138 | msgid "Security level" 139 | msgstr "Niveau de sécurité" 140 | 141 | #: inc/authorization.class.php:288 142 | msgid "SSL" 143 | msgstr "SSL" 144 | 145 | #: inc/authorization.class.php:289 146 | msgid "SSL + TLS" 147 | msgstr "SSL + TLS" 148 | 149 | #: inc/authorization.class.php:298 150 | msgid "Timeout" 151 | msgstr "Délai d'attente max." 152 | 153 | #: inc/authorization.class.php:317 154 | msgid "Refresh connection diagnostic" 155 | msgstr "Actualiser le diagnostic" 156 | 157 | #: inc/authorization.class.php:330 158 | msgid "" 159 | "Diagnostic log contains sensitive information, such as the access token." 160 | msgstr "" 161 | "Le journal de diagnostic contient des informations sensibles, telles que le " 162 | "jeton d'accès." 163 | 164 | #: inc/authorization.class.php:349 165 | #, php-format 166 | msgid "Unexpected error: %s" 167 | msgstr "Erreur inattendue : %s" 168 | 169 | #: front/authorization.process.php:43 170 | #, php-format 171 | msgid "Authorization failed with error: %s" 172 | msgstr "L'autorisation a échouée avec l'erreur: %s" 173 | 174 | #: front/authorization.process.php:53 175 | msgid "Unable to verify authorization code" 176 | msgstr "Impossible de vérifier le code d'autorisation" 177 | 178 | #: front/authorization.process.php:55 179 | msgid "Unable to get authorization code" 180 | msgstr "Impossible d'obtenir le code d'autorisation" 181 | 182 | #: front/authorization.process.php:57 183 | msgid "Unable to save authorization code" 184 | msgstr "Impossible de sauvegarder le code d'autorisation" 185 | 186 | #: ajax/dropdownAuthorization.php:48 187 | msgid "Create authorization for another user" 188 | msgstr "Créer une autorisation pour un autre utilisateur" 189 | -------------------------------------------------------------------------------- /locales/sk_SK.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # feonsu , 2023 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2025-11-10 01:44+0000\n" 15 | "PO-Revision-Date: 2020-10-15 13:49+0000\n" 16 | "Last-Translator: feonsu , 2023\n" 17 | "Language-Team: Slovak (Slovakia) (https://app.transifex.com/teclib/teams/28042/sk_SK/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: sk_SK\n" 22 | "Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n" 23 | 24 | #: templates/application_form_extra.html.twig 25 | msgid "Callback url" 26 | msgstr "URL adresa spätného volania" 27 | 28 | #: setup.php:102 inc/application.class.php:45 29 | msgid "OAuth IMAP" 30 | msgid_plural "OAuth IMAP" 31 | msgstr[0] "" 32 | msgstr[1] "" 33 | msgstr[2] "" 34 | msgstr[3] "" 35 | 36 | #: inc/application.class.php:88 inc/application.class.php:122 37 | msgid "Oauth provider" 38 | msgstr "Oauth poskytovateľ" 39 | 40 | #: inc/application.class.php:94 inc/application.class.php:131 41 | msgid "Client ID" 42 | msgstr "ID klienta" 43 | 44 | #: inc/application.class.php:100 45 | msgid "Client secret" 46 | msgstr "Tajný kľúč klienta" 47 | 48 | #: inc/application.class.php:106 inc/application.class.php:139 49 | msgid "Tenant ID" 50 | msgstr "ID tenantu" 51 | 52 | #: inc/application.class.php:182 53 | msgid "Developer help for this provider" 54 | msgstr "Nápoveda vývojára pre tohto poskytovateľa" 55 | 56 | #: inc/application.class.php:346 57 | msgid "Name cannot be empty" 58 | msgstr "Názov nemôže byť prázdny" 59 | 60 | #: inc/application.class.php:355 61 | msgid "Invalid provider" 62 | msgstr "Neplatný poskytovateľ" 63 | 64 | #: inc/mailcollectorfeature.class.php:367 65 | #, php-format 66 | msgid "Mail receiver \"%s\" has been updated." 67 | msgstr "E-mailový prijímač \"%s\" bol aktualizovaný." 68 | 69 | #: inc/mailcollectorfeature.class.php:397 70 | #, php-format 71 | msgid "Mail receiver \"%s\" has been deactivated." 72 | msgstr "E-mailový prijímač \"%s\" bol deaktivovaný." 73 | 74 | #: inc/mailcollectorfeature.class.php:462 75 | msgid "No associated receivers." 76 | msgstr "Žiadne priradené prijímače." 77 | 78 | #: inc/mailcollectorfeature.class.php:465 79 | msgid "Name" 80 | msgstr "Názov" 81 | 82 | #: inc/mailcollectorfeature.class.php:466 83 | msgid "Connection string" 84 | msgstr "Reťazec pripojenia" 85 | 86 | #: inc/mailcollectorfeature.class.php:467 87 | msgid "Login" 88 | msgstr "Prihlásenie" 89 | 90 | #: inc/mailcollectorfeature.class.php:468 91 | msgid "Is active ?" 92 | msgstr "Je aktívny?" 93 | 94 | #: inc/authorization.class.php:59 95 | msgid "Oauth authorization" 96 | msgid_plural "Oauth authorizations" 97 | msgstr[0] "Oauth autorizácia" 98 | msgstr[1] "Oauth autorizácie" 99 | msgstr[2] "Oauth autorizácie" 100 | msgstr[3] "Oauth autorizácie" 101 | 102 | #: inc/authorization.class.php:111 103 | msgid "Create an authorization" 104 | msgstr "Vytvoriť autorizáciu" 105 | 106 | #: inc/authorization.class.php:121 107 | msgid "No authorizations." 108 | msgstr "Žiadne autorizácie." 109 | 110 | #: inc/authorization.class.php:125 inc/authorization.class.php:182 111 | #: inc/authorization.class.php:238 112 | msgid "Email" 113 | msgstr "E-mail" 114 | 115 | #: inc/authorization.class.php:142 116 | msgid "Connection diagnostic" 117 | msgstr "Diagnostika pripojenia" 118 | 119 | #: inc/authorization.class.php:147 120 | msgid "Diagnose" 121 | msgstr "Diagnostika" 122 | 123 | #: inc/authorization.class.php:151 124 | msgid "Update" 125 | msgstr "Aktualizovať" 126 | 127 | #: inc/authorization.class.php:159 128 | msgid "Delete" 129 | msgstr "Odstrániť" 130 | 131 | #: inc/authorization.class.php:253 132 | msgid "Server host" 133 | msgstr "Názov servera" 134 | 135 | #: inc/authorization.class.php:264 136 | msgid "Server port" 137 | msgstr "Port servera" 138 | 139 | #: inc/authorization.class.php:281 140 | msgid "Security level" 141 | msgstr "Úroveň zabezpečenia" 142 | 143 | #: inc/authorization.class.php:288 144 | msgid "SSL" 145 | msgstr "SSL" 146 | 147 | #: inc/authorization.class.php:289 148 | msgid "SSL + TLS" 149 | msgstr "SSL + TLS" 150 | 151 | #: inc/authorization.class.php:298 152 | msgid "Timeout" 153 | msgstr "Časový limit" 154 | 155 | #: inc/authorization.class.php:317 156 | msgid "Refresh connection diagnostic" 157 | msgstr "Obnoviť diagnostiku pripojenia" 158 | 159 | #: inc/authorization.class.php:330 160 | msgid "" 161 | "Diagnostic log contains sensitive information, such as the access token." 162 | msgstr "" 163 | "Diagnostický log obsahuje citlivé informácie, ako napríklad prístupový " 164 | "token." 165 | 166 | #: inc/authorization.class.php:349 167 | #, php-format 168 | msgid "Unexpected error: %s" 169 | msgstr "Neočakávaná chyba: %s" 170 | 171 | #: front/authorization.process.php:43 172 | #, php-format 173 | msgid "Authorization failed with error: %s" 174 | msgstr "Autorizácia zlyhala s chybou: %s" 175 | 176 | #: front/authorization.process.php:53 177 | msgid "Unable to verify authorization code" 178 | msgstr "Nepodarilo sa overiť autorizačný kód" 179 | 180 | #: front/authorization.process.php:55 181 | msgid "Unable to get authorization code" 182 | msgstr "Nepodarilo sa získať autorizačný kód" 183 | 184 | #: front/authorization.process.php:57 185 | msgid "Unable to save authorization code" 186 | msgstr "Nepodarilo sa uložiť autorizačný kód" 187 | 188 | #: ajax/dropdownAuthorization.php:48 189 | msgid "Create authorization for another user" 190 | msgstr "Vytvoriť autorizáciu pre iného používateľa" 191 | -------------------------------------------------------------------------------- /locales/es_AR.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Alan Lehoux, 2025 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2025-11-26 01:41+0000\n" 15 | "PO-Revision-Date: 2020-10-15 13:49+0000\n" 16 | "Last-Translator: Alan Lehoux, 2025\n" 17 | "Language-Team: Spanish (Argentina) (https://app.transifex.com/teclib/teams/28042/es_AR/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: es_AR\n" 22 | "Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 23 | 24 | #: templates/application_form_extra.html.twig 25 | msgid "Callback url" 26 | msgstr "Url de callback" 27 | 28 | #: setup.php:102 inc/application.class.php:45 29 | msgid "OAuth IMAP" 30 | msgid_plural "OAuth IMAP" 31 | msgstr[0] "OAuth IMAP" 32 | msgstr[1] "OAuth IMAP" 33 | msgstr[2] "OAuth IMAP" 34 | 35 | #: inc/application.class.php:88 inc/application.class.php:122 36 | msgid "Oauth provider" 37 | msgstr "Proveedor de Oauth" 38 | 39 | #: inc/application.class.php:94 inc/application.class.php:131 40 | msgid "Client ID" 41 | msgstr "ID de cliente" 42 | 43 | #: inc/application.class.php:100 44 | msgid "Client secret" 45 | msgstr "Secreto de cliente" 46 | 47 | #: inc/application.class.php:106 inc/application.class.php:139 48 | msgid "Tenant ID" 49 | msgstr "ID de tenant" 50 | 51 | #: inc/application.class.php:182 52 | msgid "Developer help for this provider" 53 | msgstr "Ayuda para desarrolladores de este proveedor" 54 | 55 | #: inc/application.class.php:346 56 | msgid "Name cannot be empty" 57 | msgstr "El nombre no puede estar vacío" 58 | 59 | #: inc/application.class.php:355 60 | msgid "Invalid provider" 61 | msgstr "Proveedor inválido" 62 | 63 | #: inc/mailcollectorfeature.class.php:367 64 | #, php-format 65 | msgid "Mail receiver \"%s\" has been updated." 66 | msgstr "El receptor de correo \"%s\" fue actualizado." 67 | 68 | #: inc/mailcollectorfeature.class.php:397 69 | #, php-format 70 | msgid "Mail receiver \"%s\" has been deactivated." 71 | msgstr "El receptor de correo \"%s\" fue desactivado." 72 | 73 | #: inc/mailcollectorfeature.class.php:462 74 | msgid "No associated receivers." 75 | msgstr "No hay receptores asociados." 76 | 77 | #: inc/mailcollectorfeature.class.php:465 78 | msgid "Name" 79 | msgstr "Nombre" 80 | 81 | #: inc/mailcollectorfeature.class.php:466 82 | msgid "Connection string" 83 | msgstr "Cadena de conexión" 84 | 85 | #: inc/mailcollectorfeature.class.php:467 86 | msgid "Login" 87 | msgstr "Inicio de sesión" 88 | 89 | #: inc/mailcollectorfeature.class.php:468 90 | msgid "Is active ?" 91 | msgstr "¿ Está activo ?" 92 | 93 | #: inc/authorization.class.php:59 94 | msgid "Oauth authorization" 95 | msgid_plural "Oauth authorizations" 96 | msgstr[0] "Autorización de Oauth" 97 | msgstr[1] "Autorizaciones de Oauth" 98 | msgstr[2] "Autorizaciones de Oauth" 99 | 100 | #: inc/authorization.class.php:111 101 | msgid "Create an authorization" 102 | msgstr "Crear una autorización" 103 | 104 | #: inc/authorization.class.php:121 105 | msgid "No authorizations." 106 | msgstr "Sin autorizaciones." 107 | 108 | #: inc/authorization.class.php:125 inc/authorization.class.php:182 109 | #: inc/authorization.class.php:238 110 | msgid "Email" 111 | msgstr "Correo electrónico" 112 | 113 | #: inc/authorization.class.php:142 114 | msgid "Connection diagnostic" 115 | msgstr "Diagnóstico de conexión" 116 | 117 | #: inc/authorization.class.php:147 118 | msgid "Diagnose" 119 | msgstr "Diagnosticar" 120 | 121 | #: inc/authorization.class.php:151 122 | msgid "Update" 123 | msgstr "Actualizar" 124 | 125 | #: inc/authorization.class.php:159 126 | msgid "Delete" 127 | msgstr "Eliminar" 128 | 129 | #: inc/authorization.class.php:253 130 | msgid "Server host" 131 | msgstr "Host del servidor" 132 | 133 | #: inc/authorization.class.php:264 134 | msgid "Server port" 135 | msgstr "Puerto del servidor" 136 | 137 | #: inc/authorization.class.php:281 138 | msgid "Security level" 139 | msgstr "Nivel de seguridad" 140 | 141 | #: inc/authorization.class.php:288 142 | msgid "SSL" 143 | msgstr "SSL" 144 | 145 | #: inc/authorization.class.php:289 146 | msgid "SSL + TLS" 147 | msgstr "SSL + TLS" 148 | 149 | #: inc/authorization.class.php:298 150 | msgid "Timeout" 151 | msgstr "Tiempo de espera" 152 | 153 | #: inc/authorization.class.php:317 154 | msgid "Refresh connection diagnostic" 155 | msgstr "Actualizar diagnóstico de conexión" 156 | 157 | #: inc/authorization.class.php:330 158 | msgid "" 159 | "Diagnostic log contains sensitive information, such as the access token." 160 | msgstr "" 161 | "El registro de diagnóstico contiene información sensible, como el token de " 162 | "acceso." 163 | 164 | #: inc/authorization.class.php:349 165 | #, php-format 166 | msgid "Unexpected error: %s" 167 | msgstr "Error inesperado: %s" 168 | 169 | #: front/authorization.process.php:43 170 | #, php-format 171 | msgid "Authorization failed with error: %s" 172 | msgstr "La autorización falló con el error: %s" 173 | 174 | #: front/authorization.process.php:53 175 | msgid "Unable to verify authorization code" 176 | msgstr "No se puede verificar el código de autorización" 177 | 178 | #: front/authorization.process.php:55 179 | msgid "Unable to get authorization code" 180 | msgstr "No se puede obtener el código de autorización" 181 | 182 | #: front/authorization.process.php:57 183 | msgid "Unable to save authorization code" 184 | msgstr "No se puede guardar el código de autorización" 185 | 186 | #: ajax/dropdownAuthorization.php:48 187 | msgid "Create authorization for another user" 188 | msgstr "Crear autorización para otro usuario" 189 | -------------------------------------------------------------------------------- /locales/es_EC.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Soporte Infraestructura Standby, 2023 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2025-11-10 01:44+0000\n" 15 | "PO-Revision-Date: 2020-10-15 13:49+0000\n" 16 | "Last-Translator: Soporte Infraestructura Standby, 2023\n" 17 | "Language-Team: Spanish (Ecuador) (https://app.transifex.com/teclib/teams/28042/es_EC/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: es_EC\n" 22 | "Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 23 | 24 | #: templates/application_form_extra.html.twig 25 | msgid "Callback url" 26 | msgstr "URL de devolución de llamada" 27 | 28 | #: setup.php:102 inc/application.class.php:45 29 | msgid "OAuth IMAP" 30 | msgid_plural "OAuth IMAP" 31 | msgstr[0] "" 32 | msgstr[1] "" 33 | msgstr[2] "" 34 | 35 | #: inc/application.class.php:88 inc/application.class.php:122 36 | msgid "Oauth provider" 37 | msgstr "Proveedor Oauth" 38 | 39 | #: inc/application.class.php:94 inc/application.class.php:131 40 | msgid "Client ID" 41 | msgstr "ID de cliente" 42 | 43 | #: inc/application.class.php:100 44 | msgid "Client secret" 45 | msgstr "Secreto de cliente" 46 | 47 | #: inc/application.class.php:106 inc/application.class.php:139 48 | msgid "Tenant ID" 49 | msgstr "ID de inquilino" 50 | 51 | #: inc/application.class.php:182 52 | msgid "Developer help for this provider" 53 | msgstr "Ayuda para desarrolladores de este proveedor" 54 | 55 | #: inc/application.class.php:346 56 | msgid "Name cannot be empty" 57 | msgstr "El nombre no puede estar vacío" 58 | 59 | #: inc/application.class.php:355 60 | msgid "Invalid provider" 61 | msgstr "Proveedor no válido" 62 | 63 | #: inc/mailcollectorfeature.class.php:367 64 | #, php-format 65 | msgid "Mail receiver \"%s\" has been updated." 66 | msgstr "El receptor de correo \"%s\" ha sido actualizado." 67 | 68 | #: inc/mailcollectorfeature.class.php:397 69 | #, php-format 70 | msgid "Mail receiver \"%s\" has been deactivated." 71 | msgstr "El receptor de correo \"%s\" ha sido desactivado." 72 | 73 | #: inc/mailcollectorfeature.class.php:462 74 | msgid "No associated receivers." 75 | msgstr "No hay receptores asociados." 76 | 77 | #: inc/mailcollectorfeature.class.php:465 78 | msgid "Name" 79 | msgstr "Nombre" 80 | 81 | #: inc/mailcollectorfeature.class.php:466 82 | msgid "Connection string" 83 | msgstr "Cadena de conexión" 84 | 85 | #: inc/mailcollectorfeature.class.php:467 86 | msgid "Login" 87 | msgstr "Inicio de sesión" 88 | 89 | #: inc/mailcollectorfeature.class.php:468 90 | msgid "Is active ?" 91 | msgstr "Está activo?" 92 | 93 | #: inc/authorization.class.php:59 94 | msgid "Oauth authorization" 95 | msgid_plural "Oauth authorizations" 96 | msgstr[0] "Autorización Oauth" 97 | msgstr[1] "Autorizaciones Oauth" 98 | msgstr[2] "Autorizaciones Oauth" 99 | 100 | #: inc/authorization.class.php:111 101 | msgid "Create an authorization" 102 | msgstr "Crear una autorización" 103 | 104 | #: inc/authorization.class.php:121 105 | msgid "No authorizations." 106 | msgstr "No hay autorizaciones." 107 | 108 | #: inc/authorization.class.php:125 inc/authorization.class.php:182 109 | #: inc/authorization.class.php:238 110 | msgid "Email" 111 | msgstr "Correo electrónico" 112 | 113 | #: inc/authorization.class.php:142 114 | msgid "Connection diagnostic" 115 | msgstr "Diagnóstico de conexión" 116 | 117 | #: inc/authorization.class.php:147 118 | msgid "Diagnose" 119 | msgstr "Diagnosticar" 120 | 121 | #: inc/authorization.class.php:151 122 | msgid "Update" 123 | msgstr "Actualización" 124 | 125 | #: inc/authorization.class.php:159 126 | msgid "Delete" 127 | msgstr "Borrar" 128 | 129 | #: inc/authorization.class.php:253 130 | msgid "Server host" 131 | msgstr "Servidor" 132 | 133 | #: inc/authorization.class.php:264 134 | msgid "Server port" 135 | msgstr "Puerto del servidor" 136 | 137 | #: inc/authorization.class.php:281 138 | msgid "Security level" 139 | msgstr "Nivel de seguridad" 140 | 141 | #: inc/authorization.class.php:288 142 | msgid "SSL" 143 | msgstr "SSL" 144 | 145 | #: inc/authorization.class.php:289 146 | msgid "SSL + TLS" 147 | msgstr "SSL + TLS" 148 | 149 | #: inc/authorization.class.php:298 150 | msgid "Timeout" 151 | msgstr "Tiempo de espera" 152 | 153 | #: inc/authorization.class.php:317 154 | msgid "Refresh connection diagnostic" 155 | msgstr "Actualizar diagnóstico de conexión" 156 | 157 | #: inc/authorization.class.php:330 158 | msgid "" 159 | "Diagnostic log contains sensitive information, such as the access token." 160 | msgstr "" 161 | "El registro de diagnóstico contiene información sensible, como el token de " 162 | "acceso." 163 | 164 | #: inc/authorization.class.php:349 165 | #, php-format 166 | msgid "Unexpected error: %s" 167 | msgstr "Error inesperado:%s" 168 | 169 | #: front/authorization.process.php:43 170 | #, php-format 171 | msgid "Authorization failed with error: %s" 172 | msgstr "Autorización fallida con error:%s" 173 | 174 | #: front/authorization.process.php:53 175 | msgid "Unable to verify authorization code" 176 | msgstr "No se puede verificar el código de autorización" 177 | 178 | #: front/authorization.process.php:55 179 | msgid "Unable to get authorization code" 180 | msgstr "No se puede obtener el código de autorización" 181 | 182 | #: front/authorization.process.php:57 183 | msgid "Unable to save authorization code" 184 | msgstr "No se puede guardar el código de autorización" 185 | 186 | #: ajax/dropdownAuthorization.php:48 187 | msgid "Create authorization for another user" 188 | msgstr "Crear autorización para otro usuario" 189 | -------------------------------------------------------------------------------- /locales/es_MX.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Carlos Moreno Rodríguez , 2023 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2025-11-10 01:44+0000\n" 15 | "PO-Revision-Date: 2020-10-15 13:49+0000\n" 16 | "Last-Translator: Carlos Moreno Rodríguez , 2023\n" 17 | "Language-Team: Spanish (Mexico) (https://app.transifex.com/teclib/teams/28042/es_MX/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: es_MX\n" 22 | "Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 23 | 24 | #: templates/application_form_extra.html.twig 25 | msgid "Callback url" 26 | msgstr "URL de redirección" 27 | 28 | #: setup.php:102 inc/application.class.php:45 29 | msgid "OAuth IMAP" 30 | msgid_plural "OAuth IMAP" 31 | msgstr[0] "" 32 | msgstr[1] "" 33 | msgstr[2] "" 34 | 35 | #: inc/application.class.php:88 inc/application.class.php:122 36 | msgid "Oauth provider" 37 | msgstr "Proveedor" 38 | 39 | #: inc/application.class.php:94 inc/application.class.php:131 40 | msgid "Client ID" 41 | msgstr "Id. de aplicación - cliente" 42 | 43 | #: inc/application.class.php:100 44 | msgid "Client secret" 45 | msgstr "Frase secreta de cliente" 46 | 47 | #: inc/application.class.php:106 inc/application.class.php:139 48 | msgid "Tenant ID" 49 | msgstr "Id. de directorio (inquilino)" 50 | 51 | #: inc/application.class.php:182 52 | msgid "Developer help for this provider" 53 | msgstr "Ayuda para desarrolladores de este proveedor" 54 | 55 | #: inc/application.class.php:346 56 | msgid "Name cannot be empty" 57 | msgstr "El campo nombre no puede estar vacío" 58 | 59 | #: inc/application.class.php:355 60 | msgid "Invalid provider" 61 | msgstr "Proveedor no válido" 62 | 63 | #: inc/mailcollectorfeature.class.php:367 64 | #, php-format 65 | msgid "Mail receiver \"%s\" has been updated." 66 | msgstr "El receptor de correo electrónico \\\"%s\\\" se ha actualizado." 67 | 68 | #: inc/mailcollectorfeature.class.php:397 69 | #, php-format 70 | msgid "Mail receiver \"%s\" has been deactivated." 71 | msgstr "Receptor de correo electrónico \\\"%s\\\" se ha desactivado." 72 | 73 | #: inc/mailcollectorfeature.class.php:462 74 | msgid "No associated receivers." 75 | msgstr "Ningún receptor de correo electrónico asociado." 76 | 77 | #: inc/mailcollectorfeature.class.php:465 78 | msgid "Name" 79 | msgstr "Nombre" 80 | 81 | #: inc/mailcollectorfeature.class.php:466 82 | msgid "Connection string" 83 | msgstr "Cadena de conexión" 84 | 85 | #: inc/mailcollectorfeature.class.php:467 86 | msgid "Login" 87 | msgstr "Usuario" 88 | 89 | #: inc/mailcollectorfeature.class.php:468 90 | msgid "Is active ?" 91 | msgstr "¿Está activo?" 92 | 93 | #: inc/authorization.class.php:59 94 | msgid "Oauth authorization" 95 | msgid_plural "Oauth authorizations" 96 | msgstr[0] "Permiso OAuth" 97 | msgstr[1] "Permisos OAuth" 98 | msgstr[2] "Permisos OAuth" 99 | 100 | #: inc/authorization.class.php:111 101 | msgid "Create an authorization" 102 | msgstr "Autorizar cuenta" 103 | 104 | #: inc/authorization.class.php:121 105 | msgid "No authorizations." 106 | msgstr "Sin autorizaciones." 107 | 108 | #: inc/authorization.class.php:125 inc/authorization.class.php:182 109 | #: inc/authorization.class.php:238 110 | msgid "Email" 111 | msgstr "Dirección de correo electrónico" 112 | 113 | #: inc/authorization.class.php:142 114 | msgid "Connection diagnostic" 115 | msgstr "Diagnóstico de conexión" 116 | 117 | #: inc/authorization.class.php:147 118 | msgid "Diagnose" 119 | msgstr "Diagnosticar" 120 | 121 | #: inc/authorization.class.php:151 122 | msgid "Update" 123 | msgstr "Actualizar" 124 | 125 | #: inc/authorization.class.php:159 126 | msgid "Delete" 127 | msgstr "Eliminar" 128 | 129 | #: inc/authorization.class.php:253 130 | msgid "Server host" 131 | msgstr "Servidor" 132 | 133 | #: inc/authorization.class.php:264 134 | msgid "Server port" 135 | msgstr "Puerto" 136 | 137 | #: inc/authorization.class.php:281 138 | msgid "Security level" 139 | msgstr "Nivel de seguridad" 140 | 141 | #: inc/authorization.class.php:288 142 | msgid "SSL" 143 | msgstr "SSL" 144 | 145 | #: inc/authorization.class.php:289 146 | msgid "SSL + TLS" 147 | msgstr "SSL + TLS" 148 | 149 | #: inc/authorization.class.php:298 150 | msgid "Timeout" 151 | msgstr "Timeout" 152 | 153 | #: inc/authorization.class.php:317 154 | msgid "Refresh connection diagnostic" 155 | msgstr "Actualizar diagnóstico de conexión" 156 | 157 | #: inc/authorization.class.php:330 158 | msgid "" 159 | "Diagnostic log contains sensitive information, such as the access token." 160 | msgstr "" 161 | "El registro de diagnóstico contiene información confidencial, como el token " 162 | "de acceso." 163 | 164 | #: inc/authorization.class.php:349 165 | #, php-format 166 | msgid "Unexpected error: %s" 167 | msgstr "Error inesperado: %s" 168 | 169 | #: front/authorization.process.php:43 170 | #, php-format 171 | msgid "Authorization failed with error: %s" 172 | msgstr "La autorización falló con error: %s" 173 | 174 | #: front/authorization.process.php:53 175 | msgid "Unable to verify authorization code" 176 | msgstr "No se puede verificar el código de autorización" 177 | 178 | #: front/authorization.process.php:55 179 | msgid "Unable to get authorization code" 180 | msgstr "No se puede obtener el código de autorización" 181 | 182 | #: front/authorization.process.php:57 183 | msgid "Unable to save authorization code" 184 | msgstr "No se puede guardar el código de autorización" 185 | 186 | #: ajax/dropdownAuthorization.php:48 187 | msgid "Create authorization for another user" 188 | msgstr "Crear autorización para otra cuenta" 189 | -------------------------------------------------------------------------------- /locales/pt_BR.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Jean Vergaças , 2020 8 | # Cédric Anne, 2021 9 | # Matheus Rafael, 2023 10 | # Diego Nobre , 2023 11 | # Eduardo Mozart de Oliveira , 2024 12 | # 13 | #, fuzzy 14 | msgid "" 15 | msgstr "" 16 | "Project-Id-Version: PACKAGE VERSION\n" 17 | "Report-Msgid-Bugs-To: \n" 18 | "POT-Creation-Date: 2025-11-10 01:44+0000\n" 19 | "PO-Revision-Date: 2020-10-15 13:49+0000\n" 20 | "Last-Translator: Eduardo Mozart de Oliveira , 2024\n" 21 | "Language-Team: Portuguese (Brazil) (https://app.transifex.com/teclib/teams/28042/pt_BR/)\n" 22 | "MIME-Version: 1.0\n" 23 | "Content-Type: text/plain; charset=UTF-8\n" 24 | "Content-Transfer-Encoding: 8bit\n" 25 | "Language: pt_BR\n" 26 | "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 27 | 28 | #: templates/application_form_extra.html.twig 29 | msgid "Callback url" 30 | msgstr "URL de callback" 31 | 32 | #: setup.php:102 inc/application.class.php:45 33 | msgid "OAuth IMAP" 34 | msgid_plural "OAuth IMAP" 35 | msgstr[0] "" 36 | msgstr[1] "" 37 | msgstr[2] "" 38 | 39 | #: inc/application.class.php:88 inc/application.class.php:122 40 | msgid "Oauth provider" 41 | msgstr "Provedor OAuth" 42 | 43 | #: inc/application.class.php:94 inc/application.class.php:131 44 | msgid "Client ID" 45 | msgstr "ID do Cliente" 46 | 47 | #: inc/application.class.php:100 48 | msgid "Client secret" 49 | msgstr "Segredo do cliente" 50 | 51 | #: inc/application.class.php:106 inc/application.class.php:139 52 | msgid "Tenant ID" 53 | msgstr "ID do Cliente" 54 | 55 | #: inc/application.class.php:182 56 | msgid "Developer help for this provider" 57 | msgstr "Ajuda do desenvolvedor para este provedor" 58 | 59 | #: inc/application.class.php:346 60 | msgid "Name cannot be empty" 61 | msgstr "O nome não pode ser vazio" 62 | 63 | #: inc/application.class.php:355 64 | msgid "Invalid provider" 65 | msgstr "Provedor inválido" 66 | 67 | #: inc/mailcollectorfeature.class.php:367 68 | #, php-format 69 | msgid "Mail receiver \"%s\" has been updated." 70 | msgstr "O receptor de e-mail \"%s\" foi atualizado." 71 | 72 | #: inc/mailcollectorfeature.class.php:397 73 | #, php-format 74 | msgid "Mail receiver \"%s\" has been deactivated." 75 | msgstr "O receptor de e-mail \"%s\" foi desativado." 76 | 77 | #: inc/mailcollectorfeature.class.php:462 78 | msgid "No associated receivers." 79 | msgstr "Sem receptores associados." 80 | 81 | #: inc/mailcollectorfeature.class.php:465 82 | msgid "Name" 83 | msgstr "Nome" 84 | 85 | #: inc/mailcollectorfeature.class.php:466 86 | msgid "Connection string" 87 | msgstr "String de conexão" 88 | 89 | #: inc/mailcollectorfeature.class.php:467 90 | msgid "Login" 91 | msgstr "Login" 92 | 93 | #: inc/mailcollectorfeature.class.php:468 94 | msgid "Is active ?" 95 | msgstr "Está ativo?" 96 | 97 | #: inc/authorization.class.php:59 98 | msgid "Oauth authorization" 99 | msgid_plural "Oauth authorizations" 100 | msgstr[0] "Autorização Oauth" 101 | msgstr[1] "Autorizações Oauth" 102 | msgstr[2] "Autorizações OAuth" 103 | 104 | #: inc/authorization.class.php:111 105 | msgid "Create an authorization" 106 | msgstr "Crie uma autorização" 107 | 108 | #: inc/authorization.class.php:121 109 | msgid "No authorizations." 110 | msgstr "Sem autorizações." 111 | 112 | #: inc/authorization.class.php:125 inc/authorization.class.php:182 113 | #: inc/authorization.class.php:238 114 | msgid "Email" 115 | msgstr "E-mail" 116 | 117 | #: inc/authorization.class.php:142 118 | msgid "Connection diagnostic" 119 | msgstr "Diagnóstico de conexão" 120 | 121 | #: inc/authorization.class.php:147 122 | msgid "Diagnose" 123 | msgstr "Diagnosticar" 124 | 125 | #: inc/authorization.class.php:151 126 | msgid "Update" 127 | msgstr "Atualizar" 128 | 129 | #: inc/authorization.class.php:159 130 | msgid "Delete" 131 | msgstr "Excluir" 132 | 133 | #: inc/authorization.class.php:253 134 | msgid "Server host" 135 | msgstr "Host do servidor" 136 | 137 | #: inc/authorization.class.php:264 138 | msgid "Server port" 139 | msgstr "Porta do servidor" 140 | 141 | #: inc/authorization.class.php:281 142 | msgid "Security level" 143 | msgstr "Nível de segurança" 144 | 145 | #: inc/authorization.class.php:288 146 | msgid "SSL" 147 | msgstr "SSL" 148 | 149 | #: inc/authorization.class.php:289 150 | msgid "SSL + TLS" 151 | msgstr "SSL + TLS" 152 | 153 | #: inc/authorization.class.php:298 154 | msgid "Timeout" 155 | msgstr "Timeout" 156 | 157 | #: inc/authorization.class.php:317 158 | msgid "Refresh connection diagnostic" 159 | msgstr "Atualizar diagnóstico de conexão" 160 | 161 | #: inc/authorization.class.php:330 162 | msgid "" 163 | "Diagnostic log contains sensitive information, such as the access token." 164 | msgstr "" 165 | "O log de diagnóstico contém informações confidenciais, como o token de " 166 | "acesso." 167 | 168 | #: inc/authorization.class.php:349 169 | #, php-format 170 | msgid "Unexpected error: %s" 171 | msgstr "Erro inesperado: %s" 172 | 173 | #: front/authorization.process.php:43 174 | #, php-format 175 | msgid "Authorization failed with error: %s" 176 | msgstr "Falha na autorização com erro: %s" 177 | 178 | #: front/authorization.process.php:53 179 | msgid "Unable to verify authorization code" 180 | msgstr "Não é possível verificar o código de autorização" 181 | 182 | #: front/authorization.process.php:55 183 | msgid "Unable to get authorization code" 184 | msgstr "Incapaz de obter código de autorização" 185 | 186 | #: front/authorization.process.php:57 187 | msgid "Unable to save authorization code" 188 | msgstr "Incapaz de salvar código de autorização" 189 | 190 | #: ajax/dropdownAuthorization.php:48 191 | msgid "Create authorization for another user" 192 | msgstr "Criar autorização para outro usuário" 193 | -------------------------------------------------------------------------------- /oauthimap.xml: -------------------------------------------------------------------------------- 1 | 2 | Oauth IMAP 3 | oauthimap 4 | stable 5 | https://raw.githubusercontent.com/pluginsGLPI/oauthimap/main/docs/logo.png 6 | 7 | 8 | Ce plugin supporte la connexion Oauth pour les collecteurs de mails 9 | This plugin supports Oauth connection for emails receivers 10 | 11 | 12 | 13 | Ce plugin supporte la connexion Oauth pour les collecteurs de mails. 14 | Ceci permet de collecter des emails sur les boîtes mail des domaines G Suite et Azure AD. 15 | 16 | 17 | This plugin supports Oauth connection for emails receivers 18 | It permits emails fetching from G Suite and Azure AD mailboxes. 19 | 20 | 21 | 22 | https://github.com/pluginsGLPI/oauthimap/ 23 | https://github.com/pluginsGLPI/oauthimap/releases 24 | https://github.com/pluginsGLPI/oauthimap/issues 25 | https://glpi-plugins.readthedocs.io/en/latest/oauthimap/index.html 26 | 27 | TECLIB' 28 | 29 | 30 | 31 | 1.4.4 32 | ~10.0.11 33 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.4.4/glpi-oauthimap-1.4.4.tar.bz2 34 | 35 | 36 | 1.5.0 37 | ~11.0.0 38 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.5.0/glpi-oauthimap-1.5.0.tar.bz2 39 | 40 | 41 | 1.4.3 42 | ~10.0.7 43 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.4.3/glpi-oauthimap-1.4.3.tar.bz2 44 | 45 | 46 | 1.4.2 47 | ~10.0.0 48 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.4.2/glpi-oauthimap-1.4.2.tar.bz2 49 | 50 | 51 | 1.4.1 52 | ~10.0.0 53 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.4.1/glpi-oauthimap-1.4.1.tar.bz2 54 | 55 | 56 | 1.4.0 57 | ~10.0.0 58 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.4.0/glpi-oauthimap-1.4.0.tar.bz2 59 | 60 | 61 | 1.3.4 62 | ~9.5.0 63 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.3.4/glpi-oauthimap-1.3.4.tar.bz2 64 | 65 | 66 | 1.3.3 67 | ~9.5.0 68 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.3.3/glpi-oauthimap-1.3.3.tar.bz2 69 | 70 | 71 | 1.3.2 72 | ~9.5.0 73 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.3.2/glpi-oauthimap-1.3.2.tar.bz2 74 | 75 | 76 | 1.3.1 77 | ~9.5.0 78 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.3.1/glpi-oauthimap-1.3.1.tar.bz2 79 | 80 | 81 | 1.3.0 82 | ~9.5.0 83 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.3.0/glpi-oauthimap-1.3.0.tar.bz2 84 | 85 | 86 | 1.2.0 87 | ~9.5.0 88 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.2.0/glpi-oauthimap-1.2.0.tar.bz2 89 | 90 | 91 | 1.1.0 92 | ~9.5.0 93 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.1.0/glpi-oauthimap-1.1.0.tar.bz2 94 | 95 | 96 | 1.0.4 97 | ~9.5.0 98 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.0.4/glpi-oauthimap-1.0.4.tar.bz2 99 | 100 | 101 | 1.0.3 102 | ~9.5.0 103 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.0.3/glpi-oauthimap-1.0.3.tar.bz2 104 | 105 | 106 | 1.0.2 107 | ~9.5.0 108 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.0.2/glpi-oauthimap-1.0.2.tar.bz2 109 | 110 | 111 | 1.0.1 112 | ~9.5.0 113 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.0.1/glpi-oauthimap-1.0.1.tar.bz2 114 | 115 | 116 | 1.0.0 117 | ~9.5.0 118 | https://github.com/pluginsGLPI/oauthimap/releases/download/1.0.0/glpi-oauthimap-1.0.0.tar.bz2 119 | 120 | 121 | 122 | en_GB 123 | fr_FR 124 | 125 | GPL v2+ 126 | 127 | 128 | IMAP 129 | oauth 130 | collecteur 131 | 132 | 133 | IMAP 134 | oauth 135 | receiver 136 | 137 | 138 | 139 | https://raw.githubusercontent.com/pluginsGLPI/oauthimap/main/docs/screenshots/config.png 140 | https://raw.githubusercontent.com/pluginsGLPI/oauthimap/main/docs/screenshots/config_oauth_mailcollector.png 141 | 142 | 143 | -------------------------------------------------------------------------------- /inc/imap/imapoauthprotocol.class.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | namespace GlpiPlugin\Oauthimap\Imap; 32 | 33 | use Glpi\Mail\Protocol\ProtocolInterface; 34 | use Laminas\Mail\Protocol\Exception\RuntimeException; 35 | use Laminas\Mail\Protocol\Imap; 36 | use PluginOauthimapAuthorization; 37 | 38 | use function Safe\fwrite; 39 | use function Safe\preg_match; 40 | 41 | class ImapOauthProtocol extends Imap implements ProtocolInterface 42 | { 43 | /** 44 | * Prefix to use when writing a sent line in diagnostic log. 45 | * 46 | * @var string 47 | */ 48 | private const DIAGNOSTIC_PREFIX_SENT = '>>> '; 49 | 50 | /** 51 | * Prefix to use when writing a received line in diagnostic log. 52 | * 53 | * @var string 54 | */ 55 | private const DIAGNOSTIC_PREFIX_RECEIVED = '<<< '; 56 | 57 | /** 58 | * ID of PluginOauthimapApplication to use. 59 | * 60 | * @var int 61 | */ 62 | private $application_id; 63 | 64 | /** 65 | * Indicates whether diagnostic is enabled. 66 | * 67 | * @var boolean 68 | */ 69 | private $diagnostic_enabled = false; 70 | 71 | /** 72 | * Diagnostic log. 73 | * 74 | * @var string[] 75 | */ 76 | private $diagnostic_log = []; 77 | 78 | /** 79 | * Connection timeout. 80 | * 81 | * @var int 82 | */ 83 | private $timeout = self::TIMEOUT_CONNECTION; 84 | 85 | /** 86 | * @param int $application_id ID of PluginOauthimapApplication to use 87 | */ 88 | public function __construct($application_id) 89 | { 90 | $this->application_id = $application_id; 91 | parent::__construct(); 92 | } 93 | 94 | /** 95 | * Almost identical to parent class method, just to be able to redefine timeout in case of diagnostic. 96 | * 97 | * {@inheritDoc} 98 | */ 99 | public function connect($host, $port = null, $ssl = false) 100 | { 101 | $transport = 'tcp'; 102 | $isTls = false; 103 | 104 | if ($ssl) { 105 | $ssl = strtolower($ssl); 106 | } 107 | 108 | switch ($ssl) { 109 | case 'ssl': 110 | $transport = 'ssl'; 111 | if (!$port) { 112 | $port = 993; 113 | } 114 | break; 115 | case 'tls': 116 | $isTls = true; 117 | // break intentionally omitted 118 | // no break 119 | default: 120 | if (!$port) { 121 | $port = 143; 122 | } 123 | } 124 | 125 | $this->socket = $this->setupSocket($transport, $host, $port, $this->timeout); 126 | 127 | if (!$this->assumedNextLine('* OK')) { 128 | throw new RuntimeException('host doesn\'t allow connection'); 129 | } 130 | 131 | if ($isTls) { 132 | $result = $this->requestAndResponse('STARTTLS'); 133 | $result = $result && stream_socket_enable_crypto($this->socket, true, $this->getCryptoMethod()); 134 | if (!$result) { 135 | throw new RuntimeException('cannot enable TLS'); 136 | } 137 | } 138 | } 139 | 140 | public function login($user, $password) 141 | { 142 | $token = PluginOauthimapAuthorization::getAccessTokenForApplicationAndEmail($this->application_id, $user); 143 | 144 | if ($token === null) { 145 | trigger_error('Unable to get access token', E_USER_WARNING); 146 | 147 | return false; 148 | } 149 | 150 | $this->sendRequest( 151 | 'AUTHENTICATE', 152 | [ 153 | 'XOAUTH2', 154 | base64_encode("user={$user}\001auth=Bearer {$token}\001\001"), 155 | ], 156 | ); 157 | 158 | while (true) { 159 | $response = ''; 160 | $isPlus = $this->readLine($response, '+', true); 161 | if ($isPlus) { 162 | // Send empty client response. 163 | $this->sendRequest(''); 164 | } else { 165 | if ( 166 | preg_match('/^NO /i', $response) || preg_match('/^BAD /i', $response) 167 | ) { 168 | return false; 169 | } 170 | if (preg_match('/^OK /i', $response) !== 0) { 171 | return true; 172 | } 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * Almost identical to parent class method, some `$this->addToDiagnosticLog()` calls were added. 179 | * 180 | * {@inheritDoc} 181 | */ 182 | public function sendRequest($command, $tokens = [], &$tag = '') 183 | { 184 | if (!$tag) { 185 | ++$this->tagCount; 186 | $tag = 'TAG' . $this->tagCount; 187 | } 188 | 189 | $line = $tag . ' ' . $command; 190 | 191 | foreach ($tokens as $token) { 192 | if (is_array($token)) { 193 | $tosend = $line . ' ' . $token[0] . "\r\n"; 194 | fwrite($this->socket, $tosend); 195 | $this->addToDiagnosticLog($tosend, self::DIAGNOSTIC_PREFIX_SENT); 196 | if (!$this->assumedNextLine('+ ')) { 197 | throw new RuntimeException('cannot send literal string'); 198 | } 199 | $line = $token[1]; 200 | } else { 201 | $line .= ' ' . $token; 202 | } 203 | } 204 | 205 | $tosend = $line . "\r\n"; 206 | fwrite($this->socket, $line . "\r\n"); 207 | $this->addToDiagnosticLog($tosend, self::DIAGNOSTIC_PREFIX_SENT); 208 | } 209 | 210 | /** 211 | * Almost identical to parent class method, `$this->addToDiagnosticLog()` call added. 212 | * 213 | * {@inheritDoc} 214 | */ 215 | protected function nextLine() 216 | { 217 | $line = fgets($this->socket); 218 | if ($line === false) { 219 | throw new RuntimeException('cannot read - connection closed?'); 220 | } 221 | $this->addToDiagnosticLog($line, self::DIAGNOSTIC_PREFIX_RECEIVED); 222 | 223 | return $line; 224 | } 225 | 226 | /** 227 | * Enable diagnostic. 228 | * 229 | * @return void 230 | */ 231 | public function enableDiagnostic(): void 232 | { 233 | $this->diagnostic_enabled = true; 234 | } 235 | 236 | /** 237 | * Get the diagnostic log. 238 | * 239 | * @return string 240 | */ 241 | public function getDiagnosticLog(): string 242 | { 243 | return implode('', $this->diagnostic_log); 244 | } 245 | 246 | /** 247 | * Add line to diagnostic log. 248 | * 249 | * @param string $line 250 | * @param string $prefix 251 | * 252 | * @return void 253 | */ 254 | private function addToDiagnosticLog(string $line, string $prefix = '') 255 | { 256 | if (!$this->diagnostic_enabled) { 257 | return; 258 | } 259 | $this->diagnostic_log[] = $prefix . $line; 260 | } 261 | 262 | /** 263 | * Defines socket timeout. 264 | * 265 | * @param int $timeout 266 | * 267 | * @return void 268 | */ 269 | public function setTimeout(int $timeout): void 270 | { 271 | $this->timeout = $timeout; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /inc/mailcollectorfeature.class.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | 31 | namespace GlpiPlugin\Oauthimap; 32 | 33 | use CommonGLPI; 34 | use Dropdown; 35 | use GlpiPlugin\Oauthimap\Imap\ImapOauthProtocol; 36 | use GlpiPlugin\Oauthimap\Imap\ImapOauthStorage; 37 | use Html; 38 | use MailCollector; 39 | use PluginOauthimapApplication; 40 | use PluginOauthimapAuthorization; 41 | use Session; 42 | 43 | use function Safe\preg_replace; 44 | 45 | class MailCollectorFeature extends CommonGLPI 46 | { 47 | public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) 48 | { 49 | if ($item instanceof PluginOauthimapApplication) { 50 | $count = 0; 51 | if ($_SESSION['glpishow_count_on_tabs']) { 52 | $collectors = MailCollectorFeature::getAssociatedMailCollectors( 53 | MailCollectorFeature::getMailProtocolTypeIdentifier($item->getID()), 54 | null, 55 | false, 56 | ); 57 | $count = count($collectors); 58 | } 59 | 60 | return CommonGLPI::createTabEntry(MailCollector::getTypeName(Session::getPluralNumber()), $count); 61 | } 62 | 63 | return ''; 64 | } 65 | 66 | public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) 67 | { 68 | if ($item instanceof PluginOauthimapApplication) { 69 | MailCollectorFeature::showMailCollectorsForApplication($item); 70 | } 71 | 72 | return false; 73 | } 74 | 75 | /** 76 | * Get mail protocols specs as expected by 'mail_server_protocols' hook. 77 | */ 78 | public static function getMailProtocols() 79 | { 80 | $mail_protocols = []; 81 | 82 | $values = getAllDataFromTable( 83 | PluginOauthimapApplication::getTable(), 84 | [ 85 | 'WHERE' => [ 86 | 'is_active' => 1, 87 | ], 88 | ], 89 | ); 90 | 91 | foreach ($values as $value) { 92 | $id = $value['id']; 93 | $protocol_class = (fn() => new ImapOauthProtocol($id)); 94 | $storage_class = function (array $params) use ($id) { 95 | $params['application_id'] = $id; 96 | 97 | return new ImapOauthStorage($params); 98 | }; 99 | $mail_protocols[self::getMailProtocolTypeIdentifier($id)] = [ 100 | 'label' => $value['name'], 101 | 'protocol' => $protocol_class, 102 | 'storage' => $storage_class, 103 | ]; 104 | } 105 | 106 | return $mail_protocols; 107 | } 108 | 109 | /** 110 | * Return mail protocol type identifier. 111 | * 112 | * @param int $application_id 113 | * 114 | * @return string 115 | */ 116 | public static function getMailProtocolTypeIdentifier($application_id) 117 | { 118 | return sprintf('imap-oauth-%s', $application_id); 119 | } 120 | 121 | /** 122 | * Alter MailCollector form in order to handle IMAP Oauth connections. 123 | * 124 | * @return void 125 | */ 126 | public static function alterMailCollectorForm(): void 127 | { 128 | $locator_id = 'plugin_oauthimap_locator_' . mt_rand(); 129 | 130 | echo ''; 131 | $javascript = <<'); 141 | var application_field = form.find('[name="plugin_oauthimap_applications_id"]'); 142 | 143 | login_field.parent().append(''); 144 | var auth_field_container = $('#auth_field_container'); 145 | 146 | server_type_field.on( 147 | 'change', 148 | function (evt) { 149 | if (/^\/imap-oauth-\d+$/.test($(this).val())) { 150 | var application_id = $(this).val().replace('/imap-oauth-', ''); 151 | 152 | password_field.closest('tr').hide(); 153 | login_field.hide(); 154 | auth_field_container.show(); 155 | 156 | application_field.val(application_id); 157 | auth_field_container.load( 158 | '/plugins/oauthimap/ajax/dropdownAuthorization.php', 159 | { 160 | application_id: application_id, 161 | selected: login_field.val() 162 | } 163 | ); 164 | } else { 165 | password_field.closest('tr').show(); 166 | auth_field_container.hide(); 167 | login_field.show(); 168 | 169 | application_field.val(''); 170 | } 171 | } 172 | ); 173 | 174 | // Change login field value to trigger mail collector update 175 | auth_field_container.on( 176 | 'change', 177 | 'select', 178 | function (evt) { 179 | if ($(this).val() == -1) { 180 | login_field.val(''); 181 | } else { 182 | login_field.val($(this).find('option:selected').text()); 183 | } 184 | } 185 | ); 186 | 187 | server_type_field.trigger('change'); 188 | } 189 | ); 190 | JAVASCRIPT; 191 | 192 | echo Html::scriptBlock($javascript); 193 | } 194 | 195 | /** 196 | * Force mailcollector update if oauth fields should trigger an authorization request. 197 | * 198 | * @param MailCollector $item 199 | * 200 | * @return boolean 201 | */ 202 | public static function forceMailCollectorUpdate(MailCollector $item) 203 | { 204 | if ( 205 | !array_key_exists('plugin_oauthimap_applications_id', $item->input) 206 | || !array_key_exists('plugin_oauthimap_authorizations_id', $item->input) 207 | ) { 208 | // Plugin fields are not present, update was not made inside form. 209 | return true; 210 | } 211 | 212 | if ($item->input['plugin_oauthimap_applications_id'] <= 0) { 213 | // No application selected => mail collector does not use Oauth. 214 | // Return true to continue update. 215 | return true; 216 | } 217 | if ($item->input['plugin_oauthimap_authorizations_id'] > 0) { 218 | // Existing authorization selected => no need to trigger authorization request. 219 | // Return true to continue update. 220 | return true; 221 | } 222 | 223 | // Defines "date_mod" field of mail collector to force its update. 224 | // Indeed, if no mail collector field changed, "item_update" hook will not be called and authorization request 225 | // will not be triggered. 226 | $item->input['date_mod'] = $_SESSION['glpi_currenttime']; 227 | 228 | return true; 229 | } 230 | 231 | /** 232 | * Handle authorization process after creation/update of a mail collector. 233 | * 234 | * @param MailCollector $item 235 | * 236 | * @return void 237 | */ 238 | public static function handleMailCollectorSaving(MailCollector $item): void 239 | { 240 | if ( 241 | !array_key_exists('plugin_oauthimap_applications_id', $item->input) 242 | || !array_key_exists('plugin_oauthimap_authorizations_id', $item->input) 243 | ) { 244 | // Plugin fields are not present, update was not made inside form. 245 | return; 246 | } 247 | 248 | $applications_id = $item->input['plugin_oauthimap_applications_id']; 249 | $authorizations_id = $item->input['plugin_oauthimap_authorizations_id']; 250 | 251 | if ($applications_id <= 0) { 252 | // No application selected => mail collector does not use Oauth. 253 | return; 254 | } 255 | 256 | $application = new PluginOauthimapApplication(); 257 | $application->getFromDB($applications_id); 258 | $authorization = new PluginOauthimapAuthorization(); 259 | 260 | if ($authorizations_id > 0 && $authorization->getFromDB($authorizations_id)) { 261 | // Use existing authorization 262 | self::updateMailCollectorOnAuthorizationCallback( 263 | true, 264 | $authorization, 265 | [ 266 | MailCollector::getForeignKeyField() => $item->getID(), 267 | ], 268 | ); 269 | } else { 270 | // Create new authorization 271 | $application->redirectToAuthorizationUrl( 272 | [self::class, 'updateMailCollectorOnAuthorizationCallback'], 273 | [ 274 | MailCollector::getForeignKeyField() => $item->getID(), 275 | ], 276 | ); 277 | } 278 | } 279 | 280 | /** 281 | * Update login field of mail collector on authorization callback. 282 | * 283 | * @param bool $success 284 | * @param PluginOauthimapAuthorization $authorization 285 | * @param array $params 286 | * 287 | * @return void 288 | */ 289 | public static function updateMailCollectorOnAuthorizationCallback( 290 | bool $success, 291 | PluginOauthimapAuthorization $authorization, 292 | array $params = [] 293 | ): void { 294 | $mailcollector = new MailCollector(); 295 | $redirect = $mailcollector->getSearchURL(); 296 | if ($success) { 297 | // Store authorized email into MailCollector 298 | $mailcollector_id = $params[$mailcollector->getForeignKeyField()] ?? null; 299 | if ($mailcollector_id !== null && $mailcollector->getFromDB($mailcollector_id)) { 300 | $mailcollector->update( 301 | [ 302 | 'id' => $mailcollector_id, 303 | 'login' => $authorization->fields['email'], 304 | ], 305 | ); 306 | $redirect = $mailcollector->getLinkURL(); 307 | } 308 | } 309 | 310 | Html::redirect($redirect); 311 | } 312 | 313 | /** 314 | * Deactivate mail collectors linked to the application. 315 | * 316 | * @param PluginOauthimapApplication $application 317 | * 318 | * @return void 319 | */ 320 | public static function postDeactivateApplication(PluginOauthimapApplication $application): void 321 | { 322 | self::deactivateMailCollectors( 323 | self::getMailProtocolTypeIdentifier($application->getID()), 324 | ); 325 | } 326 | 327 | /** 328 | * Deactivate mail collectors linked to the authorization. 329 | * 330 | * @param PluginOauthimapAuthorization $authorization 331 | * 332 | * @return void 333 | */ 334 | public static function postPurgeAuthorization(PluginOauthimapAuthorization $authorization): void 335 | { 336 | $application_id = $authorization->fields[PluginOauthimapApplication::getForeignKeyField()]; 337 | self::deactivateMailCollectors( 338 | self::getMailProtocolTypeIdentifier($application_id), 339 | $authorization->fields['email'], 340 | ); 341 | } 342 | 343 | /** 344 | * Update mail collectors linked to the authorization. 345 | * 346 | * @param PluginOauthimapAuthorization $authorization 347 | * 348 | * @return void 349 | */ 350 | public static function postUpdateAuthorization(PluginOauthimapAuthorization $authorization): void 351 | { 352 | if (in_array('email', $authorization->updates) && array_key_exists('email', $authorization->oldvalues)) { 353 | $collectors = self::getAssociatedMailCollectors( 354 | self::getMailProtocolTypeIdentifier($authorization->fields[PluginOauthimapApplication::getForeignKeyField()]), 355 | $authorization->oldvalues['email'], 356 | ); 357 | foreach ($collectors as $row) { 358 | $mailcollector = new MailCollector(); 359 | $mailcollector->update( 360 | [ 361 | 'id' => $row['id'], 362 | 'login' => $authorization->fields['email'], 363 | ], 364 | ); 365 | Session::addMessageAfterRedirect( 366 | sprintf( 367 | __s('Mail receiver "%s" has been updated.', 'oauthimap'), 368 | $mailcollector->getName(), 369 | ), 370 | ); 371 | } 372 | } 373 | } 374 | 375 | /** 376 | * Deactivate mail collectors using given protocol type and given login. 377 | * 378 | * @param string $protocol_type 379 | * @param string $login 380 | * 381 | * @return void 382 | */ 383 | private static function deactivateMailCollectors(string $protocol_type, ?string $login = null) 384 | { 385 | $collectors = self::getAssociatedMailCollectors($protocol_type, $login); 386 | 387 | foreach ($collectors as $row) { 388 | $mailcollector = new MailCollector(); 389 | $mailcollector->update( 390 | [ 391 | 'id' => $row['id'], 392 | 'is_active' => 0, 393 | ], 394 | ); 395 | Session::addMessageAfterRedirect( 396 | sprintf( 397 | __s('Mail receiver "%s" has been deactivated.', 'oauthimap'), 398 | $mailcollector->getName(), 399 | ), 400 | ); 401 | } 402 | } 403 | 404 | /** 405 | * Return mail collectors using given protocol type and given login. 406 | * 407 | * @param string $protocol_type 408 | * @param string $login 409 | * @param bool $only_active 410 | * 411 | * @return array 412 | */ 413 | private static function getAssociatedMailCollectors( 414 | string $protocol_type, 415 | ?string $login = null, 416 | bool $only_active = true 417 | ) { 418 | $criteria = []; 419 | if ($only_active) { 420 | $criteria['is_active'] = 1; 421 | } 422 | if ($login !== null) { 423 | $criteria['login'] = $login; 424 | } 425 | 426 | $data = getAllDataFromTable(MailCollector::getTable(), $criteria); 427 | 428 | $result = []; 429 | 430 | foreach ($data as $row) { 431 | // type follows first found "/" and ends on next "/" (or end of server string) 432 | // server string is surrounded by "{}" and can be followed by a folder name 433 | // i.e. "{mail.domain.org/imap/ssl}INBOX", or "{mail.domain.org/pop}" 434 | // 435 | // see Toolbox::parseMailServerConnectString() 436 | $type = preg_replace('/^\{[^\/]+\/([^\/]+)(?:\/.+)*\}.*/', '$1', $row['host']); 437 | if ($type === $protocol_type) { 438 | $result[] = $row; 439 | } 440 | } 441 | 442 | return $result; 443 | } 444 | 445 | /** 446 | * Display "mail collectors" tab of application page. 447 | * 448 | * @param PluginOauthimapApplication $application 449 | * 450 | * @return void 451 | */ 452 | public static function showMailCollectorsForApplication(PluginOauthimapApplication $application): void 453 | { 454 | $collectors = self::getAssociatedMailCollectors( 455 | self::getMailProtocolTypeIdentifier($application->getID()), 456 | null, 457 | false, 458 | ); 459 | 460 | echo ''; 461 | if (count($collectors) === 0) { 462 | echo ''; 463 | } else { 464 | echo ''; 465 | echo ''; 466 | echo ''; 467 | echo ''; 468 | echo ''; 469 | echo ''; 470 | 471 | foreach ($collectors as $row) { 472 | $mailcollector = new MailCollector(); 473 | $mailcollector->getFromDB($row['id']); 474 | 475 | $name = $mailcollector->canViewItem() ? $mailcollector->getLink() : $mailcollector->getNameID(); 476 | 477 | echo ''; 478 | echo ''; 479 | echo ''; 480 | echo ''; 481 | echo ''; 482 | echo ''; 483 | } 484 | } 485 | echo '
' . __s('No associated receivers.', 'oauthimap') . '
' . __s('Name', 'oauthimap') . '' . __s('Connection string', 'oauthimap') . '' . __s('Login', 'oauthimap') . '' . __s('Is active ?', 'oauthimap') . '
' . $name . '' . $row['host'] . '' . $row['login'] . '' . Dropdown::getYesNo($row['is_active']) . '
'; 486 | echo ''; 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /inc/application.class.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | use Glpi\Application\View\TemplateRenderer; 31 | use GlpiPlugin\Oauthimap\MailCollectorFeature; 32 | use GlpiPlugin\Oauthimap\Provider\Azure; 33 | use GlpiPlugin\Oauthimap\Provider\Google; 34 | use GlpiPlugin\Oauthimap\Provider\ProviderInterface; 35 | use League\OAuth2\Client\Provider\AbstractProvider; 36 | 37 | use function Safe\json_encode; 38 | 39 | class PluginOauthimapApplication extends CommonDropdown 40 | { 41 | public static $rightname = 'config'; 42 | 43 | public static function getTypeName($nb = 0) 44 | { 45 | return _sn('OAuth IMAP', 'OAuth IMAP', $nb, 'oauthimap'); 46 | } 47 | 48 | public static function getMenuContent() 49 | { 50 | $menu = []; 51 | if (Config::canUpdate()) { 52 | $menu['title'] = self::getMenuName(); 53 | $menu['page'] = '/plugins/oauthimap/front/application.php'; 54 | $menu['icon'] = self::getIcon(); 55 | } 56 | if (count($menu)) { 57 | return $menu; 58 | } 59 | 60 | return false; 61 | } 62 | 63 | public static function getIcon() 64 | { 65 | return 'ti ti-login-2'; 66 | } 67 | 68 | public static function canCreate(): bool 69 | { 70 | return static::canUpdate(); 71 | } 72 | 73 | public static function canPurge(): bool 74 | { 75 | return static::canUpdate(); 76 | } 77 | 78 | public function getAdditionalFields() 79 | { 80 | return [ 81 | [ 82 | 'name' => 'is_active', 83 | 'label' => __s('Active'), 84 | 'type' => 'bool', 85 | ], 86 | [ 87 | 'name' => 'provider', 88 | 'label' => __s('Oauth provider', 'oauthimap'), 89 | 'type' => 'oauth_provider', 90 | 'list' => true, 91 | ], 92 | [ 93 | 'name' => 'client_id', 94 | 'label' => __s('Client ID', 'oauthimap'), 95 | 'type' => 'text', 96 | 'list' => true, 97 | ], 98 | [ 99 | 'name' => 'client_secret', 100 | 'label' => __s('Client secret', 'oauthimap'), 101 | 'type' => 'secured_field', 102 | 'list' => false, 103 | ], 104 | [ 105 | 'name' => 'tenant_id', 106 | 'label' => __s('Tenant ID', 'oauthimap'), 107 | 'type' => 'additionnal_param', 108 | 'list' => false, 109 | 'provider' => Azure::class, 110 | ], 111 | ]; 112 | } 113 | 114 | public function rawSearchOptions() 115 | { 116 | $tab = parent::rawSearchOptions(); 117 | 118 | $tab[] = [ 119 | 'id' => '5', 120 | 'table' => $this->getTable(), 121 | 'field' => 'provider', 122 | 'name' => __s('Oauth provider', 'oauthimap'), 123 | 'searchtype' => ['equals', 'notequals'], 124 | 'datatype' => 'specific', 125 | ]; 126 | 127 | $tab[] = [ 128 | 'id' => '6', 129 | 'table' => $this->getTable(), 130 | 'field' => 'client_id', 131 | 'name' => __s('Client ID', 'oauthimap'), 132 | 'datatype' => 'text', 133 | ]; 134 | 135 | $tab[] = [ 136 | 'id' => '7', 137 | 'table' => $this->getTable(), 138 | 'field' => 'tenant_id', 139 | 'name' => __s('Tenant ID', 'oauthimap'), 140 | 'datatype' => 'text', 141 | ]; 142 | 143 | return $tab; 144 | } 145 | 146 | public function defineTabs($options = []) 147 | { 148 | $tabs = parent::defineTabs($options); 149 | 150 | $this->addStandardTab(MailCollectorFeature::class, $tabs, $options); 151 | $this->addStandardTab(PluginOauthimapAuthorization::class, $tabs, $options); 152 | 153 | return $tabs; 154 | } 155 | 156 | public function displaySpecificTypeField($ID, $field = [], array $options = []) 157 | { 158 | $rand = sprintf('oauthimap-application-%s', (int) $ID); 159 | 160 | $field_name = $field['name']; 161 | $field_type = $field['type']; 162 | $field_value = $this->fields[$field_name]; 163 | 164 | switch ($field_type) { 165 | case 'oauth_provider': 166 | $values = []; 167 | $icons = []; 168 | foreach (self::getSupportedProviders() as $provider_class) { 169 | $values[$provider_class] = $provider_class::getName(); 170 | $icons[$provider_class] = $provider_class::getIcon(); 171 | } 172 | Dropdown::showFromArray( 173 | $field_name, 174 | $values, 175 | [ 176 | 'display_emptychoice' => true, 177 | 'rand' => $rand, 178 | 'value' => $field_value, 179 | ], 180 | ); 181 | 182 | echo ''; 183 | echo ''; 184 | echo ''; 185 | 186 | $json_icons = json_encode($icons); 187 | $js = << ' + item.text + ''); 195 | }; 196 | 197 | $("#dropdown_{$field_name}{$rand}").select2({ 198 | dropdownAutoWidth: true, 199 | templateSelection: displayOptionIcon, 200 | templateResult: displayOptionIcon, 201 | width: '' 202 | }); 203 | }); 204 | JAVASCRIPT; 205 | echo Html::scriptBlock($js); 206 | break; 207 | case 'secured_field': 208 | echo Html::input( 209 | $field_name, 210 | [ 211 | 'autocomplete' => 'off', 212 | 'value' => (new GLPIKey())->decrypt($field_value), 213 | ], 214 | ); 215 | break; 216 | case 'additionnal_param': 217 | echo Html::input( 218 | $field_name, 219 | [ 220 | 'data-provider' => $field['provider'], 221 | 'value' => $field_value, 222 | ], 223 | ); 224 | break; 225 | default: 226 | throw new RuntimeException(sprintf('Unknown type %s.', $field_type)); 227 | } 228 | } 229 | 230 | public static function getSpecificValueToDisplay($field, $values, array $options = []) 231 | { 232 | if (!is_array($values)) { 233 | $values = [$field => $values]; 234 | } 235 | 236 | switch ($field) { 237 | case 'provider': 238 | $value = $values[$field]; 239 | if (in_array($value, self::getSupportedProviders())) { 240 | return ' ' . $value::getName(); 241 | } 242 | 243 | return $value; 244 | } 245 | 246 | return parent::getSpecificValueToDisplay($field, $values, $options); 247 | } 248 | 249 | public static function getSpecificValueToSelect($field, $name = '', $values = '', array $options = []) 250 | { 251 | if (!is_array($values)) { 252 | $values = [$field => $values]; 253 | } 254 | 255 | switch ($field) { 256 | case 'provider': 257 | $selected = ''; 258 | $elements = ['' => Dropdown::EMPTY_VALUE]; 259 | foreach (self::getSupportedProviders() as $class) { 260 | $elements[$class] = $class::getName(); 261 | if ($class === $values[$field]) { 262 | $selected = $class; 263 | } 264 | } 265 | 266 | return Dropdown::showFromArray( 267 | $name, 268 | $elements, 269 | [ 270 | 'display' => false, 271 | 'value' => $selected, 272 | ], 273 | ); 274 | } 275 | 276 | return parent::getSpecificValueToSelect($field, $name, $values, $options); 277 | } 278 | 279 | /** 280 | * Displays form extra fields/scripts. 281 | * 282 | * @param int $id 283 | * 284 | * @return void 285 | */ 286 | public static function showFormExtra(int $id): void 287 | { 288 | $rand = sprintf('oauthimap-application-%s', $id); 289 | 290 | $documentation_urls_json = json_encode(self::getProvidersDocumentationUrls()); 291 | 292 | $callback_url = self::getCallbackUrl(); 293 | 294 | echo TemplateRenderer::getInstance()->render( 295 | '@oauthimap/application_form_extra.html.twig', 296 | [ 297 | 'rand' => $rand, 298 | 'callback_url' => $callback_url, 299 | 'documentation_urls' => self::getProvidersDocumentationUrls(), 300 | 'documentation_urls_json' => $documentation_urls_json, 301 | ], 302 | ); 303 | } 304 | 305 | public function prepareInputForAdd($input) 306 | { 307 | if (!($input = $this->prepareInput($input))) { 308 | return false; 309 | } 310 | 311 | return parent::prepareInputForAdd($input); 312 | } 313 | 314 | public function prepareInputForUpdate($input) 315 | { 316 | // Unset encrypted fields input if corresponding to current value 317 | // (encryption produces a different value each time, 318 | // so GLPI will consider them as updated on each form submit) 319 | foreach (['client_secret'] as $field_name) { 320 | if ( 321 | array_key_exists($field_name, $input) 322 | && !empty($input[$field_name]) && $input[$field_name] !== 'NULL' 323 | && $input[$field_name] === (new GLPIKey())->decrypt($this->fields[$field_name]) 324 | ) { 325 | unset($input[$field_name]); 326 | } 327 | } 328 | 329 | if (!($input = $this->prepareInput($input))) { 330 | return false; 331 | } 332 | 333 | return parent::prepareInputForUpdate($input); 334 | } 335 | 336 | /** 337 | * Encrypt values of secured fields. 338 | * 339 | * @param array $input 340 | * 341 | * @return bool|array 342 | */ 343 | private function prepareInput($input) 344 | { 345 | if (array_key_exists('name', $input) && empty(trim($input['name']))) { 346 | Session::addMessageAfterRedirect(__s('Name cannot be empty', 'oauthimap'), false, ERROR); 347 | 348 | return false; 349 | } 350 | 351 | if ( 352 | array_key_exists('provider', $input) 353 | && !in_array($input['provider'], self::getSupportedProviders()) 354 | ) { 355 | Session::addMessageAfterRedirect(__s('Invalid provider', 'oauthimap'), false, ERROR); 356 | 357 | return false; 358 | } 359 | 360 | foreach (['client_secret'] as $field_name) { 361 | if ( 362 | array_key_exists($field_name, $input) 363 | && !empty($input[$field_name]) && $input[$field_name] !== 'NULL' 364 | ) { 365 | $input[$field_name] = (new GLPIKey())->encrypt($input[$field_name]); 366 | } 367 | } 368 | 369 | return $input; 370 | } 371 | 372 | // phpcs:ignore PSR1.Methods.CamelCapsMethodName 373 | public function pre_updateInDB() 374 | { 375 | if ( 376 | in_array('provider', $this->updates) 377 | || in_array('client_id', $this->updates) 378 | || in_array('client_secret', $this->updates) 379 | ) { 380 | // Remove codes and tokens if any credentials parameter changed 381 | $this->deleteChildrenAndRelationsFromDb( 382 | [ 383 | PluginOauthimapAuthorization::class, 384 | ], 385 | ); 386 | } 387 | } 388 | 389 | // phpcs:ignore PSR1.Methods.CamelCapsMethodName 390 | public function post_updateItem($history = true) 391 | { 392 | if (in_array('is_active', $this->updates) && !$this->fields['is_active']) { 393 | MailCollectorFeature::postDeactivateApplication($this); 394 | } 395 | } 396 | 397 | /** 398 | * Redirect to authorization URL corresponding to credentials. 399 | * 400 | * @param callable|null $callback_callable Callable to call on authorization callback 401 | * @param array $callback_params Parameters to pass to callable 402 | * 403 | * @return void 404 | */ 405 | public function redirectToAuthorizationUrl(?callable $callback_callable = null, array $callback_params = []): void 406 | { 407 | if (!$this->areCredentialsValid()) { 408 | throw new RuntimeException('Invalid credentials.'); 409 | } 410 | 411 | $provider = $this->getProvider(); 412 | 413 | $options = [ 414 | 'scope' => self::getProviderScopes($this->fields['provider']), 415 | ]; 416 | switch ($this->fields['provider']) { 417 | case Azure::class: 418 | $options['prompt'] = 'login'; 419 | break; 420 | case Google::class: 421 | $options['prompt'] = 'consent select_account'; 422 | break; 423 | } 424 | 425 | $auth_url = $provider->getAuthorizationUrl($options); 426 | 427 | $_SESSION['oauth2state'] = $provider->getState(); 428 | $_SESSION[$this->getForeignKeyField()] = $this->fields['id']; 429 | 430 | $_SESSION['plugin_oauthimap_callback_callable'] = $callback_callable; 431 | $_SESSION['plugin_oauthimap_callback_params'] = $callback_params; 432 | 433 | Html::redirect($auth_url); 434 | } 435 | 436 | /** 437 | * Check if credentials are valid (i.e. all fields are correclty set). 438 | * 439 | * @return bool 440 | */ 441 | private function areCredentialsValid(): bool 442 | { 443 | return !$this->isNewItem() 444 | && array_key_exists('provider', $this->fields) 445 | && in_array($this->fields['provider'], self::getSupportedProviders()) 446 | && array_key_exists('client_id', $this->fields) 447 | && !empty($this->fields['client_id']) 448 | && array_key_exists('client_secret', $this->fields) 449 | && !empty($this->fields['client_secret']); 450 | } 451 | 452 | /** 453 | * Get list of supported providers classnames. 454 | * 455 | * @return array 456 | */ 457 | private static function getSupportedProviders(): array 458 | { 459 | return [ 460 | Azure::class, 461 | Google::class, 462 | ]; 463 | } 464 | 465 | /** 466 | * Returns oauth provider class instance. 467 | * 468 | * @return AbstractProvider|ProviderInterface|null 469 | */ 470 | public function getProvider() 471 | { 472 | /** @var array $CFG_GLPI */ 473 | global $CFG_GLPI; 474 | 475 | if (!$this->areCredentialsValid()) { 476 | throw new RuntimeException('Invalid credentials.'); 477 | } 478 | 479 | if (!is_a($this->fields['provider'], ProviderInterface::class, true)) { 480 | throw new RuntimeException(sprintf('Unknown provider %s.', $this->fields['provider'])); 481 | } 482 | 483 | $params = [ 484 | 'clientId' => $this->fields['client_id'], 485 | 'clientSecret' => (new GLPIKey())->decrypt($this->fields['client_secret']), 486 | 'redirectUri' => self::getCallbackUrl(), 487 | 'scope' => self::getProviderScopes($this->fields['provider']), 488 | ]; 489 | 490 | if (!empty($CFG_GLPI['proxy_name'])) { 491 | // Connection using proxy 492 | $params['proxy'] = !empty($CFG_GLPI['proxy_user']) 493 | ? sprintf( 494 | '%s:%s@%s:%s', 495 | rawurlencode($CFG_GLPI['proxy_user']), 496 | rawurlencode((new GLPIKey())->decrypt($CFG_GLPI['proxy_passwd'])), 497 | $CFG_GLPI['proxy_name'], 498 | $CFG_GLPI['proxy_port'], 499 | ) 500 | : sprintf( 501 | '%s:%s', 502 | $CFG_GLPI['proxy_name'], 503 | $CFG_GLPI['proxy_port'], 504 | ); 505 | } 506 | 507 | // Specific parameters 508 | switch ($this->fields['provider']) { 509 | case Azure::class: 510 | $params['defaultEndPointVersion'] = '2.0'; 511 | if (!empty($this->fields['tenant_id'])) { 512 | $params['tenant'] = $this->fields['tenant_id']; 513 | } 514 | break; 515 | case Google::class: 516 | $params['accessType'] = 'offline'; 517 | break; 518 | } 519 | 520 | return new $this->fields['provider']($params); 521 | } 522 | 523 | /** 524 | * Get required scopes for given provider. 525 | * 526 | * @param string $provider Provider classname 527 | * 528 | * @return array 529 | */ 530 | private static function getProviderScopes(string $provider): array 531 | { 532 | $scopes = []; 533 | 534 | switch ($provider) { 535 | case Azure::class: 536 | $scopes = [ 537 | 'openid', 'email', // required to be able to fetch owner details 538 | 'offline_access', 539 | 'https://outlook.office.com/IMAP.AccessAsUser.All', 540 | ]; 541 | break; 542 | case Google::class: 543 | $scopes = [ 544 | 'https://mail.google.com/', 545 | ]; 546 | break; 547 | } 548 | 549 | return $scopes; 550 | } 551 | 552 | /** 553 | * Get documentation URLs. 554 | * Keys are providers classnames, values are URL. 555 | * 556 | * @return array 557 | */ 558 | private static function getProvidersDocumentationUrls(): array 559 | { 560 | return [ 561 | Azure::class => 'https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth', 562 | Google::class => 'https://developers.google.com/gmail/imap/xoauth2-protocol', 563 | ]; 564 | } 565 | 566 | /** 567 | * Get callback URL used during authorization process. 568 | * 569 | * @return string 570 | */ 571 | private static function getCallbackUrl(): string 572 | { 573 | // @phpstan-ignore-next-line : getWebDir() is deprecated, but mandatory for this case 574 | return @Plugin::getWebDir('oauthimap', true, true) . '/front/authorization.callback.php'; 575 | } 576 | 577 | public function cleanDBonPurge() 578 | { 579 | $this->deleteChildrenAndRelationsFromDb( 580 | [ 581 | PluginOauthimapAuthorization::class, 582 | ], 583 | ); 584 | } 585 | 586 | /** 587 | * Install all necessary data for this class. 588 | */ 589 | public static function install(Migration $migration) 590 | { 591 | /** @var DBmysql $DB */ 592 | global $DB; 593 | 594 | $default_charset = DBConnection::getDefaultCharset(); 595 | $default_collation = DBConnection::getDefaultCollation(); 596 | $default_key_sign = DBConnection::getDefaultPrimaryKeySignOption(); 597 | 598 | $table = self::getTable(); 599 | 600 | if (!$DB->tableExists($table)) { 601 | $migration->displayMessage("Installing $table"); 602 | 603 | $query = <<doQuery($query); 623 | } 624 | 625 | // Add display preferences 626 | $migration->updateDisplayPrefs( 627 | [ 628 | 'PluginOauthimapApplication' => [1, 5, 6, 7, 121, 19], 629 | ], 630 | ); 631 | } 632 | 633 | /** 634 | * Uninstall previously installed data for this class. 635 | */ 636 | public static function uninstall(Migration $migration) 637 | { 638 | $table = self::getTable(); 639 | $migration->displayMessage("Uninstalling $table"); 640 | $migration->dropTable($table); 641 | } 642 | } 643 | -------------------------------------------------------------------------------- /inc/authorization.class.php: -------------------------------------------------------------------------------- 1 | . 24 | * ------------------------------------------------------------------------- 25 | * @copyright Copyright (C) 2020-2025 by Teclib' 26 | * @license GPLv3+ https://www.gnu.org/licenses/gpl-3.0.fr.html 27 | * @link https://services.glpi-network.com 28 | * ------------------------------------------------------------------------- 29 | */ 30 | use GlpiPlugin\Oauthimap\Imap\ImapOauthProtocol; 31 | use GlpiPlugin\Oauthimap\Imap\ImapOauthStorage; 32 | use GlpiPlugin\Oauthimap\MailCollectorFeature; 33 | use GlpiPlugin\Oauthimap\Oauth\OwnerDetails; 34 | use League\OAuth2\Client\Token\AccessToken; 35 | 36 | use function Safe\json_decode; 37 | use function Safe\json_encode; 38 | 39 | class PluginOauthimapAuthorization extends CommonDBChild 40 | { 41 | // From CommonGlpi 42 | protected $displaylist = false; 43 | 44 | // From CommonDBTM 45 | public $dohistory = true; 46 | 47 | // From CommonDBChild 48 | public static $itemtype = 'PluginOauthimapApplication'; 49 | public static $items_id = 'plugin_oauthimap_applications_id'; 50 | 51 | /** 52 | * Authorization owner details. 53 | * @var OwnerDetails 54 | */ 55 | private $owner_details; 56 | 57 | public static function getTypeName($nb = 0) 58 | { 59 | return _sn('Oauth authorization', 'Oauth authorizations', $nb, 'oauthimap'); 60 | } 61 | 62 | public static function getIcon() 63 | { 64 | return 'ti ti-key'; 65 | } 66 | 67 | public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) 68 | { 69 | $count = 0; 70 | if ($_SESSION['glpishow_count_on_tabs'] && $item instanceof PluginOauthimapApplication) { 71 | $count = countElementsInTable( 72 | $this->getTable(), 73 | [ 74 | PluginOauthimapApplication::getForeignKeyField() => $item->getID(), 75 | ], 76 | ); 77 | } 78 | 79 | return self::createTabEntry(self::getTypeName(1), $count); 80 | } 81 | 82 | public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) 83 | { 84 | if (!($item instanceof PluginOauthimapApplication)) { 85 | return false; 86 | } 87 | 88 | /** @var \DBmysql $DB */ 89 | /** @var array $CFG_GLPI */ 90 | global $DB, $CFG_GLPI; 91 | 92 | $iterator = $DB->request( 93 | [ 94 | 'FROM' => self::getTable(), 95 | 'WHERE' => [ 96 | PluginOauthimapApplication::getForeignKeyField() => $item->getID(), 97 | ], 98 | ], 99 | ); 100 | 101 | $item->showFormHeader([ 102 | 'formtitle' => $item->fields['name'], 103 | 'target' => $CFG_GLPI['root_doc'] . '/plugins/oauthimap/front/authorization.form.php', 104 | ]); 105 | 106 | echo '
'; 107 | echo '
'; 108 | echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]); 109 | echo Html::hidden('id', ['value' => $item->getID()]); 110 | echo ''; 113 | echo '
'; 114 | echo '
'; 115 | 116 | echo ''; // #mainformtable 117 | echo ''; // [name=asset_form] 118 | 119 | echo ''; 120 | if ($iterator->count() === 0) { 121 | echo ''; 122 | } else { 123 | echo ''; 124 | echo ''; 125 | echo ''; 126 | echo ''; 127 | echo ''; 128 | echo ''; 129 | 130 | echo ''; 131 | foreach ($iterator as $row) { 132 | echo ''; 133 | 134 | echo ''; 135 | 136 | echo ''; 163 | 164 | echo ''; 165 | } 166 | echo ''; 167 | } 168 | echo '
' . __s('No authorizations.', 'oauthimap') . '
' . __s('Email', 'oauthimap') . '
' . $row['email'] . ''; 137 | $modal_id = 'plugin_oauthimap_authorization_diagnostic_' . mt_rand(); 138 | Ajax::createIframeModalWindow( 139 | $modal_id, 140 | self::getFormURLWithID($row['id']) . '&diagnose', 141 | [ 142 | 'title' => __s('Connection diagnostic', 'oauthimap'), 143 | 'height' => 650, 144 | ], 145 | ); 146 | echo ''; 147 | echo ' ' . __s('Diagnose', 'oauthimap'); 148 | echo ''; 149 | echo ' '; 150 | echo ''; 151 | echo ' ' . __s('Update', 'oauthimap'); 152 | echo ''; 153 | echo ' '; 154 | echo '
'; 155 | echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]); 156 | echo Html::hidden('id', ['value' => $row['id']]); 157 | echo ''; 161 | echo '
'; 162 | echo '
'; 169 | 170 | return true; 171 | } 172 | 173 | public function showForm($id, $options = []) 174 | { 175 | $options['colspan'] = 1; 176 | 177 | $this->initForm($id, $options); 178 | $this->showFormHeader($options); 179 | 180 | echo ''; 181 | echo ''; 182 | echo __s('Email', 'oauthimap'); 183 | echo ' '; 184 | echo Html::showToolTip( 185 | __s('This email address corresponds to the "user" field of the SASL XOAUTH2 authentication query.'), 186 | ['display' => false], 187 | ); 188 | echo ''; 189 | echo ''; 190 | echo Html::input( 191 | 'email', 192 | [ 193 | 'value' => $this->fields['email'], 194 | 'style' => 'width:90%', 195 | ], 196 | ); 197 | echo ''; 198 | echo ''; 199 | 200 | $this->showFormButtons($options + ['candel' => false]); 201 | 202 | return true; 203 | } 204 | 205 | /** 206 | * Displays diagnostic form. 207 | * 208 | * @param array $params 209 | * 210 | * @return void 211 | */ 212 | public function showDiagnosticForm(array $params) 213 | { 214 | $application = new PluginOauthimapApplication(); 215 | if ( 216 | !$application->getFromDB($this->fields[PluginOauthimapApplication::getForeignKeyField()]) 217 | || ($provider = $application->getProvider()) === null 218 | ) { 219 | return; 220 | } 221 | 222 | $user = $params['user'] ?? $this->fields['email']; 223 | $host = $params['host'] ?? $provider->getDefaultHost(); 224 | $port = (int) ($params['port'] ?? $provider->getDefaultPort()); 225 | $ssl = $params['ssl'] ?? $provider->getDefaultSslFlag(); 226 | $timeout = (int) ($params['timeout'] ?? 2); // 2 seconds timeout by default 227 | 228 | echo '
'; 229 | 230 | echo ''; 231 | echo ''; 232 | echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]); 233 | 234 | echo ''; 235 | 236 | echo ''; 237 | echo ''; 240 | echo ''; 249 | echo ''; 250 | 251 | echo ''; 252 | echo ''; 255 | echo ''; 263 | echo ''; 266 | echo ''; 277 | echo ''; 278 | 279 | echo ''; 280 | echo ''; 283 | echo ''; 297 | echo ''; 300 | echo ''; 312 | echo ''; 313 | 314 | echo ''; 315 | echo ''; 324 | echo ''; 325 | 326 | echo ''; 327 | echo ''; 353 | echo ''; 354 | 355 | echo '
'; 238 | echo __s('Email', 'oauthimap'); 239 | echo ''; 241 | echo Html::input( 242 | 'email', 243 | [ 244 | 'disabled' => 'disabled', 245 | 'value' => $user, 246 | ], 247 | ); 248 | echo '
'; 253 | echo __s('Server host', 'oauthimap'); 254 | echo ''; 256 | echo Html::input( 257 | 'host', 258 | [ 259 | 'value' => $host, 260 | ], 261 | ); 262 | echo ''; 264 | echo __s('Server port', 'oauthimap'); 265 | echo ''; 267 | echo Html::input( 268 | 'port', 269 | [ 270 | 'type' => 'integer', 271 | 'min' => 1, 272 | 'value' => $port, 273 | 'size' => 5, 274 | ], 275 | ); 276 | echo '
'; 281 | echo __s('Security level', 'oauthimap'); 282 | echo ''; 284 | echo Html::select( 285 | 'ssl', 286 | [ 287 | '' => '', 288 | 'SSL' => __s('SSL', 'oauthimap'), 289 | 'TLS' => __s('SSL + TLS', 'oauthimap'), 290 | ], 291 | [ 292 | 'selected' => $ssl, 293 | 'class' => 'form-select', 294 | ], 295 | ); 296 | echo ''; 298 | echo __s('Timeout', 'oauthimap'); 299 | echo ''; 301 | echo Html::input( 302 | 'timeout', 303 | [ 304 | 'type' => 'integer', 305 | 'min' => 1, 306 | 'max' => 30, 307 | 'value' => $timeout, 308 | 'size' => 5, 309 | ], 310 | ); 311 | echo '
'; 316 | echo Html::submit( 317 | __s('Refresh connection diagnostic', 'oauthimap'), 318 | [ 319 | 'name' => 'diagnose', 320 | 'class' => 'btn btn-secondary', 321 | ], 322 | ); 323 | echo '
'; 328 | echo '
'; 329 | echo ''; 330 | echo __s('Diagnostic log contains sensitive information, such as the access token.', 'oauthimap'); 331 | echo '
'; 332 | $protocol = new ImapOauthProtocol($application->fields['id']); 333 | $protocol->enableDiagnostic(); 334 | $protocol->setTimeout($timeout); 335 | $error = null; 336 | try { 337 | $protocol->connect($host, $port, $ssl); 338 | if ($protocol->login($user, '')) { 339 | new ImapOauthStorage($protocol); // Will automatically send 'select INBOX'. 340 | } 341 | } catch (Throwable $e) { 342 | $error = $e; 343 | } 344 | echo '
'; 345 | echo htmlspecialchars($protocol->getDiagnosticLog()); 346 | echo ''; 347 | if ($error !== null) { 348 | echo '
'; 349 | echo sprintf(__s('Unexpected error: %s', 'oauthimap'), $error->getMessage()); 350 | echo '
'; 351 | } 352 | echo '
'; 356 | echo '
'; 357 | } 358 | 359 | public function prepareInputForAdd($input) 360 | { 361 | if (!($input = $this->prepareInput($input))) { 362 | return false; 363 | } 364 | 365 | return parent::prepareInputForAdd($input); 366 | } 367 | 368 | public function prepareInputForUpdate($input) 369 | { 370 | // Unset encrypted fields input if corresponding to current value 371 | // (encryption produces a different value each time, so GLPI will consider them as updated on each form submit) 372 | foreach (['code', 'token', 'refresh_token'] as $field_name) { 373 | if ( 374 | array_key_exists($field_name, $input) 375 | && !empty($input[$field_name]) && $input[$field_name] !== 'NULL' 376 | && $input[$field_name] === (new GLPIKey())->decrypt($this->fields[$field_name]) 377 | ) { 378 | unset($input[$field_name]); 379 | } 380 | } 381 | 382 | if (!($input = $this->prepareInput($input))) { 383 | return false; 384 | } 385 | 386 | return parent::prepareInputForUpdate($input); 387 | } 388 | 389 | /** 390 | * Encrypt values of secured fields. 391 | * 392 | * @param array $input 393 | * 394 | * @return array 395 | */ 396 | private function prepareInput($input) 397 | { 398 | foreach (['code', 'token', 'refresh_token'] as $field_name) { 399 | if ( 400 | array_key_exists($field_name, $input) 401 | && !empty($input[$field_name]) && $input[$field_name] !== 'NULL' 402 | ) { 403 | $input[$field_name] = (new GLPIKey())->encrypt($input[$field_name]); 404 | } 405 | } 406 | 407 | return $input; 408 | } 409 | 410 | /** 411 | * Create an authorization based on authorizarion code. 412 | * 413 | * @param int $application_id 414 | * @param string $code 415 | * 416 | * @return bool 417 | */ 418 | public function createFromCode(int $application_id, string $code): bool 419 | { 420 | $application = new PluginOauthimapApplication(); 421 | if (!$application->getFromDB($application_id)) { 422 | return false; 423 | } 424 | 425 | $provider = $application->getProvider(); 426 | 427 | // Get token 428 | try { 429 | $token = $provider->getAccessToken('authorization_code', ['code' => $code]); 430 | } catch (Throwable $e) { 431 | trigger_error( 432 | sprintf('Error during authorization code fetching: %s', $e->getMessage()), 433 | E_USER_WARNING, 434 | ); 435 | 436 | return false; 437 | } 438 | 439 | // Get user details 440 | /** @var AccessToken $token */ 441 | $this->owner_details = $provider->getOwnerDetails($token); 442 | $email = $this->owner_details->email; 443 | if ($email === null) { 444 | trigger_error('Unable to get user email', E_USER_WARNING); 445 | 446 | return false; 447 | } 448 | 449 | // Save informations 450 | $input = [ 451 | $application->getForeignKeyField() => $application_id, 452 | 'code' => $code, 453 | 'token' => json_encode($token->jsonSerialize()), 454 | 'refresh_token' => $token->getRefreshToken(), 455 | 'email' => $email, 456 | ]; 457 | 458 | $exists = $this->getFromDBByCrit( 459 | [ 460 | $application->getForeignKeyField() => $application_id, 461 | 'email' => $email, 462 | ], 463 | ); 464 | if ($exists) { 465 | return $this->update(['id' => $this->fields['id']] + $input); 466 | } else { 467 | return $this->add($input); 468 | } 469 | } 470 | 471 | /** 472 | * Get a fresh access token related to given email using given application. 473 | * 474 | * @param int $application_id 475 | * @param string $email 476 | * 477 | * @return string|null 478 | */ 479 | public static function getAccessTokenForApplicationAndEmail($application_id, $email): ?string 480 | { 481 | $application = new PluginOauthimapApplication(); 482 | if (!$application->getFromDB($application_id)) { 483 | return null; 484 | } 485 | 486 | $self = new self(); 487 | if (!$self->getFromDBByCrit([$application->getForeignKeyField() => $application_id, 'email' => $email])) { 488 | return null; 489 | } 490 | 491 | try { 492 | $token = new AccessToken(json_decode((new GLPIKey())->decrypt($self->fields['token']), true)); 493 | } catch (Throwable $e) { 494 | return null; // Field value may be corrupted 495 | } 496 | 497 | if ($token->hasExpired()) { 498 | // Token has expired, refresh it 499 | $refresh_token = (new GLPIKey())->decrypt($self->fields['refresh_token']); 500 | 501 | $provider = $application->getProvider(); 502 | $token = $provider->getAccessToken( 503 | 'refresh_token', 504 | [ 505 | 'refresh_token' => $refresh_token, 506 | ], 507 | ); 508 | 509 | $input = [ 510 | 'id' => $self->fields['id'], 511 | 'token' => json_encode($token->jsonSerialize()), 512 | ]; 513 | if (!empty($token->getRefreshToken()) && $token->getRefreshToken() !== $refresh_token) { 514 | // Update refresh token if a new one has been received in response. 515 | $input['refresh_token'] = $token->getRefreshToken(); 516 | } 517 | 518 | $self->update($input); 519 | } 520 | 521 | return $token->getToken(); 522 | } 523 | 524 | /** 525 | * Get existing access token. 526 | * 527 | * @return AccessToken|null 528 | */ 529 | public function getAccessToken(): ?AccessToken 530 | { 531 | try { 532 | $token = new AccessToken(json_decode((new GLPIKey())->decrypt($this->fields['token']), true)); 533 | } catch (Throwable $e) { 534 | return null; // Field value may be corrupted 535 | } 536 | 537 | return $token; 538 | } 539 | 540 | /** 541 | * Returns owner details fetched when creating authorization. 542 | * 543 | * @return OwnerDetails|null 544 | */ 545 | public function getOwnerDetails(): ?OwnerDetails 546 | { 547 | return $this->owner_details; 548 | } 549 | 550 | // phpcs:ignore PSR1.Methods.CamelCapsMethodName 551 | public function post_updateItem($history = 1) 552 | { 553 | MailCollectorFeature::postUpdateAuthorization($this); 554 | parent::post_updateItem($history); 555 | } 556 | 557 | // phpcs:ignore PSR1.Methods.CamelCapsMethodName 558 | public function post_purgeItem() 559 | { 560 | MailCollectorFeature::postPurgeAuthorization($this); 561 | } 562 | 563 | /** 564 | * Install all necessary data for this class. 565 | */ 566 | public static function install(Migration $migration) 567 | { 568 | /** @var DBmysql $DB */ 569 | global $DB; 570 | 571 | $default_charset = DBConnection::getDefaultCharset(); 572 | $default_collation = DBConnection::getDefaultCollation(); 573 | $default_key_sign = DBConnection::getDefaultPrimaryKeySignOption(); 574 | 575 | $table = self::getTable(); 576 | $application_fkey = PluginOauthimapApplication::getForeignKeyField(); 577 | 578 | if (!$DB->tableExists($table)) { 579 | $migration->displayMessage("Installing $table"); 580 | $query = <<doQuery($query); 598 | } elseif (!$DB->fieldExists($table, 'refresh_token')) { 599 | // V1.3.1: add new refresh_token field 600 | $migration->addField( 601 | $table, 602 | 'refresh_token', 603 | 'text', 604 | [ 605 | 'after' => 'token', 606 | 'nodefault' => true, 607 | ], 608 | ); 609 | $iterator = $DB->request(['FROM' => $table]); 610 | foreach ($iterator as $row) { 611 | $token_fields = json_decode((new GLPIKey())->decrypt($row['token']), true); 612 | if (isset($token_fields['refresh_token'])) { 613 | $migration->addPostQuery( 614 | $DB->buildUpdate( 615 | $table, 616 | [ 617 | 'refresh_token' => (new GLPIKey())->encrypt($token_fields['refresh_token']), 618 | ], 619 | [ 620 | 'id' => $row['id'], 621 | ], 622 | ), 623 | ); 624 | } 625 | } 626 | } 627 | } 628 | 629 | /** 630 | * Uninstall previously installed data for this class. 631 | */ 632 | public static function uninstall(Migration $migration) 633 | { 634 | $table = self::getTable(); 635 | $migration->displayMessage("Uninstalling $table"); 636 | $migration->dropTable($table); 637 | } 638 | } 639 | --------------------------------------------------------------------------------