From 0c8bd90320aeb56dd79a3a91d40f9b64a96394df Mon Sep 17 00:00:00 2001 From: William Jacoby Date: Mon, 26 Oct 2026 10:28:13 -0500 Subject: [PATCH 1/2] Add Admin Activation Details for phpBB 3.3 and 4.0 --- .gitattributes | 9 + .github/workflows/tests.yml | 42 ++++ .gitignore | 6 + README.md | 59 ++++++ SECURITY.md | 7 + composer.json | 26 +++ config/services.yml | 8 + docs/testing.md | 83 ++++++++ event/listener.php | 83 ++++++++ ext.php | 30 +++ language/en/email/admin_activate.txt | 39 ++++ language/en/install.php | 20 ++ license.txt | 280 +++++++++++++++++++++++++++ phpunit.xml.dist | 8 + samples/admin-activation-ipv4.txt | 26 +++ samples/admin-activation-ipv6.txt | 26 +++ tests/functional/.gitkeep | 0 tests/notification_test.php | 95 +++++++++ tests/runtime/board-33.yml.example | 12 ++ tests/runtime/board-40.yml.example | 12 ++ tests/runtime/browser.js | 91 +++++++++ tests/runtime/fixture.php | 104 ++++++++++ tests/runtime/read_mail.py | 32 +++ tests/runtime/smtp_capture.py | 53 +++++ 24 files changed, 1151 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 composer.json create mode 100644 config/services.yml create mode 100644 docs/testing.md create mode 100644 event/listener.php create mode 100644 ext.php create mode 100644 language/en/email/admin_activate.txt create mode 100644 language/en/install.php create mode 100644 license.txt create mode 100644 phpunit.xml.dist create mode 100644 samples/admin-activation-ipv4.txt create mode 100644 samples/admin-activation-ipv6.txt create mode 100644 tests/functional/.gitkeep create mode 100644 tests/notification_test.php create mode 100644 tests/runtime/board-33.yml.example create mode 100644 tests/runtime/board-40.yml.example create mode 100644 tests/runtime/browser.js create mode 100644 tests/runtime/fixture.php create mode 100644 tests/runtime/read_mail.py create mode 100644 tests/runtime/smtp_capture.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f67e056 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +* text=auto eol=lf +*.png binary +*.jpg binary +*.zip binary +/.github export-ignore +/.gitignore export-ignore +/.gitattributes export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..7999ad0 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,42 @@ +name: Tests +on: + push: + pull_request: +permissions: + contents: read +jobs: + tests: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + phpbb: ['3.3.x', 'master'] + php: ['8.3', '8.4'] + name: phpBB ${{ matrix.phpbb }} / PHP ${{ matrix.php }} + steps: + - name: Checkout phpBB + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: phpbb/phpbb + ref: ${{ matrix.phpbb }} + - name: Checkout extension + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: phpBB/ext/phpbbmodders/adminactivationdetails + - name: Set up PHP + uses: shivammathur/setup-php@d52fc211a0436f8839a587b3b5b8efddab4744ea # v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, gd, intl, mbstring, mysqli, pdo_sqlite, sqlite3, zip + coverage: none + - name: Install phpBB test dependencies + run: .github/setup-phpbb.sh none '${{ matrix.php }}' 1 + - name: Run regression tests + run: phpBB/vendor/bin/phpunit --bootstrap tests/bootstrap.php --configuration phpBB/ext/phpbbmodders/adminactivationdetails/phpunit.xml.dist + - name: Check extension coding standards + run: phpBB/vendor/bin/phpcs -s --standard=build/code_sniffer/ruleset-php-extensions.xml --extensions=php --ignore='*/tests/*' phpBB/ext/phpbbmodders/adminactivationdetails + epv: + uses: phpbbmodders/.github/.github/workflows/epv.yml@v1 + with: + EXTNAME: phpbbmodders/adminactivationdetails + PHPBB_BRANCHES: '["3.3.x", "master"]' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..246c41d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor/ +/composer.lock +/.phpunit.result.cache +/tests/runtime/board-33.yml +/tests/runtime/board-40.yml +/tests/runtime/__pycache__/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..feee101 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# Admin Activation Details + +Adds the registering account's email address, stored registration IP and +registration date to phpBB's administrator activation emails. Valid IPv4 +and IPv6 addresses include WHOIS/RDAP and Stop Forum Spam lookup links. + +Requires phpBB 3.3.19 or phpBB 4.0 and PHP 8.3.36 or later. phpBB 4.0 is +still a development branch; see [testing](docs/testing.md) for tested revisions. + +## Installation + +Copy this extension into `ext/phpbbmodders/adminactivationdetails/`, then +open **ACP → Customise → Manage extensions** and enable **Admin Activation Details**. +Set **Account activation** to **By admin** in User registration settings. +Administrators must enable email for the existing **User requiring activation** +notification in their UCP notification settings. + +There are no additional settings or database migrations. Disable the extension +to restore phpBB's original activation notification and email template. + +## Email behavior + +- Covers administrator activation notifications from registration and resends. +- Uses the account's stored registration IP, not the current visitor's address. +- Keeps phpBB's existing recipients, notification preferences, profile link, + activation link and mail queue behavior. +- Uses phpBB's current email-rendering user's date format and timezone, shown + explicitly beside the date. A queued email keeps that rendered date. +- Missing or invalid IPs show **Not available** and omit lookup links. +- Sends no passwords and makes no requests to lookup services. Clicking a lookup + link submits the IP to that service. A registration IP may identify a proxy or VPN. + +The extension supplies a plain-text English email template; phpBB's normal +language fallback applies. Translations can provide +`language//email/admin_activate.txt` inside this extension. Existing +core-template customizations and HTML-email modifications do not automatically +carry over to the extension's template. + +## Development + +See [implementation and testing](docs/testing.md). The extension listens to +`core.modify_notification_template` and changes only the `admin_activate` email. +Another extension replacing that template may conflict. +No phpBB core files need editing. If using phpBB's CLI to enable or disable +the extension, run `cache:purge` afterward to rebuild its cached services. + +The feature was requested in this [phpBB discussion](https://www.phpbb.com/community/viewtopic.php?t=2667817). + +Synthetic [IPv4](samples/admin-activation-ipv4.txt) and +[IPv6](samples/admin-activation-ipv6.txt) email examples are included. Their example activation keys do not work. + +## Acknowledgments + +- Implementation, testing and documentation assisted by [Codex](https://openai.com/codex/). + +## License + +This extension is licensed under the **GNU General Public License v2.0**. +See [license.txt](license.txt). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d66abfc --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security policy + +Report suspected vulnerabilities privately to security@williamjacoby.cc. +Include the organization name **phpbbmodders**, repository name +**adminactivationdetails**, affected versions, and reproduction steps. + +Do not post activation keys or account details in a public issue. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8af6c35 --- /dev/null +++ b/composer.json @@ -0,0 +1,26 @@ +{ + "name": "phpbbmodders/adminactivationdetails", + "type": "phpbb-extension", + "description": "Include registration IP, account details and IP lookup links in administrator activation emails.", + "homepage": "https://www.phpbbmodders.com/", + "version": "1.0.0-dev", + "license": "GPL-2.0-only", + "authors": [ + { + "name": "phpBB Modders", + "homepage": "https://www.phpbbmodders.com/", + "role": "Extension Developer" + } + ], + "require": { + "php": ">=8.3.36", + "phpbb/phpbb": ">=3.3.19,<4.1.0@dev", + "composer/installers": "^1.0 || ^2.0" + }, + "extra": { + "display-name": "Admin Activation Details", + "soft-require": { + "phpbb/phpbb": ">=3.3.19,<4.1.0@dev" + } + } +} diff --git a/config/services.yml b/config/services.yml new file mode 100644 index 0000000..fadbfc0 --- /dev/null +++ b/config/services.yml @@ -0,0 +1,8 @@ +services: + phpbbmodders.adminactivationdetails.listener: + class: phpbbmodders\adminactivationdetails\event\listener + arguments: + - '@user_loader' + - '@user' + tags: + - { name: event.listener } diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..8919fbd --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,83 @@ +# Implementation and testing + +## Implementation + +The extension listens to `core.modify_notification_template` on phpBB 3.3 and +4.0. Only emails using `admin_activate.txt` are affected. Core's activation URL +identifies the account; the extension loads its stored registration details. +Missing accounts cannot expose Anonymous account details. + +This event covers notification-manager mail from new registrations and direct +mail from activation resends. Core still controls recipients, preferences, +activation keys, and queuing. The extension does not replace a core service. + +phpBB's resend path sends directly to administrators. It does not use the +registration notification-manager subscription preferences; the extension +preserves that existing behavior rather than changing who receives mail. + +## Results — 10/26/2026 + +Both live boards used PHP 8.4.27 with SQLite and a localhost-only SMTP capture. + +| Board | Source revision | Unit tests | Browser and delivery checks | +| --- | --- | --- | --- | +| phpBB 3.3.19 (3.3.x source) | `1b1ffdf89c033f584a1daf0c6995c13e59dcb81b` | 8 tests, 42 assertions | Passed | +| phpBB 4.0.0-a3-dev | `2a2dad29a4d47d26d8f01f4bf4851c98f809c481` | 8 tests, 42 assertions | Passed | + +Source inspection also compared the current 3.3.x (`d8e558ca55`) and master +(`a4cea6406f`) revisions: the exercised UCP, notification and messenger paths +are unchanged from the runtime snapshots above. The phpBB 4.0 claim is for the +measured development snapshot, not a future final release. + +Verified on each board: + +- Real registration through Playwright, with separate applicant and admin mail. +- Correct account email, stored registration IP, registration date/timezone, + and existing profile/activation URLs; no password or unexpanded variables. +- IPv6 output and encoded lookup URLs; invalid addresses omit lookup links. +- Real activation resend from localhost retains the stored synthetic IPv6 IP. +- Mail remains queued until processing and retains its enriched contents. +- Registration notification email opt-out is respected. +- Disable plus cache purge restores the original email; re-enable plus cache + purge restores the enriched email. +- Administrator login and the email's activation key activate the correct account. + +The installable package passes EPV with **0 fatal, 0 error, 0 warning, 0 notice**. +The production PHP passes both branches' phpBB extension coding standards. +Composer metadata validates; its explicit extension version produces Composer's +usual Packagist advisory. GitHub CI is configured but has not been run remotely. + +## Repeat unit tests + +Use the committed `phpbb-release-tools/phpbb-test-board/run-unit-tests.sh` +runner against this checkout, once with `-b 3.3.x` and once with `-b master`. +It uses phpBB's own test bootstrap and supported PHPUnit dependencies. + +CI repeats the regression suite and code standards on both phpBB branches, +using PHP 8.3 and 8.4. The organization EPV workflow validates both branches. +The local runtime tests above used PHP 8.4; PHP 8.3 CI has not run yet. + +## Repeat browser/delivery tests + +Use `seed-forum` with a copy of the appropriate `tests/runtime/board-*.yml.example`. +Change the source and disposable board paths, choose free ports, and install +with `reset-board.sh --config YOUR_CONFIG --reset`. These fixtures are strictly +for disposable local boards. Mark the new board root by creating an empty +`.adminactivationdetails-test-board` file; the PHP fixture refuses unmarked boards. + +Start `python3 tests/runtime/smtp_capture.py /path/to/artifacts/mail` in a +separate terminal. It listens only on `127.0.0.1:18250` and never relays mail. +Configure the board with `sudo -u www-data php tests/runtime/fixture.php BOARD_ROOT setup`. +Keep test-board CLI cache operations under the web-server user so Twig cache +permissions remain consistent. Run `cache:purge` after CLI extension state changes. + +Run `tests/runtime/browser.js` through the Playwright skill runner, or with a +locally installed Playwright and Chromium, setting `TEST_URL`, `TEST_BOARD_ROOT` +and `TEST_ARTIFACT_DIR`. The script uses the local SMTP capture and invokes the +PHP fixture through noninteractive `sudo -u www-data`. + +The browser test creates only synthetic `@example.org` accounts. It tests +registration, resend, IPv6, invalid IPs, queueing and opt-out; results are written +as JSON to the artifact directory. The test restores the admin subscription. +After testing, use the fixture's `mail-off` operation and stop the capture server. +Restore or remove only the disposable boards and their dedicated nginx sites. diff --git a/event/listener.php b/event/listener.php new file mode 100644 index 0000000..a376e7f --- /dev/null +++ b/event/listener.php @@ -0,0 +1,83 @@ +user_loader = $user_loader; + $this->user = $user; + } + + /** + * @return array Events shared by both supported phpBB branches + */ + public static function getSubscribedEvents() + { + return array('core.modify_notification_template' => 'activation_email'); + } + + /** + * Enrich only admin_activate emails, including direct activation resends. + * Read the account ID from core's activation URL, never from request data. + * + * @param \phpbb\event\data $event Email template before rendering + * @return void + */ + public function activation_email($event) + { + $template = $event['template']; + if (basename($template->get_source_file_for_handle('body')) !== 'admin_activate.txt') + { + return; + } + + $query = parse_url(html_entity_decode($template->retrieve_var('U_ACTIVATE'), ENT_COMPAT, 'UTF-8'), PHP_URL_QUERY); + parse_str(is_string($query) ? $query : '', $params); + $user_id = filter_var($params['u'] ?? null, FILTER_VALIDATE_INT, array('options' => array('min_range' => 2))); + if ($user_id === false) + { + return; + } + + $row = $this->user_loader->get_user($user_id, true); + // user_loader falls back to Anonymous for a missing account. + if (!$row || (int) $row['user_id'] !== $user_id) + { + $row = array(); + } + + $ip = $row['user_ip'] ?? ''; + $ip = filter_var($ip, FILTER_VALIDATE_IP) !== false ? $ip : ''; + $encoded_ip = rawurlencode($ip); + $regdate = (int) ($row['user_regdate'] ?? 0); + + $template->assign_vars(array( + 'USER_IP' => $ip, + 'USER_EMAIL' => html_entity_decode($row['user_email'] ?? '', ENT_COMPAT, 'UTF-8'), + 'REG_DATE' => $regdate > 0 ? $this->user->format_date($regdate, '', true) : '', + 'REG_TIMEZONE' => $this->user->timezone ? $this->user->timezone->getName() : 'UTC', + 'U_WHOIS' => $ip !== '' ? 'https://search.arin.net/rdap/?query=' . $encoded_ip : '', + 'U_STOPFORUMSPAM' => $ip !== '' ? 'https://www.stopforumspam.com/search/?q=' . $encoded_ip : '', + )); + $template->set_filenames(array('body' => '@phpbbmodders_adminactivationdetails/admin_activate.txt')); + } +} diff --git a/ext.php b/ext.php new file mode 100644 index 0000000..a4e6c74 --- /dev/null +++ b/ext.php @@ -0,0 +1,30 @@ += 80336 && + phpbb_version_compare(PHPBB_VERSION, '3.3.19', '>=') && + phpbb_version_compare(PHPBB_VERSION, '4.1.0-dev', '<')) + { + return true; + } + + $language = $this->container->get('language'); + $language->add_lang('install', 'phpbbmodders/adminactivationdetails'); + + return $language->lang('ADMINACTIVATIONDETAILS_NOT_ENABLEABLE'); + } +} diff --git a/language/en/email/admin_activate.txt b/language/en/email/admin_activate.txt new file mode 100644 index 0000000..29cc5ee --- /dev/null +++ b/language/en/email/admin_activate.txt @@ -0,0 +1,39 @@ +Subject: Activate user account + +Hello, + +The account owned by "{USERNAME}" has been deactivated or newly created. Check the account details before deciding whether to activate it. + +Username: {USERNAME} + +Email address: {USER_EMAIL} + +Email address: Not available + + + +Registration IP: {USER_IP} + +Registration IP: Not available + + + +Registration date: {REG_DATE} ({REG_TIMEZONE}) + +Registration date: Not available + + + +WHOIS / RDAP lookup: +{U_WHOIS} + +Stop Forum Spam lookup: +{U_STOPFORUMSPAM} + +View the user's profile: +{U_USER_DETAILS} + +Activate the account: +{U_ACTIVATE} + +{EMAIL_SIG} diff --git a/language/en/install.php b/language/en/install.php new file mode 100644 index 0000000..53addaf --- /dev/null +++ b/language/en/install.php @@ -0,0 +1,20 @@ + 'Admin Activation Details requires phpBB 3.3.19 or phpBB 4.0, and PHP 8.3.36 or later.', +)); diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..d8cf7d4 --- /dev/null +++ b/license.txt @@ -0,0 +1,280 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d9a4d80 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,8 @@ + + + + + ./tests + + + diff --git a/samples/admin-activation-ipv4.txt b/samples/admin-activation-ipv4.txt new file mode 100644 index 0000000..7282556 --- /dev/null +++ b/samples/admin-activation-ipv4.txt @@ -0,0 +1,26 @@ +Subject: Activate user account + +Hello, + +The account owned by "ExampleApplicant" has been deactivated or newly created. Check the account details before deciding whether to activate it. + +Username: ExampleApplicant +Email address: ExampleApplicant@example.org + +Registration IP: 192.0.2.41 + +Registration date: Mon Oct 26, 2026 3:17 pm (UTC) + +WHOIS / RDAP lookup: +https://search.arin.net/rdap/?query=192.0.2.41 + +Stop Forum Spam lookup: +https://www.stopforumspam.com/search/?q=192.0.2.41 +View the user's profile: +https://forum.example.org/memberlist.php?mode=viewprofile&u=84 + +Activate the account: +https://forum.example.org/ucp.php?mode=activate&u=84&k=EXAMPLE_ACTIVATION_KEY + +-- +Thanks, The Management diff --git a/samples/admin-activation-ipv6.txt b/samples/admin-activation-ipv6.txt new file mode 100644 index 0000000..b2f1a88 --- /dev/null +++ b/samples/admin-activation-ipv6.txt @@ -0,0 +1,26 @@ +Subject: Activate user account + +Hello, + +The account owned by "ExampleApplicant" has been deactivated or newly created. Check the account details before deciding whether to activate it. + +Username: ExampleApplicant +Email address: ExampleApplicant@example.org + +Registration IP: 2001:db8::12 + +Registration date: Mon Oct 26, 2026 3:17 pm (UTC) + +WHOIS / RDAP lookup: +https://search.arin.net/rdap/?query=2001%3Adb8%3A%3A12 + +Stop Forum Spam lookup: +https://www.stopforumspam.com/search/?q=2001%3Adb8%3A%3A12 +View the user's profile: +https://forum.example.org/memberlist.php?mode=viewprofile&u=84 + +Activate the account: +https://forum.example.org/ucp.php?mode=activate&u=84&k=EXAMPLE_ACTIVATION_KEY + +-- +Thanks, The Management diff --git a/tests/functional/.gitkeep b/tests/functional/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/notification_test.php b/tests/notification_test.php new file mode 100644 index 0000000..bd7c52b --- /dev/null +++ b/tests/notification_test.php @@ -0,0 +1,95 @@ +getMockBuilder(\phpbb\user::class)->disableOriginalConstructor() + ->onlyMethods(array('format_date'))->getMock(); + $user->timezone = new \DateTimeZone('America/Chicago'); + $user->ip = '198.51.100.200'; + $user->method('format_date')->willReturn('10/26/2026 9:00 AM'); + $loader = $this->getMockBuilder(\phpbb\user_loader::class)->disableOriginalConstructor() + ->onlyMethods(array('get_user'))->getMock(); + $should_load = $filename === 'admin_activate.txt' && strpos($url, 'u=42') !== false; + $loader->expects($should_load ? $this->once() : $this->never())->method('get_user') + ->with(42, true)->willReturn($row); + $template = $this->createMock(\phpbb\template\template::class); + $template->method('get_source_file_for_handle')->with('body')->willReturn('/language/en/email/' . $filename); + $template->method('retrieve_var')->with('U_ACTIVATE')->willReturn($url); + $vars = array(); + $template->method('assign_vars')->willReturnCallback(function ($added) use (&$vars) { $vars = $added; }); + $template->expects($should_load ? $this->once() : $this->never())->method('set_filenames') + ->with(array('body' => '@phpbbmodders_adminactivationdetails/admin_activate.txt')); + $listener = new listener($loader, $user); + $listener->activation_email(new \phpbb\event\data(array('template' => $template))); + return $vars; + } + + public function test_uses_applicant_details_and_keeps_core_links_untouched() + { + $vars = $this->render(array('user_id' => 42, 'user_ip' => '192.0.2.12', + 'user_email' => 'applicant@example.org', 'user_regdate' => 1793023200)); + $this->assertSame('192.0.2.12', $vars['USER_IP']); + $this->assertSame('applicant@example.org', $vars['USER_EMAIL']); + $this->assertSame('10/26/2026 9:00 AM', $vars['REG_DATE']); + $this->assertSame('America/Chicago', $vars['REG_TIMEZONE']); + $this->assertStringEndsWith('q=192.0.2.12', $vars['U_STOPFORUMSPAM']); + foreach (array('U_ACTIVATE', 'U_USER_DETAILS', 'USERNAME', 'PASSWORD') as $core_var) + { + $this->assertArrayNotHasKey($core_var, $vars); + } + } + + public function test_ipv6_arguments_are_encoded() + { + $vars = $this->render(array('user_id' => 42, 'user_ip' => '2001:db8::12')); + $this->assertSame('2001:db8::12', $vars['USER_IP']); + $this->assertSame('https://search.arin.net/rdap/?query=2001%3Adb8%3A%3A12', $vars['U_WHOIS']); + $this->assertStringEndsWith('q=2001%3Adb8%3A%3A12', $vars['U_STOPFORUMSPAM']); + } + + public function test_invalid_ip_cannot_create_lookup_links() + { + $vars = $this->render(array('user_id' => 42, 'user_ip' => "192.0.2.1\r\nInjected: value")); + $this->assertSame('', $vars['USER_IP']); + $this->assertSame('', $vars['U_WHOIS']); + $this->assertSame('', $vars['U_STOPFORUMSPAM']); + } + + public function test_missing_fields_are_safe() + { + $vars = $this->render(array('user_id' => 42)); + $this->assertSame('', $vars['USER_IP']); + $this->assertSame('', $vars['USER_EMAIL']); + $this->assertSame('', $vars['REG_DATE']); + } + + public function test_missing_account_does_not_expose_anonymous_details() + { + $vars = $this->render(array('user_id' => ANONYMOUS, 'user_ip' => '203.0.113.8', + 'user_email' => 'anonymous@example.org')); + $this->assertSame('', $vars['USER_IP']); + $this->assertSame('', $vars['USER_EMAIL']); + } + + public function test_false_loader_result_is_safe() + { + $this->assertSame('', $this->render(false)['USER_IP']); + } + + public function test_other_emails_are_not_modified() + { + $this->assertSame(array(), $this->render(false, 'user_welcome.txt')); + } + + public function test_missing_or_invalid_account_id_does_not_query_users() + { + $this->assertSame(array(), $this->render(false, 'admin_activate.txt', 'https://board.example/ucp.php?u[]=42')); + $this->assertSame(array(), $this->render(false, 'admin_activate.txt', 'https://board.example/ucp.php')); + } +} diff --git a/tests/runtime/board-33.yml.example b/tests/runtime/board-33.yml.example new file mode 100644 index 0000000..79adacf --- /dev/null +++ b/tests/runtime/board-33.yml.example @@ -0,0 +1,12 @@ +# Copy to an ignored local YAML file and set the source/root paths. +board: + root: /tmp/adminactivationdetails-33 + phpbb_branch: '3.3' + server_name: localhost + server_port: '8120' + nginx_site: /etc/nginx/sites-available/phpbb-adminactivationdetails-33-test.conf + seeds: [] +styles: {} +extensions: + adminactivationdetails: + source: /path/to/adminactivationdetails diff --git a/tests/runtime/board-40.yml.example b/tests/runtime/board-40.yml.example new file mode 100644 index 0000000..9511420 --- /dev/null +++ b/tests/runtime/board-40.yml.example @@ -0,0 +1,12 @@ +# Copy to an ignored local YAML file and set the source/root paths. +board: + root: /tmp/adminactivationdetails-40 + phpbb_branch: 'master' + server_name: localhost + server_port: '8121' + nginx_site: /etc/nginx/sites-available/phpbb-adminactivationdetails-40-test.conf + seeds: [] +styles: {} +extensions: + adminactivationdetails: + source: /path/to/adminactivationdetails diff --git a/tests/runtime/browser.js b/tests/runtime/browser.js new file mode 100644 index 0000000..672030d --- /dev/null +++ b/tests/runtime/browser.js @@ -0,0 +1,91 @@ +// Exercise registration, activation resends and local SMTP delivery on a marked test board. +// Inputs: TEST_URL, TEST_BOARD_ROOT, TEST_ARTIFACT_DIR; outputs JSON test results. +const fs = require('node:fs'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); +const assert = require('node:assert/strict'); +const base = process.env.TEST_URL; +const root = process.env.TEST_BOARD_ROOT; +const work = process.env.TEST_ARTIFACT_DIR; +if (!base || !root || !work) { + console.log('Usage: TEST_URL=http://localhost:PORT TEST_BOARD_ROOT=/path/to/marked/board TEST_ARTIFACT_DIR=/path/to/artifacts node browser.js'); + process.exit(0); +} +const fixture = path.join(__dirname, 'fixture.php'); +assert.equal(new URL(base).hostname, 'localhost'); +const { chromium } = require('playwright'); +const username = 'iptest' + Date.now().toString().slice(-10); +function cli(...args) { return execFileSync('sudo', ['-n', '-u', 'www-data', 'php', fixture, root, ...args], {encoding:'utf8'}).trim(); } +function mails() { return JSON.parse(execFileSync('python3',[path.join(__dirname,'read_mail.py'),path.join(work,'mail')],{encoding:'utf8'})).filter(m=>m.body.includes(username)); } +function adminMails() { return mails().filter(m=>m.subject === 'Activate user account'); } +(async()=>{ + const browser = await chromium.launch({headless: !process.env.DISPLAY}); + const checks = []; + try { + const page = await browser.newPage(); + await page.goto(base+'/ucp.php?mode=register'); + await page.waitForTimeout(2200); + await page.getByRole('button',{name:'I agree to these terms',exact:true}).click(); + await page.locator('#username').fill(username); + await page.locator('#new_password').fill('SyntheticTest123!'); + await page.locator('#password_confirm').fill('SyntheticTest123!'); + await page.locator('#email').fill(username+'@example.org'); + await page.waitForTimeout(2200); + await page.locator('#submit').click(); + const body = await page.locator('body').innerText(); + if (!/account has been created|account has been registered/i.test(body)) throw new Error('Registration did not succeed: '+body.slice(-4000)); + const account = JSON.parse(cli('account',username)); + let mail = adminMails().at(-1); + assert.ok(mail, 'Administrator activation email delivered'); + assert.ok(mail.to.includes('admin@example.org')); + assert.ok(mail.body.includes('Registration IP: '+account.user_ip)); + assert.ok(mail.body.includes('Email address: '+username+'@example.org')); + assert.ok(mail.body.includes('Registration date:') && !mail.body.includes('Registration date: Not available')); + assert.ok(mail.body.includes('u='+account.user_id+'&k='+account.user_actkey)); + assert.ok(!mail.body.includes('SyntheticTest123!')); + assert.ok(!mail.body.includes('7>C;eQcVmRICeA5OGp%g{l8nr5BQb3wA1z{r7IW*LhD|Wwl zw$$#{EvCRB5lGOR-b>Q5SCbe=1yC_%gewDO8=L&P{iU7!ZP0kpLTK2K&z2%qzCJw! zt>$QX+_9?J-!=9r8w=CS&=I}wvAjFo3~i}sU3zXa=t%pcf6^B)dEI-j&`r8OHM4z` zh-Vl$*jbxU^pP<(FGY@$eulj`sM$jpi+pFQs8M=8Y-xX^tQNM*t8&`dQzrrT&7%ea z@!Ed&f840aAz7^&z02LQ)z$0&Ks@Gs3p6R)!EgG~GzNcjyuZI#NIy1OMB!1}x}$$y z-%?uAl%4;$Syu*!5&40SC-{UXerzfk4>86N?@W4Hll!iR`NBT3b1!w!WJBTgIRcpo zzNMhqDa)-yw=>&|r~_R_!;4OWHXn={G|<}Ib9aj#UvxwtH>%w=LF3e+T2&lI=JJrN zO<3YQmEU>P*Z;P&H`^bZYdVR*PIY^7L$V@;EVk%+38{$pCb2%m+5NJn#E%s%L{hTb z`<6NOeu+}O&}_BDbcBybck1aA@iK*I_%_b-pEVaA4M-=0JmEVs=g1-w&bAfBTA*zN zJ8@$J)o!dJlis#3O1>U)sRGbsa$*n;0C!JCc{=n3PPi6x1O@JL!otmCFoiXS0W)qj zaf7B4_Nj2(zBryj0(>LSDv^n?I`!N9skXI^-h1p32UqeFrp59LuWAWOFTRW5Oi0)b z$ve@nbLWpS1$g@T~6`=q+yODpFAEEc;`&#D9y&@dN6zwaG``*d8aBXIWtf_l5|Gn=&_L zf6wYPPyB{|@|jkSyh1}tgEIc@6P!{?sw1by*9V73Xy`TOZkAW1G6R`$#+4!Hh(hbn zkA$n1RKxU^SbpLXjXd>pg@}!@jDxDEMc!N@gJCE9oiLvDBklB z#Njx%w$HHgTzPCxWZ#-9nSfDC86)yLh9k?K8WUHtDg%fOO-|nw{qoIb;jYJv zzQcxl1WhfSGeabBt2yepSmln7j#04_JVumT1vQd|E=gH|XX6wRhxbC`OzsW-luKVG(xyvbk(X z*okAL{?bHL;e6>cj+?RdU+$P~#7brUSA&q^j?CR(*!iNv{P?N!Fr|p9L)?&uHGrD0 z*pN{oe)m8Zw5<5)j_w$Us5sV?kH?au)xA!gKqM9LOjxs#ThtW_%HrdmU0n7$+COjJ z@>6=RZEw)KV>E-uvC!uSl&Gz9mxhv^rSx+ZWP&bS6`qR_U`>7G56ERtUgS^n5wMzg z+9O-gV1JGLmoZKd*5{a_BBAcS43+K}DJ`?-eSFXNc8yxt?B#RLetz?n%?CBQt1HqW zDfqg+d2S+zpM{+6b~4XRFx^pH$;*8CTCs`ytm4_dR%N)iBdF9b(mTkC2m`n(5m2e?Y<9qg?@9&S=o#3I*O%X#@M;VcVXPjV?a~8MymGC3{S&+JM4%Brv<6}bER*(h`G&PA368+t0$i5Z{?18&P+)kacT1k4l<5ho>4wy6V<2CWGt6w>+AHA zAFFZcPvHmuLxG?@H)EIAtLqTn4v8|qxtk)cH7cckn~dLayNOk_5djpuVQ?qw72DcM z7HtK(wouqvUz(PNNhv}2b$wrwkB2*u*gw88wUnF3UZz)Na9g0nah68nfh(g zSrtGgpe1)d6eA!PaJ+aql3WXWK@wn`w?j4z2;Rg8YsR&WI;LZn0D zdp0`H=48xwsusi#k>EnGzzYi=};j_T*s-{uNl)9*M%S3a#c?CQ%Y#uhM zAs4xcvWyoRv@AFIjP6P`2mQ4_w2MRXXO@r9L3V9W8+)?W7qJMlQJum@yOgr)S z+{+A#yKpJsQ(7^{+3~QGYyYvX#1hv|PfMO1j_QHtazN6l7iY^IIh6#xbRfAKFV+GH zr{vjh@!xn#Lz>-ql21KvZm%WZMFZnTlkw8OyXQkHduaq_*f5p~rUrVzUAm~dWIhu~ zH<=42fvq5@Z}m!dPTMU+MWLRb(^Jv%DHs37P%}vb4}MT>H$$b}$KXKY7^4&K??% zMg975<49vhd%i z<|*>f%TyLdBaUtRJQH<4XQScf$i-AUQRpJPny+dz(K^zOEd?nPXUR*27m#urlJVGC z$iCBKbUufHnx&7(-inL}6DA$jqKsyGvI2^}n0p>iM!I^wLvl>5Q<(1ncnwzH*3o%z zqN|7a1Ue;F+#$r^@r>W!4xLZ-M#D^WbOLT9aNLKednhrJtb0}NqPNmJASKw1fzo4F zQnx@P_o4BLb|Pd~GS2=i0Xi`Aj#;6Trf1(T0jy~WecBlR{w#RD(Sic+qF*d3xNu+| zCogJ4l22F@(kRsmNtEstN-TH8PQQB1s<3V#5*A1INn4yyHth<}G5$rx8x{@{Nt{kP zI3HS>9$G0A3ufKC*E3xk2=nA6Ns4iNaDM23&Rr`~&@@OW-zcF&33)`?Z z?Mv4WH=9gG&bHj&QtXmm4!;@F7}^G+dY!RNuQBm|r9cRCRlro2d|RvI2o|qGA1*kB zo!EVIPX**GIx&ENawZ{c(7QjL7LC{4T^;dB&-B}G;uy&8H$^X7hlwB(r}f@SF3VV< zdti39UfEHS?ZuzO(%-;L?-CtyZ{`THU6>rOsLBgYO@TC*0zEP;)7tftp`5fqy?Nw} zbW%pT6n-e0;gE)8(^_nO@>9e>{LM}3YG9LctmB^jdN1I^ZvRT8cs(SP`}k4h@FRg5 z?p98@SY)lR8p8yH+vWIf_WUi|CZ{p^(^1c6;_oe~u#MVdc|_5u>THF1ezMTQh-e5B zBgtz_BwsN%l73i$E60fmNLS#6l8w5$eZxYxg1m6Cfxw@wq7oBSkWm_nVAv^g$AnSRmo)2o6#7?e1;*%A<01R{%7iR0?Ei38E(p>^zBH@cn%ruKiY0D8YioMH2k)uy%eNoJ@kW1ZrKCf|MSk zTA~bBdPnB(v!bgjxh}q3-?g5o;m`J$@@JHUYAdWX;^PS~@C^}BUx#Sb=*{MncL)Qn zd5dF4?JeR3fo2O2c$o4C>0H#d(+}ou2w4n#u^~i}kSq>j()9_;{m@(n9R{V8K)UCc zjGfEwm1s&n&CfgQy@5a(WLAW^{654!6`!@rWd3#BSJiWjRN(890fB-_RLb%L<%^zK1G&}E-X^tt2BZEijLzp=450k-0a1tXb3A+XdJgJToTM`-t=87 zSXQlQe@Bg1mL5>+g-${2Gc6qxaONu56V{Gte1P|M)AsT5kGzKoHhf7iM4Ok46yaGo z7Prh@K7Dv^C83#)FflPRXb9JKqLsNAOOyP~P|U?JxmB-t-|L%xL9J}5-e53<%w{H` z766f~EZK$>+G8VpAaRjHObu`>ow1MR3wWS5H0^anLh1vTrd~!-?xZ40D|(fcCW2%u z@$_F-`IC<=84GUT$vF{3*gD6HXesWWf1U1BW=VJP*1X%h3x@6f zqfub==I6}7p?kr5YFIM)t%=RGRZBi6$5?F_$uKbETfULFs@=G$M3?mIuP^s2?E7mp z8)zB2DkjzTfG}_F#wZ{}MJ;cV=h-B6D1-IME zi{{M^Lb;<8F2%tIr=sooSTz81uz{D|ddJaO8&V23k*0f%Jf zw7^{RVTt`T9kGZ%y3+}Os`m=D7^|wNO`h?T0NYmcbWS+mg_OZDT>z~^f>g1U9HzZ| zyUG8?Is~oBL4Ucw-@unBm%Fzo5dQo}(++MGS#Y8{-lE5D6UC?3+qD~4qHQ}jfp7o9 zeh+Vgh*;=`Q#9DvBIJw}$J<1Y92=m~J~}=7uUfzrTRAe~@TP9!dp%ourcK8wUE(SF z6eFSe`K#mw9mW>%BaV||GJDhF2xAPdSrMf-Kvo)OFqz{qraV4Ze2Q1=<$39w^4>{X za&cUGX1g7k48KVHeTLl1gM(HpRjny0QsG4g)hTKhHB0-syQVqy8~0|UR78GSL}R8V zWFk#n@nsFyGBH{htg5ugT=C0B^6jdaO~(*KIpb_CNPWLS@;BjJ@)^B`N_?Ec3@Dl# zlW%Oc;KO-r&r5yg#>hFiHAW#k{a7nIGha$HWjKA>0B@G7TNFXn?DY-f?WBC}MbwIg zN-Yxs%RPmjMyY-ys(FK>SX-ae|6POBKOBs`Yz_XdSIoq+RNm)8mwb=L?LoIPju+i! zfO{1Oj$ic65?oIgvr&?bB*C+zS;JJ7$>H>PkW*t1pLzT}`DqX`Y6;-+0Td zuO;uRW?BRHJ|Cs^GrFwm{mv1kmXkkNJZ)km?+)Y8_g79M?E4SACy(#=GqYaOs*94i zz5G3@-eq7@@FwVVG8Hhhb5t-iMX9ZkMD^f8dSBc=U0YN zgrbD{?tM^=E>)t1H1o1Jo_ULbh;|P!gD0j15R9 z%02VavW9@dP7VBR3!M}>ubU5p>U7wO3LdSokG}*Nc)mE0-hx=!N2i5hQQiDQVVDFLZZIO%|-KN$U4CF zmTOWlSih`Ff3u0RaHe&_5U{2of$h5A!M*mcFd^GE^=N1%Bldw@;&(+Cg!_Qxn%>%6!YhjT4^f#|b zE&9ijcx)bVe{NM9sBu zTp~-;!bevho8sli^$_iHeEQ>EO!cr~J8L^O-GhO9ps*Rr%a&;51|4i^p_({aK=L2@ zr~CuuI*<`WK!uq&n!hntj%}KooZra^hE4AWRlMl&<+jX)jFCOq$F)w^D``9IcuN+{ zlO~|C%89=u7UvW7jv= zNi>hjWqxD37-{-?oM*g`zvB}`bF`euQ8-KW>*~vBL21ZtNN)bQy-U^lPvtn#ZJN#j zGxS9i_Rz6wR4qa6rQ8TNAO(WTja|)Hbo(-n|1P(3 zI<;lRrR|v_guGS2N2^&)_&>nAa4bbxlB7?VTJ`X%_0`Bl&C^^cAx%XQR+NWaSyU$n z=K>c~mbPMgo0KeMu>_vJ*U3%MBC)|v($6v|vmTXm?nB2js=B+-l}F*#|EN@o=sLIz z;rt*Um83bn*K6b9;rPQ+>zOyCBv$Ei$UUJBo^-cAZP=wZN~=&y#&TGrHa4AVN{gY` zBokUkJ2P%tv;ba3xS;>ecOoMG{J)sMMyGOlacM%%+onDq&vlh8PiaKmAOAAd*;~nb zEAxEOsoI+`^1X$X+29i|S1>F_Z%AObhYkExT+VklJRg$0jHg>HdW4D&vA-&?;KPa3w-u8 zoRd{KSl-GR8%*B?b3|(wX?kg=*y&x{$^XEGjBPp(F{Ku6<(k~g(w{lt@1TR6?Ty5Y z#O8H0BuAjqXCqzLna;@htR(nKRLpWEQES)^q0t?{>05^A3i`WF(}@S{p{8Zr*;)DQO#K;43XPXf!rCmjVD?s>mPA;2Nvu z$I7Uo<;90WvnlYrh~D<(^}_9^f-Yga)ZAk5-Ks%n-?5Cm{JuqOJ-j41or67KVP`IM z9CqfoLeKF#_8GhU&~{}8LZ#$WnR6^h(Uaa!e7lzHb>T1tLV8IvMdc#wLTWKTipveE z+psW$M|QZg(qkKM=hh$1@aK7}?4QO65?C?m={~nSl9$z1Z%BzUqe(<%?*YVEA(M*J zU(I7_rDR5S?u$EHhs%vbCCDSR{aUs!;bWuUojGgm#K98;+u}*a$**Rw{^NzIK*@?< z3#qZbk8<9&h=I%JjyH_vk%;>LE5+DFy2{a&-+Uf!A9d6-KZE>ijdRye8XawjqN|pG zud*UTIWWcN1f%x+0x<3*UCRN70uaxcn3j3LU5#xgcZ_@ zQHjav-(uzdOc(tOLQ#5`K^i1yao5@%Yqk`K6p!sW8we|{ua=&c6j@EKoy;;8hbn5h zTr}_Unb%F6n>to5bol`{iCZvB?~NN}9_ubFq=Wi<9(l=KI;hq{^J7ocf z*KF${z~5Y>j*-*GBkI6+fZ0q8U^!63vLs4aoOhmF`Y(mJ*pT5}7B>L`rZ`kP_bNJ$@J!_<$=&_1+ zKl8w!nzE23v|Zydd80b+zM!}D&^L*1IKU6FmL*KIlKB0xfNc!O!0Z` zSa!)TcpNHg=nIT2jXmjKZ%bQ|$t~J`E^PNw!+v45oXGvWw;UlE(7mia4En{7a!rV0 z7T+xg5hWAGE4;db2WDXjQI_diY3|Py*?pJSVDKc$R%MQua_HIyJW1 zNO+Zw+=flB`gx`TJX7i#*vNx~F5o|$i6f340{2?Zw<-&5wpSMAnmL1R?2LMuW6@?>X*Ps{4i7^YeI?|+4Oo?VdocWeCM90@17qThF1M$%B6|(QiDdA&+0Hd9~X*0jJ|Cy2F@lshVmwDRA1l zv#{R{wJxv%+0`aD$mPD`)%Yt-_A4iT^7_W%v-ipQV~Q{M)|Lq6;TP6Y4ohS2_X?21 zB_qpA(kLXW+8T|ke<-JCKmMb*MR$%2RVI*wwh2 z_ufr(CbE`kXKoNL-DVKzJ-{4p8F1bMJ!plx11Yzx#xQZRjr(wuMAgZUXLox;iSe-6i?QahrBa8=UZ#4+r`$}%xIijzuax7c34R-PK73LpcygK%p zqz&T*#t>M`C#B%sxJ;|$pkI$DAF&7wwsP|Qq%FHR_-JoykJDBHcy_mg&4@Dpbi94( zSLGDDiUu_nl)$}M?V)RTeHdihkP#D!F5~|2TLP5d8d>|?4!ChS89iH9NA|^FkG8ty zpiXT*3KZ8&IR_oV&gb)zACZ@bucIfVwU(U4%!75n%JE+6ITZNQ*1no{k^x($frSJSpZbUQDQSUw_ zum3-!K?XcZkgNY6T)=-92o0$s=KOvj$}VQvU_e7uvVT(b-wu zcXFyUg`30gpcyrYC!A=`k|gojbqv~I*(upIMl`;F^duhd2!x{c;XL;`QW01P|H>wP z-a*){NsiQVlM2iTydH83(M{j4gKAjl3M7;sBk`)~QuDf=n&@_0Io}yQ2s{+GlxzGA zS1FD})30S(G*hfbo;AU=dc{4xC3j^y?%tL~IT9P``NeNGs`=$lt0nxJ zg&6(sm;4U|ew@_N)BZdx`T_l)EH8mUZ1?ju{|;A%Apxm>`9H&zO7gXiuLAAfq{z}A zK3Lc#4IM7qghnWT6}U}u`yl)Ah#z-s=A8YCy2_X$jap&&%sLO|UPEXyOT%=-e{)d; z>hOj__Hswgjg_|4q)3e`gX~@?Oxu<%feeBA6U^oISXz9dQ!|_EAsNR!WJ$bp)VQ=2 zI^=6VA`ZQ~hem+RXHhHF_0jpOo6>&@K}<}eW4$QNdj_VwWZ<>H6IPv<{ZUm;Eu0}~ z(wJoVvFkzF;>%u=uUEiN%Gba^#m(-{h5RggE^fcJ!Z9*@9W+Z@Wa$_dsyLh8@|(Qx znSHK)({Dr3NcH!FAtmf-cI-B}+U5HiWvq}QHkm0haFJ$A?wJnNdOfpN_N#|PWBuPA zlA04&;J?M_pGW@KCNs@TuR^ng?rwDxvw<WO`Sl*~!CP4*&7{TRz?3SW@PMOXz%Z*gmwuwrljz4|Z5eb^HBcHnzt zt3mh`Wx``9&LJ*1qQ;Cxq+)+I%S^(G^3wjG!5yW42~!muv~*Ah`iO0C%V;~~ivdoo zuyqThle4$x(nHa<*{_0+fA=o@%1@$k5iHF5FV)_OPlLIM#JOqweUUH}xfK*aj$sVX zdMq-f#8Y?DfIDf~R-uI&W+AzkCz|TU$J_PcX3+N4;De+?4p7^AdTc)vQqtC04^I7^ zzS2D6IqN@Gw=d_zF5eC~mnz*C6mLS*cUJphB*gO8d{!YP1YLGHjy>-0D=Xyp28cWn zJUH3t=)J<)1sHo$E0zMQl%LHnJfaa$hPE83*-{Yv>NW=jhW78b; z&(vf)a-@bd8_*j2W*v$cGiXdRA8QFc1%(C4N|Xxw^GBB0fqhut(f-O2vH}Z&<}cvU zCu9~v@>-YPdTD}`or~g;8M*E24>)s_^D=NpmchIBky_$zhzh!^nZ$(81Jt(IW24C? z^@ZjKWKLekQNR|d}QeK-i zsBq8m0(vegmR{7+RKGhNLzrcolpqwL0Y4pP9& zU#;@c16Qst=!G;R+$vVhoKL5Goija<@w!Sbjm`6?`9+n#wM4mG!X}aCs&GcD8VXEr zSC}g06&&^76vrG~siE%OmjINyy5jTIHtT9%pZLcJ(WPYP4bIH~qUp}v=dXM?MezH) zw#hoq2J39{i)G;r#3Ija`JGFXM9!6;3;FMO4+LTT-R;gnn;G$5;;T7}FIUSAZInkm zHj7=WBYSvtW^!Aej_ZrHFWBc^3z~HcrO*9%y{(eP-~-e0WO*OsT!wVf5Lvt1woXoT7#|;kIv4QLoTjnDEkl-d zmh>Koa2Iqindfs>wUH`3M&*>A2MW}oFgRZ#>I)P&QyP`4F=BJ|`4iy;QkeBbk#;%qzy(H4hHng#8fV;c4Z4xOx1jvo`f5VHNEI;HC3L zdz3ppMak9Y`Q>=B2oFm@u-0y~3>1&rP;4RCk|9zG)3cu{FIa4JuHnY+)VFwGYxQhn z(zJsV4J>z?M*!eL-wP0k_`mGqjQGXeHBb(E9U68DP1hMCoybwwL=K8+pov&yR4HH) z3fg36Ct=&(w6*b^A79J2J#6OI&92D4~8@z!Q|dKfSDbF(bmJanr;8+eGBc=J6uv<+{UpnL`4B48K>;4$&xM zK1NRP#cXe+qcxpR!qd{A9}$+#eAwnNJ5kUXyhdWVCfi}Py5RFJ7`d+Gy!VH6gFRT9 z?}kt1@D4_>U|07au_V+xB6xsG0csj0>vEAH zVY5tYvfmrb_>*E4BCF5fe>LfL$Jg;OmZm(kms4NWn(Z9?C*8KEchKJ(a9u1Vqp>0q zQKh<4HOk{rDRsBxJ0+&0F7GQ(qfc|2^`omw?ecOGJn@z3{5rP2;+gJJ^}bfUJTUnC z-ZxGtyo%=_v{7p<4@#@K-GYS%u7TfzzcWA`G2LFV`}J;iijL=C{AIfGwZaf_JR^}a zif)J=q{?;kS8k1YCslc26G!()r-G1OtSTG8Q2vwO{D_=^kG|tQ8zueN=a1PppuZ~` z`BASW(+AG86oo5UkM@j~f83BTjqR7yw1|ZSu73Jw$L#;lcJ0Z}*?8e%F7*MF)8pM% zppoDyjCFzI*PDbk;rS+@^IV`w`Ok?dw^CneDoW(}xDikxJx{!MGV+p)H_S0J=QE${z8bz< zHS;}DG>lJhFUd%;grCOR^kvZ614CpuL#?Vi_g~K1;siAuTu^60!JcXvYR2*Ope_Ma*@4 zx;zuRGS*#rP_XdGl}>Xq04c_(g5>nkNkgQXipjNT&+&X#1l^SAWoaw1E8SupW?dPb zL@1P18uN*6Z)#;)Z71Hn05NDO#-zm3{>;RuTRT6#wBBmX)H$Smn?k&y`A!gda5CKp z+w971b!mZc>0lz!)KoQ2KpWtg6%J_m+urbX-2dnepBz;IQ$qQKf<28%2veo=I>|MQ z7z={aOVCKYFOt6fD>NIitlNnEue5HAfqQj2vs2l)(I;}DTgqltQAD4m zs7KFO)FZE@a@8IrzI?*E-+@db(ft*{=kehorqNy(XQoBYSyeM4a3lIWp%Qx%O?dr2 z7t?JYT*g!I-Ct?B7i|i+Ga(h~SHEsdsCOvDd=`MK+cT|{k_H%GWn)(9G6hPZV7S{$ z`ffy}aYUJc**T?HLsO|vauI9^%}LKI$Ptm+#?@s&g$2#o6zMmMy+4Y6Oh+@UcP|J= z|6A2!67aU(;Ebzbib&P#wk?IX?+7suLzthPU1nbPMD${`$_*q2T!ko8GKjlNSci0v zX~k4IIEz(^4}eNWZSaMO0u{4Y^obe{{i=uwJa5Q)Gd}S1=96)+uVA+x$)fKJH|z<| zwZ55in_7EEvWIkh=KlbF~>|I~lq!V`ZM z?46j-F(NZwSaIYyP5OY^=$S>)Tz+e6rSyH`?feS@~dQEmfeRq&xj=gI6Mka(tU` zh)|b3vL-swgh(3prT{O;{rN)r%h@t%!w&qxyoKBs)#C%mF^5;%rGXexc^mHd z{?Ae|Ltj9agn~6FM_aLP@77QV%#j5;y5%jCBF298(_L?kuj98k0eB z{#gs4fru55QTeXPhN8)i5djwuk#5*=&9BkdsgjUy!1OUJnT(?#8jA`KHy5a(qIzJo zi(v?@rk3hxu6v0kur^_G`3IuG4|QOTb74{cp1Gpgz!^C;wse{0y(`2n0G)rRbaoKS z6rUlV-;ni+C0}V;x{}XoeRYC0?kdn}Ia1J4hnDLV{5g6}>LJ!eWmPQKb^)P1Gd&%k}4-UY1D1-WUQ(m(MsDc1tKXRvJ0Kgc_uU`9!I_p>zv@)RC8mvp zYN4fjk6=+Dxu1<85XLuv*dT6bR(;qew1}!g=_H$Cj?rw>aulEUUK2ATBI$vY40)VB z3o61g0R9bcaQPr+DeAkhXu8{5dV&WzL5F?gvA6`o@tU>r+qu6#!?expA-EBR-|A}u zMEg^wvv`+d+rvwLZ0uctJd~=GTwOJ9)ecQB}y?@}g<$ zw|I6|&yGW=YceBiE4KSPIXyMMlz`8^$y0}oqk~x~Cv>r@ zC~8YRbo5&qwh)m=(b|1UT|x5B&XVWa=&(lTR4}v}@*vfQP2qK7N8)xK`{`c=N<5b) zSb1{o8@_eP@X`nJ=|wvoPrvO_dB$miIxs~_1_k9CV|ZR&Y1FeR)chC*I;Mq~8FSr5 zzZ{uu;!)0-RqaPh=?GM1K``(F$JqyQ|43lg7Nnu2L?%>gu`>j!*l3+KgV>v(FDg_$ zo+%fNj=kzF-z}wn0)0&J(dHXj%u-~Z_)Gdb-*f|kH>6Vqvokn{i^u#PhU@#bOt-e`fDM}S$G1&^o^2IIot5K&iV?T z0v9s}+__CrDiS{!RHuPauh zwuj-KM9^;5YpKD}b0#$plYQ(*L1f+E109EIKmH5Q zk?5vYK;bcm>y;5wu?1hoK*Be@fuAw%*ir));H=DUciaW!HG-DZbbIc_>317i%{+;z zqq~VP3+J>fS*IzJkhZP0e@NFy_<5tCd#xKUq}K^+y+`A<#m}RY;g4o(k*s{marGJB zqTSr>sAZe*FDTdkg3*^+8KG54^QP+5-Sm?&S3+t^Gc=ot$wF`A2DWgk@byx6zntHg zS&A<=6l=q2V>g~sqXszc8t-Loj%zOqHirKis23m@qbS=A4W7A2WPp#*g>HGN78{82M;;KnT^fD)%L`ZQkaJ14>)2gbv*sWaN zJ~>HkJvK@TjNV=fq%z1$+r-F+EPl*Q(KV$Qu+ERPMPxFa2`MDApSv%9?a{J>d8Q+2 zV(;SK2K5;eUy5>BKY!Z11W#ZhVe}*H4kVNxCOX5xE7dj@m_6-`7^23#wfvh5d=G&= zmc-#%B_UVY2%p7(IwyxkUUr()V<<@oYEs;zIYT~UdQ;9lh{{#2abOmFaF<$XDj(nf zkPZng+>>!AG*fcAXVbG3m9w~QyXojop&hjTn!C^HQjj~oWvi<{tC0+X*k=XuRd6=m z(R}~An)lUN$AIoQr(C{?&Ip1?yH|tw^tmQFY@;nBx{<-Do|(;0E zCGRSjc1MMn3f`kFz!uT_jEv3s+4y>_ALZz&#~=?6GZdo`8$U-)D~xA#6Upn!f23xd+oNFp&&zuQ$H z11{9dmLcQqylRaxHo{9VMm|}7V}s%V4-;>-eSB&o-*{5fwL=bfQa990IHQS+31*LX zjh#8rHiBp`OEx=Y``1!%QVX813^c?2V?iNnHBLuM+%S)x2gBP3RiC3^= zBv6`$L<)OpteU5@kWgs0*vTRS>Yi)otfdx?JaCM6QZ+%{Y|NyQ)90ACjylNRD1chr zve0G>1aU3eRFZu;n87tw-auxyC*41ZUr?J9m$TeMlGnMNX4LpODieKh@HSBU)h1)r zZsBZ9O{APocHK##u*iq58cp8UauZlbv|Nts2?!+2`UHDx8ENaMY!&Q8O?a0ILX&Lj zsw!2^QT*jU(Zf&Q?ZEP_`;5SZNYc{sMekZ}FIHg!1P5ias2r7N-W0GSy8l36G6J|c z`O2?lyov>Rg}-h?TOBFbu<|&t5?zMSQ_3Xg5zQG-jS0DFtee^IYzxYA^C2vph7YAB z5YWlLQ7~#H|3SfsJW#N)1cKptYMTEIf=Oe?z%^(-azDclL^!OABJE! z_)>1aU-NNq$^<#dZnbhl{E+{%8eqJTCc>K+yf&B{(F?lEud-TJbRD*BPo>u7bQ*KM z1o__RHXx*uc`!3rb%qSERvM2x4dWw?IrcRs&&H1@WSd~Fr7)if+c*wk68x~4K-blj zhONdgmgrpk%6c<+mXkg1?Jn_KlD#uyQ|H2U>yc5iJXn!iHGb+?v0A%Xh=~T{oFwdd zx_?QTGMAfMVn?Sa@}msyon56CN5u|mB(hQ>oTQID>Aw0fqXenc5S0&8)@q7jT z#Oi_ip9svn01Sb7_@0ojbeXh_)ro&2!TH*cncAUHO9a>2_cZKjMR#!lB#Chie4(}h z#*|G6Oq+b&>kpK3tU1)}BxKJL-1DnfJuM~PCKQSj@`&1tlvd7U@%S>9g5HTAb!Q|x z0JCiIWxT8AYgn`unM2nEUX%~Vj$G}2-s%V0+4IUOK@0xhn3Io5SMkE5aUV{f5*nKvzN(Q8pV>HIcwFi({VN{KP zdX1AvF%JnfeJPABx()t{EWQ*0wW;~gb(Z>WZ;#f#+PaAMOwvL~bZl&LVw6AT_J%LR zq7#;J>sR9mZ~z@m1uqHb%2~Ic$Rc4_zCv1BZuljFP5CceCchr-PGHgE!>G7=e&?WU z!xQ0db;ZhYacLUU?t3;yu>O~|(ecI*JVVOl8ofoB_$g219YakH7snFe#OPe5`oy=( z#Y~ce1%ON5m5*ARx~H^Men<~1f4;6`&PPUe>XYz9yUQ2>)1{JC!Ypb@Jq^S2H<&+* zRvz>{Sexfd=>&&6oa36HlA+OcF#skX3(pH#R9wvQ0MQe;sAYqRmqQXE$w`V%hp{|! zDZ_G~cmQSPm{E9>8Afw~XPER;P}!-3g7hhc`nv38g_vzc?k_&6I~yz|m+Z9ly*{0&~|arBaD_SS2RfCv*5xeqQH}E-wUE$=BZQ zOA~iH-Zrk>Z_eVWCl&1#AmU|{y|l=B<~sL#0)iP8#Q}_D)&ZqDYp1fR+LS`beW6J4 zKkg``=fYC(EN>EUI}e@_ibmsafhA~h6H?`3vbywz1e{vP57NE(*a>VdjiYJ}A>~wO z;d9hhp~c-irpsZR=fP^KsvKySWv_r<0ULKMdl@R;yTZErDwOL%_cGUG+Bw-B-ZJ$G zdFM>Esdzv>+-h9zVm+r0+fYWFeffC>#iuEG;G4fZ3}R1tMXG7&ubV$g&FT(MO#9tX zkS>~_ox&5&RQ#ut=g<9&6^~9i; zm9|>NH*^f%Zfu5K6f!k)FWNxQQI8IDTt09HX2vQj1LG21oXdG+ldZT)4frT2QBJBM z0Teoe%g!FkE^QE~nxyAkjYOPxrGV6n{NZtff;xw+tT+s=*6yF!pleltw^*e?!$bV5 z-g_^ctfq6~`kftwee_4vX2@KN;+IC^1CI8>Q~Yn9gs=8!+XziyW`nVIZjdA@`fzdC7$36FS1OWG=oE6XY6QOLoJK(dF#jp}VPU zCA67|V)QvO?D`1vQL-PRLaVM2uksT!_;j&3(moJRvXl3ow^2J{pvsvXP~Y@j z>yL{qPTfNNf*eB`CfExaT8QhWRef-U_eiN?9{>-ep~OK>Rf|yz5)G?jv{JLDR>81j zbqeKMoKvA%>Xe#K|CEKNKEjj3^1UW-!NpuNF4&~81TONhd@n;94KaC^*zJv9Zf4UK zvXs4!b(z8xRMj3o^aZ83N7;94sxVxHvviNT41(DkN_V%$Ji&NmbUbU{zG-{K`+Lj5CuPtW zwN`r6@QTwN;Mb9jvQL&A#p*MjDkm4#K(i41eDUWha#T6&4GoI7Hi%2QcExUj_UFEYsG@S;o-`>550-E9P zoFg!Y*5uQ1V@$N(`KdX|hB_k~&H1;5-8 z01p8)8Ek`Uf)a7K2)!3?-u$HH%kZH-S2fh9T22V!2o`drPU=%Gs< z5me@m`z!1ohB{pJwvvu|y%Gc>M8C%AT}m3z^FKe?u{l)Jt>!7EyOh|q%ySdPRAlrMFYwt`^I0WiE)R6_eH)&$e75fqsyP!fTl#WA z%_>@6juqb<0?L}G*Go7tq!6YNPj`0FhtYhut4H$lw@>x8Dv$o1CS!|d zX)u_i!f``Hn)ywoje^^GkTVfx4rNY+hQL}wfddGkmRah4L zi8Ev9Iv1SpHk0djG$8>fEhICyQBixjOv+`oX;`kn6kgiDl&QmtkYcuY{AxOfFWd~?+DV0D;&(Yw#$ zg)+z)Zm*PJViJE*Wf%fCe0D+oZR=cmflhYbLyI&W0xYUKwzbEh)@(J}!olTYi!h~4 z@RzEW*=mM9CQA3WWNyzAeASMAF?)Zl5t7hS7gUZ;Hat2nejAVN_$4W%Xw<2YH9HsQ z3ZM!qQ55Goo8^c!dQ5buBSG2^>5^8BARAtUH1Nr8y>;%sX@?GP%Gs+f|Me$_dZ28{is9*V&Yi#A2 zyudQGjoNDX%Yyj|V=z+i)v&KCWb@g|D9NYW5GR4LhLS!ku!xvXQO+dvr=fP~iDQPt zWth{Y(IXn5hLnzm|LF`Y5tD)jivs$Kk!#$}%paM^?AbLs30#WCCmd%eQ>k5`cpEkE zf!is|E);4tQ1De-gW1dO{D4>2Sy}5WM~aoo+FM3^839Zh_FWIU-dj>Kw2N?cR7FlHCvO=53t1Y!UFHxOT!z8t9nl+|7*dnCeX>n7DQrUG$cT=2L z0k!4Z^U0cdJ1W%P-x0m8QX?mpbaGO5VFkT=^6Ql0^M)~kj82+7Rn7i`WoyjU8< zL4Qyh(gL^B=hbpc7zy8@M#D6|L9vwy3o99wFFJKv@g-Li+o|&@j@}sg$;gfeoPHUr z_E06cOjzjXU>x!)3FkP>+Hm_ka5X~o;l$AnRxlQ2-<+=whxaho9bABEdHS`Gd07$( z?4+?KnaTmVXK@IN60OpYaviIV*kuX{5o`bfn~n#<`oap@DzC2w<`|a=KA`yqh5%h- z#!r{75jyXq7(a+nHCtNj@9F2VM<84HqBs0(a_QQPLx0`EK|^}5!F?rulcFZY$Llh; zTb}|S>u`@E76SV8`o;bE&w;CPvAJU^CBfvv?pimOsR3EpaYuDYk3rLIWcq+V5)gm= z@ENA)ByfZ{QnbuHv;X%a=VZ?&W!*+EMGaS#HtJ13ZJUx_ZatxJL}a3I+nt}OFrix1{G^;*}O^vnvWFnwh|cIixv zPdisX_djwWt;W?Zhwf&76!gtC)=n{O8~}|Htu$7}u@5BpmZ~3*V5p!Av6}i7!5@Q+ z!Pl6yY-m{jttu5y!arHTAWLlp7nJvlg=y{v{{Kq1l<~Psx5*#mvWTT;%8>2M1TatB z{mU|A^z@QY!lN8JQ}S|`#;YmKZ7j21f6RQ!l)L<&U0w>Om?o5c5$f`U42cn|Bin+# zEoLgMECf9A6PXMg#X-`veNy{okwvwB_Wa2Tusg z2LdW%>1=0xi*DQ$ij5wjP2=+jyxnc5nFw^=n7LC{U8K{(WMkR}*4(4mUvah#b_ZiV zSLc9H8q_i0iKv@OT*IE{vM}8ZdQQ4&>MsDHXrYv&++^DQZIdJkrFCH3=LUoLqpv@6 zpbPYPL+Mm}s#&YIEi{L?x0AMQZlYyxHd=Fl{6EFVxsJ;*aU5f z8el-{5L2A;75(M5A;e|yamRZ2=4Ls8r~)b7Zl;E&GcPo-v_jkmpzQ4TT`YVJ*~sr* zCwq<@Ay|;+$jqMB@g+7hr(K^Slt)L~lc`9@ge&}4o-G_o5o3!?zyPAV-Jt;WTg_@E zu_-XwmtAkEZ|n6GE!+OOv`ai$PcLBF8!4W-&$7wb?F_RrkigI4JeEjT1S8~uYUzw7 zJfDm)Hdz26Dpvq~{pfgc0F3f;i!Z!T&fbqFh1pwB&N<1nFO$BicjC(qCPC)0ch8eX zK6ya+oPDxJUz}K-br2K0ze)dWv21@c&^ydf=w@^;wgh-r`Dhi9h)ar^vXU>KJn@vU zqOQ}EhPjPP#9Ol=m28yDh>eM7b61tUVO$^RbcQL&hL!qWCV9iT?BnoxxA1^TYm6vk z$OoaMBk8^$PdG_0zjl8m%*~Ie0@_#RvnpYrAIjn!4dn;&y4K@FyoPy<@H=JcCG?s^ z@eUa`X6?iwA>lmTyK5Om>st9eRtYx+@Ou(bGtPhcmWZ+_2!CU4KH?u!dpgqi)# z5nY9n+$iIj?h-?IIw_}RCt)mYLRfEvW`E)sMgZLr;)yXKD4vi^Z3djU!FcFO%=L14_af)5_gV4ZA+LOnk(k3IDHpov(L}HX97u z1~?dWxwKK_8DnpR!7^;-`eYAZe2!l)vY%vrHv=%pq-NF?FWr<{Ef{|zfZ0qu zG3j(PK%pYKk3~k+-mRh6D;Qm3>~rDp48!VnzIsdQwxWm5;W5Tn1N96K4R(-{%S7zZ zwrT>GhNV$7V~ma8W5a{D2BKD4vpo}>ym&=J>QymNtJu3M zo><0*uwmEy%93ICL0(6%PB>$s#MD({w(U(m}PuR6Hk zpYt%YnZQ2CZ96D4S=Zy)`9fGzT~l$WW4>)=+$NU*TU)=vfEJBoT#b5k*HJSMdv<0f+XzRzDiwnFF;z}bKHRi*cr@LfmT%OP z6f{(FvPh00?RGf;!|RoC(3+uSy;udUqsY@D7py1Ub*lc0mSavJxAb{ZP@K(~1fGGu z>&`=9i$Rqj|Ug;b(U@&A2aCKBkpNx^ef2 zxSEsLob%(D+D2k)g(n~Y^bkS{l}uzwKQkED71TFZ+YD&|UwjiH8J0_nKL7#?sd$>=RToEEqnC=AC|Yu^Y2=pcOO&Lqs`Cz$Du0OKqFpYF5#l0 zcYJ-51joiv2(-B=Jo!D|k3mVvo+C1By%DrxP6KuWCryz*I zcrgO2&rVJgEV@Z$or`>|rJK4@H;l89!P~$3`rjGVkD>o!R5|~z8PzaI~ zOkbPg#!}8+ZKF~akD*U_4x7){l(_#?l5S%2ndU95+1x39Afa0PfYl4K5Cknzo)=%4v*;9p*zN2TTQz&|o-9(&*dgOaA0B zQKEj>d&f{W!gy zf+`sbzRGfJva`DkCFBm=4Gu&pqq}0L9csx_n7k*mz879O#4g{YAC}(V!hJGBlShA2 z1g{?sO3wBUEX_t!xbCfucGbQgqc+~G`SOvOm4EfEBb@yUpf`jw_--V01}2Q!QF~-> zn%?$q( zbVUxTS@@+oZ)jsH$DdGScZsNZ(QIkbR>A78nM_u4y+eG|lq*6RH&xLsT@Tk`qk~h@ zf*2QHR2F5KQc=j%q%eK3j*InlSXclGyk+ZlY+YKvyo*E0=$X-w+E2B0*_3OJgP1v6 zIMfJ^Q=R=9nn{o1XC)=EcHGM(N~Kyxf*FqrPuyp*KDKeHDAWt)ecs2dCTlx*oFqvHcVDc057mhD7=4@EBm+qPYeZE92bH;M- zF4AWblJsVpU=eXp%Q`$~l?n1Z-gq~M^?x(r6e$)W~OUFD~u#8w;l<(uWd)xBU zu}RThNw`>D2QS~fGgeu`k%`aU(Jcr>KpV_o@~eV2)emXcgFkM;0xQEy9gK?kEFLF# zMJ-4VT>Vf$7h4*bjM>cl8}XyWu%N;Z_qCwT9fqU=eTXa*pDmDj#n%2qt92&_b&1? zHOMx1h;Y_~vS{Z$o1O-+`-QL8&5Z`rx!)ese&ha&0Ujw@p2H`*|A&p~>vPzv{`vX; zjPQReWBQ-tsQpM@y=BpIR9>9=-ErK$sgw&dCOb>Rmps*C1`X#R{P~Yur+oU#*0Td% z2*?WAJh*M)YHbi)WRLN9I=H-@_;H-;-VX5PHixq$Lx}lv{htN{ma!#47fM@jbC?+d z^*cwIWfug!vu5R@RmML1_t~VsFZ`N`3PZ03Q;)d6E$=sA;kV8_d;SMacqS;Wk^9$g z{|zAh=l%XJ<^BIMxBaib|63Z}-F>GY-gluX-;`stVS$_m za+S5!qqR%d`oVBqnw7S(DBdq(uQ6HHf6NC{%E0(h?Gh)tpo5azLKUHBel`UIva{0B zk8%8Nu*$%1#PB$m_!BN&Zk(rDvCfseR*Ire_V4bd);|5ayUc5L6qfy~AG?$vh?xVD z-n&PR#3!+vceyL1f=^izeo6oMahrn7`g>G<1)vNFx$l{o*lV>p)t|!wHAhkxAa@CF zT8gm=bxwf`(|2&DKx|3_&#@2h=sbr{^_{hGWKl08kT!s#9&+uAWN!)g>LI5s}=3BxtHl` zCRa%u4imDU9WET?V{Qe-lF8FJPN>AQnIYWXjTwTU`qF$_x^t@qeB|2}Mn0j&+#!9m z&HOGJ0(V9E6$k8}mw+YEwo5!iSh+V)N4%vA)SM}N8IT{FQW8MGGS^(eW@28~QpuOl=Za5ZO3rA#Q?PJa+9zQp%ym*E6}TF~l(n zD0o-il?X`t7CKdj&*s3Q!08wD(moXTtC|9zF7kqLe~;*aZ_jqAb6_1|Yb<-zz>xT1 z6<>@slu2BR(kckoT^x;}%CYaYE-z*e*&oB~8Q(R{0^G%aGi# zbam|FaO5r8*LFNRknl}M4v-2THg1czvOy^3u*C;t~-oifn6?Hff zD5&%wG&JKn^TpL|{48tSP(0O^YaL6qdMr^nD`iE4yP)D>pPP}*PS@0iW!vm$e;Ntq#kWSM*qlbrNKQ&KKx&v4QLUFc&kCs*%-CMDZ9Yjtr1UfWz zn(!ha@az)IOn4GS)Gc@)o`g`m->{tB#lW2^9gB6_^5TWpjblFf76D@fA#aH3=*7h! zs=9wNmDslIERbmFd=qvei zpe{S<;YvA;TjPXp6O?LMvmA+HT*Z1du`6Ws%&;-xJZ0=&S~itQz^#OMSAm}0zWwCj zHjdpqn-e&frfDv!+|<@^*X>S?2wP%nd?YqY>(_}T!|%S95KZ>`J!vpK+Yxe~OQ(>z z9Al*8_y=vBSo$fTXF(C>?f5G%fUUF-wsdRcr_!1{_EU@sDsfG3Ai@85aKi`F*?_yq z21cjArCK0|cGGtFBQb;s4sqCtsXw@0a@6L0ZoG)wF;7U~_E&fnCd476CathWt_D!_ zuc%ioNtgxjx&6h0XkPs)-Y)S4(5y#t$ZW0GGk~MZR?|sb{1!E|o4?!Gx9~u3T3Q65 zRioTn644NW{O9(_RtAceruL)^g6GS(`D4Dc87q!0VOuWa^=DFs#-CPpN)yf{O7lN| zrm-Aa9Lba&3+%4M6^?x$MCR7GN+#q$Ww{n9CJjWVXU|Jw*D?R@12p|WetuFht(Gup z0pXpxrT+uo9ZQV(ojW0u96-4P-5Hg=YGHV`EU-k~@xV!JuKfN?9oNPMUKD=~G&)Tk zx>s6_T3+>Wr)Hj_+E7%_f#$A7Rz0=a-+ub}CVI20#cJXC?ihyLNAdimdtXYq#E^P< z>dzmkg|4(Lu-;?1jvQ1S*6mH%{lgP=hXh%gRX=Fi z(D#kDB=lAj6yFOE>!3}|V2XYJx}!K7?>bJGy3I^0p@zqLc44tPUi(vzujcw^;ZOx{ z2sz1o`mC&fN`B6;amerN%XJ)Q12il(U&Y0fBWJ( z6<$(-uk=^&VWa;S25iRP7PfExeoevYIvk_x@GwFRS)KDX{QByY8lzh(ShX@VXAA?_ z0y!Wkx>vCfWr|4{f5mh z8<1~-coHiTv|YakKUyFq*kpQ00gE~%iy9_1SFMbtc)R=8iB+JY%lm)jHok4(kknZA zpg6c@RQSwl&X{=X@MlcALYUxpx7$L=5Rt~f9&7!BNGY?`Q5+VbxV%0hxe75?OIymi zVGGV<6w)lw_;}zo@7ncmj5YD(!y(~sq`9(OrP{0i!$K8jvd?3F#%tlu-Vs|xj7Tdt z%|-?(W!jG)Td=+I;z{1=43nqolT+WtemqoBQ(JccDe+=d4CYo-8F52H9Nb>A)} zC*{@5wY^A7;Q!) zDr%Rab3>_2j^<~umW;ovl#c%7jNFcQLksAyOd3*98G0-$7v_cBETmv`4z3~JsY~4r z>NNufbR_MS9A*Z(c7l`m59qZ^0oPDtIPQlB-PJ9Zs@Vv1q~Nh|+5MiLUXW*GL_5_m z8vdU}Mys6#jkdJJbIg&Ag-7a0pD%5<%Vb0VBQ^{#T5ahsgEsFQs2)J^v0m zztmC%c(p5Wn1;R|jp3G|kTS=kv3xIruaoh!`1i~IV#Eua`SzUql)$^^<3H;>B z`Xy_nkvcU0HBXzpQFo!9nw1z0FFe@AD@n-D(SPL-*GJkI+jL!yy>*$i&0^boKxTga zqC*^Z!Lt@Sgy?64aNLGtfyI(|e@5@MYAZh3@8_QV&+YF1pV><5Bb?wlRu`oF_4(}d z+A@4{K;wD$=b=7m>#1JyK2blI;Lqd#Co2hK#E`r7l#QPSXL|d$wtZ{}T^XYx(x-im4YaP;JO~?e)R1_jN>O#XLJs4C~U|Np;|d?IrMyzT+>$U1w6PWJ)0^)!rK}Q>uYpR`(PZU^~1YBOZrMSBwSGAYMieGG?H;9 z$?rR##-dISsyHYj;tk1Pk0H`~hWfjd5U4dHkxwrX45;`riWk=eA}_`A`f0u`pi}fv zaXoD6>O&lo*;5iG9*G60c4amca6B}`zUH@-yti7oS&i%Pr_ZxkQ{Mt6w!2RJ%<9N7 zFB#IPVmR7c7aY9nUuvBj{Z4+(%-&AebZV7QJ|H(O;NB`Ck6xBi>5Ejrq}1iw)}Xcfz9~ z^JBZ!R>^zndmRwcNF1zkQo5`uxDH)=$YvL+lYZDBh-;oZbrHLnwf)HVbRQ1)ys7-c zle9JNy1?mv-^XUD#sx70pXG7;J?`#8v!eZ_ds-9G39XU&(OIo4(c0*Os*ke!ZZ`^G zt^6Xc$*(%6Rr=V-JswGRUgNlPvZ2(rI6M)l*r`k=dYFuGz6E#K`=R|nv17%RE2spN z)@XKNyf(d8ntdsJAjm6*A^+={{GhzY)uXe=_3H}_pqwA!8&tV9`4!VMov(x>pWG|x z=;`Q>x*ODf>1SC*=F_C)jeYFvn|FhzvUISquUh<> ztgGIilu9rYM^3sE!>T}2g4H#<5M=?2i!G;!&UUwi%uJ*_f0?wRG1pV z3w)c_b>uUFLhyz0_&EBW{qxo-yh|BDp|qP1BacUcqP@qDU$e4=_>by9sibu908sG9 z`zPzX{Ylc4%p}^m0NY|O#&hm9f4Yea!5{qu5b0P%Sd=t@e?MPrb)@qur-BVnt zO+`0b10nKK43o4IwNJyhDQFCgL5+kb=(XJnh3i{+7y%}mNu?YQ@^=azFyrFsRuMz{ zW7!q`&gpTw^oH*?o*=iTT7g+D{^LSjbWZ>V2=9yiRrlC=?G@m9HCWaSc_# zvgWhf{qa|XXq~P1kJz) zOAAQZaNE=BQzwr;0_jY^LFb$h9=J!u|gx8jkyoZ!B5r@jekKb=@7tyB8 z1+NZAUS{yIcI5L0beXa^-obN26{6#8o7`gEgt$!T8_>XYQt?(3xa4^DD76srqJ>D$uYiZQfc+@JxUYSLI_CG~sbo5tu8u)Hs|H>!aFA zSf;-bwoWO7g7eTa+ho<9yK(N8!IE+|Ey$6Oq*_V7fjaCCFPCvQwm2;KC_b7;q;-Vi zwbjWSw~X*qRJ7U$DXEK218yy-2CPl#ZnZNoMr7Uu4g}=zUMh_v4dMfjTo%X!W1q}# z^mD2YU);xP9os;Sg{D2`iteTk=qlWdrY)9JMiN2 zd~oONbIkguTGSuI%zNcP4uLngS7nydLnD+=naiLaKF!CAyrRN}#P-aK$NmX8A%?|i zqhapl^_1A52U$fjecf zR6cV3rN~DL1ZzZDlP_afixMkdMOa5hglq%sOU}t++uL{9QBhwKIjbwKta|~JGlF)H z?gIma^2o|sgarzV4=4MCL^423(YqyqOFy&Q*cn9rD#;5^de|J?acF(l?07TJYuoqK zk;v)tbG9qID<&o_>0a~GxqZ^qaGwUs}ktQJqF=!v=77q|7OxrX+~ z4HoayeO7?C9FXI#juL}^2YRidu=dYd!2ZM|NW!m^1}SZ%SUH+M^guo%fRy5m!<)odj2nqN6o{t zb@UC&&2GBE(pMOT)JG*3lt*$}4@>V?f5J;i3!;`Sl?i2hTO8;XP|#@zRg(vDw~M>? z_bz2J9<#!Bdx(2=7Z3G~7zqapNe?;@B3)pDD$&8{ecDl_{;p$x9*wI#E%!CQ+^sz1A6pmweCOqLqJFka zyEb31452+`9NbCj9`+i9)m>^igS1dh zP%fWt7uuexX$9t!xY|rlX~(;?Yni!S)>ek*pRjAUCQUO>!shrrXU42CBS$T#uW`m) z?!>bJsMxHcqv9v%P!R`w|qa?Moy;Vn4Rr2lxHC0C~CmrV{cc?Ul2E_8fu3$D)(DmOzi5 zt{?|QlZ%izQKEax9ah_AALwGYv9Mwn{N>^4N!%lrYgzei5ylgvwK(O)l1vgjVUT~d z-4?IqL9O-NRoO|}^z!n=0XQy%_cZl!MZiree*Ne%zwsy4E`P-4e9RJGCa0-hQ>y1$ zPBu11@~2xh=P#G)PlOj@zB)VlynL`0u%_`LZ5w&oBUl(xr^}Qpfy$U*u~hS=2Yx;RVS+vmeL_ zO6OCzd`b%gBM6^v0S6&d(kAtyn!E^yhnk1&ge4ehXA9B{901T$H~b9;Ex@t1$tSTy z_uk9jG*sY8iv%0tqBe4>2)j>pDhGhq{%Sz5LUeNjWwUZj{gF@kHuK4*(RJ;-H`hmN zu3H{GGMdurCPz_acu(OSG*8QYt+b(x&q-}$@#zL~4o&K2u@W!VBL?3N+<=BuJmPv4 z&t0qqzDE`BTO$E|sOBmU{Pr4NoMe)2y`j@i-8=eU?R{rdQ(L#TdX9Q5$Wf0X9Ti2T zM5Rg1;UGm3Q0XlQNG~BEEtIGTC|wkgt{}bFkVXz7Aco#s5+$@y5&|iZkmTmP_j||q zzA?Tr?zs2&{kO;3YpuQa+-p8_t~KVI&&jcr=WC=kH|uA!kn&AjtT(Zpr{OhPBlB_~ zTm9Q@F|Xs!9M5xj(-a7I2j8B5}kE)ZHSu7y#B(Z#!b9J;sQs&zTvk zB+Ge1OOW9W*+<8m{2hEz?5wtO+6xTm#L#!o4&Fdda=%+ZN%sDp=t2TOV2_rl+BRnI z8bkx(MJN@EKY+X0xTkxc;{*@ltrzEy*6eEfe%VIQ>P^qtRbqL?t6w|Q6pG&D%|&-J zg3M!9Pe<55%2*ETzZ|hfUgO~&p%B7^kD4v=8V$r}k}7q=I(s)n2;(2(Lgo$~3u9#$iir z0?Pfe;>qVRC!{CQI#!zK^&I!*%~^WC2W7U`&=E8txH?L{Q#c_cmKjZ{eAJg#|PM6$zcJwHhCGF_Z zV-SZ5Q@5Gl+_f1?11N@rvpR%ddGGJuP^MNbO;@m7>6r7dLTy5DZ%=fD(Ga`ivIr9B zX*XsYxkK?99G)XOvzMKRI4vC;Fp*}o$? z_B!z>->}3{^x5Ni**0J6qTZNx;aoYXA$@YkZ}1ir+=|}w!&q$#M>%_tMiL^r#o@O@ zmr3PhaFp)jY<`sQ&W!o@-n7Eck(S!+hszFEXY16Z?=M}UWS5>JR`h%eQSX%oIH94e znZCzH%d{sFGWdYq4P%7;P48|f7jLZz4WH|gl+&%3(8BefjHrmKEM`3@lx?}hzV%+- zxy3$Ux66oKjxq_ew27Hgv)lSJlO13&v*JuxFatG0UzAgl)3S3=hf_IuI1h4DTzJIB zh0Z!t>HKSK!!u;=(S^6#w*9YRPV7a#6H@E8EF_K>-rH_lneASv4H*coTOTePeYeu` zC1*YRRobx5{=RH+tb=+!+22;hqP#I8f>Mkn4+1V`hVS1Z*J6uy_W=RQ&QYsU!1>@# zHU%Q?CZRYEE<)S$hfqqBI#%SGSHU(4!@vgT7;-m}AMD_PpOB`u(_t@vYt!ktP2H*1 z$Mcz!hoaY;SUY$3dx|$kX(h2m&5VEYd_H^fJaUU_ijaU<{7v4hh;XESA$seOFp zET?v2c0+rk^%+C8%kSFM+|&TG%fz$Z7abB2;_%3cDS7eayH#k=hXL+3Q_mtwJ)+SPC6x>9(I)h`$)-@G8*`Ru9X zw&!nB>De%8J2_r+Sb%*5dE)gWq|pZ^r`ehRguLI=ev~R6#)%1MgbAJcK7g)A<=bf- z1KQYrc1aHbjHYi*zQhik%2Dvc5!xU)(GshX4~c`aEXwK2VPG}YHU}-}Z`3sM0%G5w ze{;8}I)#6!5E6RYz552rD(VqDYf#(CXp5j7E9KR63cwVs$B>hN)=!C zny&1I9sj4M@C`A3#FPL9ejH%;FCH|9NU?i8b57;Je5tzauZQt%Y?H)PU`&0O9- zm8ZFvmKf^*zmiE{6f%*HTNT0ioaxhgZ?v7GmM7yB!C>x>=IYwzY=z)Di%5}?tva3A zn@b7dJ>3RRu95Wq~bfq zIH?NBX-N&A+g}1tZVvlsYI3x?qm?d?<7{ zgo`z+emhlbQ?fwc|2j#NJ51zv&NelZh$=hJA)?&=H)lgqYDa<5P9;)TPoBs8?Z3RT zH0kIs$tj=p_1BkllE^tRwt(205O=(v)U=b>R&%3Cl6+R>p1N&YGq-J7=@Z+h3tj)< zOrJLv-+E|b*o8l19+}bo5ckP&>1oBl%?vV{PfV4953fiiq@*Xqcz#So(f|Os{t>*M zyFlK0cxMM}sufJa*}?DhE{2FNr@Hm21N0v-3i`Od{7+4Ezel24NX$f_h4^gOmNLH4 z!Jhjmff2{Xznu8pgwz75NeCrySd?yE<@ zcrJ|1)9)@tNkv~z?Ff^-ptX+i)Jf7E7}7;IUrRV9)noK(e`ZOTd)O^4uz;7UVRZHI z!0KyC8whLrY`MBQgvhV(Q6C?TKbtnRZC?0uM@OH~o}0}UuxCfN4K-XQ7D8B=r|LcC zpxthoN=_BvmDKD|b5XJTwsykJ5)TzLpIvaD$At46*c#o@a0Ub7pS!BF4#gje#IEh) zo4abIqDT<17IKJ{CC)|6b*ku}Y~8~=s#3^Ve^%ilYXuaz!$+h`AKJ0DpRAGOxu6pd z+zubr^6hLOG^47l;>R+gb&O2ogEK^XdKbfO5@@N-z; zQe{@%+IC4d_J*LdUb)7OXTjb$0QPe#@}Uq}{_yFljySkr^Kbu3nHGgQ8#V zb*HQ*ymVPkrtfX7al|%cjx2c)dYuMq@?RAx3h0KWABuMt)x4=zpV4~Hv-V=kz{hQr za?+o#cEnS89Oq3*eL;ojoT&{RN5Ack4ruE=r#+N{IM(J4p%xRrKXTrF`m#+YRR ze?rkS3?ZU6;ofD@6rf@Xc7h8|(q(zkHtsn0Zn#Zk@^@BJtC9w+Q%`&q1}Y@*3Y*#_ z*fmki^~WUC9yMO4i>{hebaB};h)AiLId-IV5@4Q(jOl#sS@gDadbFx0c==2V<_S~)hD~sUz<1sMofs?JD zc~8A0_NPkCx2wPo4i0KX0}AhI-U3~d$BC)&v#DDrofkxJmy&VC=ze~7D{pcbZ^GYy zKooDiki6c4+eD(_@IUZDZvpvyBrbSJ*DV^p{i94M`*o+Q#{8A1i5u%k?Y5zZ5A1lI zG{C4QR@;I$V9H6Y$6`5h&iMTVii)u4*3CGOj}vz>ipZ71IOUbGzav5Lo8uXkp?ra* zj1J6XyZ~$&(e6HY9(Xh7?C=3~#J5kmGVim`e|qU}n^0rZa$`Yh?Rl7bv*d_%Z5yaQ z1gaR)GI-u&w%@wl=5fSZ(BIWA)=gH^u2s(~J}%ilFuZ3Kbnn=~Gh_L#EZK;LU$>7m zRnJA~@~>z_6y_5n3W+-$9wEa|dpK%A7+MoLY6dM=OsPDbmdYCOrbLU~GimM1P&7*A zRNLpyQD&Lg+Fnj+$r!dmGx}x<1!@msvHlviFW^DBJ$KU!Au5fOnW?M$thUlLOr_h9 zZug`ab#0MSHF-8QOw-0DZIt9J7Ut4j_MJFxdSQQ-N%S=PU@_hHRoxEbLer7=Q9>^u z8Hbt&QZ5>6|XFc8GaCTbOu;>i3Q7Y2enomg;p$Mv4X zV%q&R9a#q1YOX;e2Iqi8_A*szV17R@etH+zlRwVLxdEq0gYd-Ia=VqCmH+ES8cZ*o4R@IC%*HnC@->q(L%0FM@_V|Qr60nY3ho*)eZ0_8cVti zdaa}Y!`kQPp7RS2b})tP@1Fk?keV(=BSw6@29Kt6j>Lamk90T!Ix`fY^-ZHzGDor3 zN$V}WQ;PZOGc_f`>`H{)6xg->6G^%H>F_6&k|r zo<5{h=plMx<+VRYgXkqdnCFkJ#>->s)an!QLpQV$ZX7TC@;P~&u*my)pgObORgWx=RM-nJY2S%#jtxkv!(G1XDzemD80pTbLoUpp@?R*dDrlLE zp)SV*hY0s3tuPKE5(7d|xzX7LzMF`;rL5X*_1p&`;;N3pey7c%7Y3@9pZv)gioDi; zRyWri@~Bmd);Z3>MbnubFAtE}wgeGocg&x9szZYOhIV&dtt@aO{y&$m(}#WBwDf zKLNnn>gtu_Hx$G6-6Qe}=a{o)bSK7&NWUNP`?i<<1en?S)Vwf^lpL7$l^cT9`fxIp zp}`EKC5uU!gtmWn@FC7yi{Awd`Qi_d-rtJx%}A9McV` z;`+NrnpClrCM0U^3$bk`W!((2&v}H>*c*^w50Q-+pKdqzlqtvDOeUm@Vk-WfC9K#% zN@RAYj=YU?V}onw&)=0B^rPzCLwyX~*b|?$nk*HP_bGzTA+P%wT)G3h3)(>cAf<)! zw@cTI2kDci;Kqcd<|MdM1p6)DD!P08I&Iz(b`Qs^s`|F8F=pihikmOs*>xq{RhyCxrAn(`ksIA5WR+HfDupzo>1Dn3 z=(=6TD5DfNgkd(Jjr9kPN=x-!+g-23-1#fml>y{lYTwTA3Lao80<6&!@3nl_1PQrX zmxH}(U}J;M)|MyHvTE0q)xqH^k@WYE$iVvf-F0txRK!DlMzzDF&K&e_O8oUBBK>>p z-2$LPOfTz9xAS$0cSy4gAysP3d(H&;Z2ae5>}}PK2<{rpK8MDDG{#6K=&QK~!chk}Yt#_Cs!2=|VKg`ahr&vVx5IKI}giq!q9 zvnImgOwyEsqL7DK-?23!|6`d(FqD>zmZ~by+dF&06|gzoUJ3nTg{WK(UjoQaUV&l* z6>tA|=^{Gdpq2?n2HvMUUv1cZeX36lN*gC59Hw*_Uz?hfl%=b%NBA*Ep&%?s&{v2UdQU3Vwj|rifw~8@pd6z zUrJ+N#!f8BPlAE_eb)@-M22Vy{q4K$CeY~!J&S=d#?nNroFe)*e{4fOUCqy@uXcj9 zAKtZ~x+hG0{QKso)k}>gfV_sbmi-x-3u?aKVPty5_*hqULgZG9fG&1K{~mM8@rUwN znF5!-5}{1_`|@*G9G{({mO(kG<7fH9J8NIwa%de_U?t4hjEnM{!Fw<3u)Q3Z8B)}2EiSue`Ws|tFd%O&-G+|m zTymFpR)~x;ar61}36zFsnStO>-E4aA;|eW<#$Myavw$t-3}{lcH}gw`ft=C6I4Q6b zom*4~+7muuUDlDDT6hc0+h}Q$>=aFxDpCz2aBw#D(Bj*#zt&?}KYbHClW#z`Ui7=0 z*IjpRZo0%cEi5V~o~yNULJt?2cfV*hQ?2&%Q~SBqg}-<64;!Fh4KB2*eWR7Mw9v=s z@`z&VJ|teNN?nK8cKlL?o$a$Z{1hB4JKYD-{Pm4FtVrstsgp_bDMbzzh=y3LM_3ej z^r62srRZ>HXS_!`b1EYRdNxdRPZ!lG{}j~qJ<-!1`+8|f1avEG(B`W7x4qAZOjVcJ zDsCe)%;imFmy_wGi;F**Lh!|!_TW`V(Cys4rMzdt^U@Zg9YX1yc#8@@Jj z%`o0q5NCtvqLR0%(Xc*wxF` zw_>B&{y3^_Q+31Q3siajtePX;AmJ>o1O9ot_;4B_bWL%Pe}Lb&wupUV`sQ5PJRjG; zN5o>nr^^RCqM-aHcaF%3-e05R-CegdhVyDee(qeigL7@P)`kEdKr)ge%~`)ppz`8D zx;ngw2S!B?GIE=>Rq9N+6GshSmL!CNBT*gBzi&h+ zL!S63X#JME9(r|cROy4`9oaJy(~TQHN4L8o3=C|X^SHfL{tn-#-cxE2dp}_xa>c!Y zj!jGkBMmc}u1s4))ej|t(lgI*aCQ)<1-6WAC z=ic0PG9e7NbWtrS_m)lb#QgA9h?U``nOOT%mKa)0;Ll8&Ex+vRtf#>D#QuB{w_{jZ z9<#Ge&NhwkpmD1U?Q{@Z6o8PA&U9~IPp)t?@y^aKF+vr z)b&}AVR+}=gGMd4(V*kK9&*)ZtQbbIY#psL3CeO0q|`5>yjj)M&mCjClsVPD{zh7U z%hN6jaq{p?i5w_BX2WJ|=c-vM3v9fx_mTFlWNF~TPN^Gt^{mml8gTFHTJHWH5m{=i zdFa4%Wr24cJUDPY+4Bm~bX-NE3W`C-7)SZV@TQZR#aJ5^_oG*8F)6X6I0U(KWby6t zQ~RDD$)rPO_Wb^X+}NzLvMe`LxY6xF+-lgDV;LzZwf7jush~hAB~092u-1N@Jexk4 zA4A?d93gttH`E%_tTXbv{Do}bO&$JdI6aWTBz80ho9i3KXv3bY9Y%ObNwBvWAnc}%JByze#JsVXbnh#YhJ{gsM|z^TIpi98_PpWQ;JkqM)fcn_huU^ zufWYxtiiTu4;$`?%$)|35pj>=AC_r7y)B5m)|EU+PELH9!u>M*!|Qj&c3EuniTIe< zo@7Z$)9w#0w8oiUYe}K&*AV==JsPqVXtzBqoTUK0aqDBn;>eumB9poq@t^1#?%%y# zni-<_mOlNr%!T)&o8Y-40yVZA)Eo|!T~+=!hzjt4x{u4+bXJf!OBXf#@1cZ1H~wo# zc}P5N>{$>pS;V843kIylS&}p+UY%lRbeu~cy|kjj|8zyr481rJZl?Mvw&OGS@Mk)G zN9>fO0GghIj8zLQOjkbw$PNdu2mg2;9dcB#pgxK`PFb-$tGQ~zw*FHSm>aQcY!Th{ z^PkzRt7h_jw99jc^_)jnLL0@!a6Es(p}&}5ka*>h;D6mc8vUT?gmLAsQG^ICahjW& zEWo^i`>L51Ld%xTh2naxNAH% zY#S;qq2)6b_RC|G zoQ^ORy#4{?{{z7P3Dbh+|M~~~8`J-{^FQMJUupdR?EU|2ZLAUndo+V0O3euO~1612raGd;kCd literal 0 HcmV?d00001 diff --git a/samples/admin-activation-missing-ip.txt b/samples/admin-activation-missing-ip.txt new file mode 100644 index 0000000..ca9a4b2 --- /dev/null +++ b/samples/admin-activation-missing-ip.txt @@ -0,0 +1,21 @@ +Subject: Activate user account + +Hello, + +The account owned by "ExampleApplicant" has been deactivated or newly created. Check the account details before deciding whether to activate it. + +Username: ExampleApplicant +Email address: ExampleApplicant@example.org + +Registration IP: Not available + +Registration date: Mon Oct 26, 2026 3:26 pm (UTC) + +View the user's profile: +https://forum.example.org/memberlist.php?mode=viewprofile&u=85 + +Activate the account: +https://forum.example.org/ucp.php?mode=activate&u=85&k=EXAMPLE_ACTIVATION_KEY + +-- +Thanks, The Management