Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84c7b31dfb | ||
| 1499347b7b | |||
| 1d39bc03d8 | |||
|
|
8f45736b1b | ||
|
|
8385197f11 | ||
|
|
ca16e3ca39 | ||
|
|
c194661fa3 | ||
|
|
e539c60e9b | ||
|
|
bea25d8396 | ||
|
|
b4506a0a3e | ||
|
|
54b96ef059 | ||
|
|
7de47160b6 | ||
|
|
e81002c528 | ||
|
|
efcca69d43 | ||
|
|
dd477c1b83 | ||
|
|
c86f38672a | ||
|
|
b9e16f186d | ||
|
|
05a7a2967f | ||
|
|
da573c184e | ||
|
|
0dbba36345 | ||
|
|
08673a4399 | ||
|
|
e4bd92ca63 | ||
|
|
08797ab5a8 | ||
|
|
fd9836c7ae | ||
|
|
3a8202bd7a | ||
|
|
39fe417104 | ||
|
|
36b9b3e35f | ||
|
|
780cfc6a7e | ||
|
|
fc13088c62 | ||
|
|
12572b563d | ||
|
|
6ea4aee0a7 | ||
|
|
546e9ba882 | ||
|
|
9ebe30c40a | ||
|
|
16af27a3f4 | ||
|
|
3c4d856d60 | ||
|
|
958af7769f | ||
|
|
7b1a68aad6 | ||
|
|
651bb19d05 | ||
|
|
456d44c677 | ||
|
|
70f863ac68 | ||
|
|
d979e45fa3 | ||
|
|
85ed8815f1 | ||
|
|
8b9447fc63 | ||
|
|
6a4a4e09df | ||
|
|
8f800610cf | ||
|
|
6cf3d3da3a | ||
|
|
7627e6bc23 | ||
|
|
745b8cc38f | ||
|
|
52923e1278 | ||
|
|
5577699dae | ||
|
|
eb8175fb7c | ||
|
|
3e803f65a2 | ||
|
|
ad97f4bfd0 | ||
|
|
181ac6b27c | ||
|
|
ae7b905824 | ||
|
|
5aed4c9947 | ||
|
|
46eba62185 | ||
|
|
ca790d72de | ||
|
|
af54e4b099 | ||
|
|
da17fb896b | ||
|
|
afc4f54594 | ||
|
|
7795a0fbf9 | ||
|
|
6fa82d6ba5 | ||
|
|
fff6c6b95a | ||
|
|
92d0f40e3c | ||
|
|
33555a94d4 | ||
|
|
5f6f2eb095 | ||
|
|
2e09a92bc3 | ||
|
|
072ef65fdf | ||
|
|
cb7936bf71 | ||
|
|
46f58d694f | ||
|
|
07d1366825 | ||
|
|
b8a751c4ff | ||
|
|
312026b059 | ||
|
|
f54d9c9618 | ||
|
|
8176ab9c26 | ||
|
|
dbb50903d9 | ||
|
|
fa0d0c1684 | ||
|
|
0342dd4975 | ||
|
|
98e0f16836 | ||
|
|
f1d2e3193a | ||
|
|
f30f72099b | ||
|
|
67fc6b8b66 | ||
|
|
abc3c9ea5f | ||
|
|
148052a6d0 | ||
|
|
7bb10e55d7 | ||
|
|
ac32196fc0 | ||
|
|
ebc4324ecd | ||
|
|
c0a92bb436 | ||
|
|
652d1aaa1f | ||
|
|
6f86e64442 | ||
|
|
080a2ee591 | ||
|
|
4a20b77edb | ||
|
|
89791e1f49 | ||
|
|
c2380e630b | ||
|
|
a062f5ebb5 | ||
|
|
f164ccfd05 | ||
|
|
34fd9ca6f6 | ||
|
|
6ba63dfe50 | ||
|
|
e79ec4e45b | ||
|
|
f1bd9679da | ||
|
|
68164414c2 | ||
|
|
d8ea3d5539 | ||
|
|
a4dc73c6a7 | ||
|
|
f796334126 | ||
|
|
d65ad79709 | ||
|
|
43a9ed178e | ||
|
|
cc68ab1521 | ||
|
|
ea21509f2a | ||
|
|
24a9584d6f | ||
|
|
bcea012af0 | ||
|
|
a437a107ba | ||
|
|
8b2798e412 | ||
|
|
c28305509f | ||
|
|
8a8bca1ae5 | ||
|
|
28d7841af5 | ||
|
|
707abf2a81 | ||
|
|
c90efaa00b | ||
|
|
e4aeef2e07 | ||
|
|
e19b33a842 | ||
|
|
d540de77ff | ||
|
|
913065548b | ||
|
|
683b02027b | ||
|
|
7111e9e759 | ||
|
|
f5f3fbdf90 | ||
|
|
fcdff0ccc7 | ||
|
|
0e0b00dea2 | ||
|
|
c76942a6cd | ||
|
|
17044fe501 | ||
|
|
f644b80c0e | ||
|
|
dd2d8e10a4 | ||
|
|
8ca4d3bf56 | ||
|
|
829615ec5d | ||
|
|
6285d0bffd | ||
|
|
a3ccfb5a8f | ||
|
|
08f6c86773 | ||
|
|
8b527820e9 | ||
|
|
b3adf0842f | ||
|
|
deac908a77 | ||
|
|
f1874e3fe7 | ||
|
|
5096826621 | ||
|
|
7a97816d1c | ||
|
|
6849c47fd9 | ||
|
|
ecbadcd866 | ||
|
|
9ebacf0d61 | ||
|
|
ab7bae56b5 | ||
|
|
d93dd2be03 | ||
|
|
f7b650db3e | ||
|
|
4da9b48776 | ||
|
|
d667518d1f | ||
|
|
5ad75ce989 | ||
|
|
dbadde3333 | ||
|
|
b499688318 | ||
|
|
6cfbeab7f0 | ||
|
|
b10f3625c2 | ||
|
|
f28fb5bb58 | ||
|
|
1b424b4348 | ||
|
|
6705312240 | ||
|
|
05ea494fa6 | ||
|
|
5a704f2717 | ||
|
|
01edffa66b | ||
|
|
9367409f36 | ||
|
|
bbf28eed57 | ||
|
|
ec71910156 | ||
|
|
f700593730 | ||
|
|
9db4f48593 | ||
|
|
2b9c43858e | ||
|
|
95716183d7 | ||
|
|
40e7f5664e | ||
|
|
ea62869891 | ||
|
|
057c88ebc5 | ||
|
|
41186b613e | ||
|
|
3e41e57888 | ||
|
|
0925fca08a | ||
|
|
67dc78a48c | ||
|
|
1b7d90a92b |
8
.gitattributes
vendored
Normal file
8
.gitattributes
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
* text=auto
|
||||||
|
|
||||||
|
*.sql text
|
||||||
|
*.php text
|
||||||
|
*.inc text
|
||||||
|
*.js text
|
||||||
|
*.md text
|
||||||
|
*.php.dist text
|
||||||
24
.github/workflows/php-lint.yml
vendored
Normal file
24
.github/workflows/php-lint.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: PHP Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
php-version: ['8.2', '8.3', '8.4']
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Setup PHP ${{ matrix.php-version }}
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php-version }}
|
||||||
|
|
||||||
|
- name: Lint PHP files
|
||||||
|
run: find . -name '*.php' -not -path './vendor/*' -exec php -l {} + | (! grep -v "No syntax errors")
|
||||||
25
.github/workflows/phpcs.yml
vendored
Normal file
25
.github/workflows/phpcs.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: PHP CodeSniffer
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
phpcs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.2'
|
||||||
|
tools: composer
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer config allow-plugins.roundcube/plugin-installer true && composer install --prefer-dist --no-progress
|
||||||
|
|
||||||
|
- name: Run PHP CodeSniffer
|
||||||
|
run: composer phpcs
|
||||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -1 +1,21 @@
|
|||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Editors
|
||||||
*.swp
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Plugin config
|
||||||
|
config.inc.php
|
||||||
|
|
||||||
|
# Composer
|
||||||
|
/vendor/
|
||||||
|
|||||||
47
.phpcs.xml
Normal file
47
.phpcs.xml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<ruleset name="NextBridge">
|
||||||
|
<description>PSR-12 coding standard for NextBridge RoundCube plugin</description>
|
||||||
|
|
||||||
|
<!-- Use PSR-12 as base with RoundCube-specific exclusions -->
|
||||||
|
<rule ref="PSR12">
|
||||||
|
<!-- Allow longer lines for readability -->
|
||||||
|
<exclude name="Generic.Files.LineLength"/>
|
||||||
|
<!-- RoundCube plugins don't use namespaces -->
|
||||||
|
<exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace"/>
|
||||||
|
<!-- RoundCube plugins use lowercase class names by convention -->
|
||||||
|
<exclude name="Squiz.Classes.ValidClassName.NotCamelCaps"/>
|
||||||
|
<!-- RoundCube plugins can have side effects (require_once) -->
|
||||||
|
<exclude name="PSR1.Files.SideEffects.FoundWithSymbols"/>
|
||||||
|
<!-- RoundCube hooks use snake_case method names by convention -->
|
||||||
|
<exclude name="PSR1.Methods.CamelCapsMethodName.NotCamelCaps"/>
|
||||||
|
</rule>
|
||||||
|
|
||||||
|
<!-- Allow @url/@link in class doc comments -->
|
||||||
|
<rule ref="Squiz.Commenting.ClassComment">
|
||||||
|
<exclude name="Squiz.Commenting.ClassComment.TagNotAllowed"/>
|
||||||
|
</rule>
|
||||||
|
<rule ref="Squiz.Commenting.FunctionComment">
|
||||||
|
<exclude name="Squiz.Commenting.FunctionComment.MissingReturn"/>
|
||||||
|
</rule>
|
||||||
|
|
||||||
|
<!-- Line length: warn at 120, no hard limit -->
|
||||||
|
<rule ref="Generic.Files.LineLength">
|
||||||
|
<properties>
|
||||||
|
<property name="lineLimit" value="120"/>
|
||||||
|
<property name="absoluteLineLimit" value="0"/>
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
|
||||||
|
<!-- Require class documentation -->
|
||||||
|
<rule ref="Squiz.Commenting.ClassComment"/>
|
||||||
|
|
||||||
|
<!-- Require function documentation with @param and @return -->
|
||||||
|
<rule ref="Squiz.Commenting.FunctionComment"/>
|
||||||
|
|
||||||
|
<!-- Files to check -->
|
||||||
|
<file>ident_switch.php</file>
|
||||||
|
<file>lib/</file>
|
||||||
|
|
||||||
|
<!-- Exclude vendor directories -->
|
||||||
|
<exclude-pattern>*/vendor/*</exclude-pattern>
|
||||||
|
</ruleset>
|
||||||
661
LICENSE
Normal file
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
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
|
||||||
|
them 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.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey 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;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If 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 convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero 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 that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
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.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
213
README.md
213
README.md
@@ -1,2 +1,211 @@
|
|||||||
# ident_switch
|
# Ultimate Identity Switcher for Roundcube
|
||||||
ident_switch plugin for Roundcube
|
|
||||||
|
[](https://github.com/Gecka-Apps/roundcube-ident_switch/releases)
|
||||||
|
[](https://github.com/Gecka-Apps/roundcube-ident_switch/actions/workflows/phpcs.yml)
|
||||||
|
[](https://github.com/Gecka-Apps/roundcube-ident_switch/actions/workflows/php-lint.yml)
|
||||||
|
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||||
|
[](https://www.php.net)
|
||||||
|
[](https://roundcube.net)
|
||||||
|
|
||||||
|
Switch between multiple IMAP accounts within a single Roundcube session.
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
**ident_switch** is a Roundcube plugin that turns identities into full account shortcuts. Configure each identity with its own IMAP, SMTP and Sieve server, then switch between accounts with a single click from the toolbar dropdown. Aliases, notifications, and domain preconfig are all built in.
|
||||||
|
|
||||||
|
*Originally created by Boris Gulay. This fork is a major rewrite by [Gecka](https://gecka.nc) — modernized for PHP 8.2+, restructured codebase, and packed with new features: alias identities, Sieve support, background mail notifications, domain preconfig, connection testing, and per-protocol security.*
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Multi-Account Switching
|
||||||
|
|
||||||
|
- One-click account switching from a toolbar dropdown
|
||||||
|
- Each identity can be linked to a separate IMAP/SMTP server with its own credentials
|
||||||
|
- Automatic special folder mapping (Sent, Drafts, Junk, Trash) per account
|
||||||
|
- Encrypted password storage in database
|
||||||
|
|
||||||
|
### Alias Identities
|
||||||
|
|
||||||
|
- Link identities as aliases of any configured account (not just the primary)
|
||||||
|
- Aliases share the parent account's IMAP inbox, SMTP and Sieve configuration
|
||||||
|
- Compose "From" dropdown automatically filtered to show only the active account and its aliases
|
||||||
|
|
||||||
|
### Sieve (ManageSieve) Support
|
||||||
|
|
||||||
|
- Per-account Sieve server configuration with independent host, port and credentials
|
||||||
|
- Three authentication modes: same as IMAP, none, or custom credentials
|
||||||
|
- Sieve section only appears when the `managesieve` plugin is active
|
||||||
|
|
||||||
|
### New Mail Notifications
|
||||||
|
|
||||||
|
- Background mail checking across all secondary accounts
|
||||||
|
- Unread count badge in the account switcher dropdown
|
||||||
|
- Per-account notification settings: favicon, sound, desktop notifications
|
||||||
|
- Optional round-robin mode to reduce IMAP connections with many accounts
|
||||||
|
- Integrates with Roundcube's `newmail_notifier` plugin
|
||||||
|
|
||||||
|
### Domain Preconfig
|
||||||
|
|
||||||
|
- Pre-fill server settings based on the identity's email domain
|
||||||
|
- Wildcard domain support (`*`) for catch-all defaults
|
||||||
|
- Lock preconfigured fields as read-only to prevent user changes
|
||||||
|
- Restrict account creation to preconfigured domains only (`preconfig_only` mode)
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Per-protocol security selection: None, STARTTLS, or SSL/TLS (IMAP, SMTP, Sieve independently)
|
||||||
|
- Connection testing on save (IMAP, SMTP, Sieve) to catch misconfigurations early
|
||||||
|
- Passwords encrypted using Roundcube's built-in encryption
|
||||||
|
- Warning displayed when selecting unencrypted connections
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- PHP 8.2+
|
||||||
|
- Roundcube 1.6+
|
||||||
|
- MySQL, PostgreSQL, or SQLite
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### With Composer (recommended)
|
||||||
|
|
||||||
|
Navigate to your Roundcube installation directory and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require gecka/ident-switch
|
||||||
|
```
|
||||||
|
|
||||||
|
The [roundcube/plugin-installer](https://github.com/roundcube/plugin-installer) will automatically place the plugin in the correct `plugins/` directory, initialize the database schema, and offer to enable it.
|
||||||
|
|
||||||
|
> **Don't have Composer?** See [getcomposer.org](https://getcomposer.org/download/) for installation instructions.
|
||||||
|
|
||||||
|
> **Running as root on a VPS?** Roundcube files are typically owned by `www-data`. Run Composer as the web server user to avoid permission issues:
|
||||||
|
> ```bash
|
||||||
|
> sudo -u www-data composer require gecka/ident-switch
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
1. Place this plugin folder into the plugins directory of Roundcube:
|
||||||
|
```bash
|
||||||
|
cd /path/to/roundcube/plugins/
|
||||||
|
git clone https://github.com/Gecka-Apps/roundcube-ident_switch.git ident_switch
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add `ident_switch` to `$config['plugins']` in your Roundcube config:
|
||||||
|
```php
|
||||||
|
$config['plugins'] = array('ident_switch', /* other plugins */);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Initialize the database schema:
|
||||||
|
```bash
|
||||||
|
bin/updatedb.sh --package=ident_switch --dir=plugins/ident_switch/SQL
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Copy the sample configuration file and edit it to match your environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp plugins/ident_switch/config.inc.php.dist plugins/ident_switch/config.inc.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Domain Preconfig
|
||||||
|
|
||||||
|
Pre-fill server settings per email domain so users don't have to enter them manually:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$config['ident_switch.preconfig'] = [
|
||||||
|
'domain.tld' => [
|
||||||
|
'imap_host' => 'ssl://mail.domain.tld:993',
|
||||||
|
'smtp_host' => 'tls://mail.domain.tld:587',
|
||||||
|
'sieve_host' => 'tls://mail.domain.tld:4190',
|
||||||
|
'user' => 'email', // 'email' = full address, 'mbox' = local part
|
||||||
|
'readonly' => true, // lock fields in UI
|
||||||
|
],
|
||||||
|
'*' => [ // wildcard: default for unlisted domains
|
||||||
|
'imap_host' => 'ssl://mail.example.com:993',
|
||||||
|
'smtp_host' => 'tls://mail.example.com:587',
|
||||||
|
'user' => 'email',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Setting | Default | Description |
|
||||||
|
|---------|---------|-------------|
|
||||||
|
| `ident_switch.check_mail` | `true` | Enable background mail checking across secondary accounts |
|
||||||
|
| `ident_switch.round_robin` | `false` | Check one account per refresh cycle instead of all at once |
|
||||||
|
| `ident_switch.hide_notifier_warning` | `false` | Hide the warning when `newmail_notifier` plugin is not active |
|
||||||
|
| `ident_switch.preconfig_only` | `false` | Restrict account creation to preconfigured domains only |
|
||||||
|
| `ident_switch.debug` | `false` | Log SMTP/Sieve routing and alias resolution to `logs/ident_switch.log` |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Create a new identity in **Settings > Identities**
|
||||||
|
2. Choose the account mode:
|
||||||
|
- **Primary account** — default identity, no server config needed
|
||||||
|
- **Separate account** — configure IMAP, SMTP, and optionally Sieve servers
|
||||||
|
- **Alias of [account]** — link to an existing account's servers
|
||||||
|
3. For separate accounts, fill in server details (or let preconfig handle it)
|
||||||
|
4. The account switcher dropdown appears in the toolbar once at least one separate account is configured
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer update gecka/ident-switch
|
||||||
|
```
|
||||||
|
|
||||||
|
Database migrations are applied automatically by the Roundcube plugin installer.
|
||||||
|
|
||||||
|
## Migrating from the Original Plugin
|
||||||
|
|
||||||
|
If you are upgrading from `boressoft/ident_switch`, `toteph42/identity_switch`, or another fork:
|
||||||
|
|
||||||
|
> **Important:** This version (5.x) requires a **v4.x database schema** as its starting point. If you are running v1.x–v3.x, you must first upgrade to v4.x (`boressoft/ident_switch`) before migrating to this fork. This applies to all supported databases (MySQL, PostgreSQL, SQLite).
|
||||||
|
|
||||||
|
### With Composer
|
||||||
|
|
||||||
|
1. Replace the old package in `composer.json` with `gecka/ident-switch` and run `composer update`
|
||||||
|
2. Database migrations are applied automatically
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
1. Replace the plugin files in `plugins/ident_switch/`
|
||||||
|
2. Run the database migration:
|
||||||
|
```bash
|
||||||
|
bin/updatedb.sh --package=ident_switch --dir=plugins/ident_switch/SQL
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
If you use `config.inc.php`, update it to the new format — the `'host'` key has been replaced by separate `'imap_host'` and `'smtp_host'` keys (old `'host'` key still works as fallback).
|
||||||
|
|
||||||
|
## Localization
|
||||||
|
|
||||||
|
Available in 7 languages: English, French, German, Italian, Dutch, Russian, Slovenian.
|
||||||
|
|
||||||
|
## Version Compatibility
|
||||||
|
|
||||||
|
| Version | Roundcube | PHP |
|
||||||
|
|---------|-----------|-----|
|
||||||
|
| 5.x | 1.6+ | 8.2+ |
|
||||||
|
| 4.x | 1.3 — 1.5 | 7.x — 8.1 |
|
||||||
|
| 1.x — 3.x | 1.1 — 1.3 | *discontinued* |
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This plugin is released under the [GNU Affero General Public License Version 3](https://www.gnu.org/licenses/agpl-3.0.html).
|
||||||
|
|
||||||
|
Original code by Boris Gulay licensed under GPL-3.0+. New contributions licensed under AGPL-3.0+.
|
||||||
|
|
||||||
|
## Authors
|
||||||
|
|
||||||
|
- **Boris Gulay** — Original developer (2016–2022)
|
||||||
|
- **Christian Landvogt** — Special folders support
|
||||||
|
- **Gergely Papp** — Bug fixes
|
||||||
|
- **Laurent Dinclaux** — Current maintainer ([Gecka](https://gecka.nc))
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Built with 🥥 and ☕ by [Gecka](https://gecka.nc) — Kanaky-New Caledonia 🇳🇨
|
||||||
|
|||||||
90
SQL/mysql.initial.sql
Normal file
90
SQL/mysql.initial.sql
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `ident_switch`
|
||||||
|
(
|
||||||
|
`id`
|
||||||
|
int UNSIGNED
|
||||||
|
NOT NULL
|
||||||
|
AUTO_INCREMENT,
|
||||||
|
`user_id`
|
||||||
|
int UNSIGNED
|
||||||
|
NOT NULL,
|
||||||
|
`iid`
|
||||||
|
int UNSIGNED
|
||||||
|
NOT NULL
|
||||||
|
UNIQUE,
|
||||||
|
`parent_id`
|
||||||
|
int UNSIGNED
|
||||||
|
DEFAULT NULL,
|
||||||
|
`username`
|
||||||
|
varchar(64),
|
||||||
|
`password`
|
||||||
|
varchar(255),
|
||||||
|
`imap_host`
|
||||||
|
varchar(64),
|
||||||
|
`imap_port`
|
||||||
|
int
|
||||||
|
CHECK(`imap_port` > 0 AND `imap_port` <= 65535),
|
||||||
|
`imap_delimiter`
|
||||||
|
char(1),
|
||||||
|
`label`
|
||||||
|
varchar(32),
|
||||||
|
`flags`
|
||||||
|
int
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT 0,
|
||||||
|
`smtp_host`
|
||||||
|
varchar(64),
|
||||||
|
`smtp_port`
|
||||||
|
int
|
||||||
|
CHECK(`smtp_port` > 0 AND `smtp_port` <= 65535),
|
||||||
|
`smtp_auth`
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT 1,
|
||||||
|
`smtp_username`
|
||||||
|
varchar(64),
|
||||||
|
`smtp_password`
|
||||||
|
varchar(255),
|
||||||
|
`sieve_host`
|
||||||
|
varchar(64),
|
||||||
|
`sieve_port`
|
||||||
|
int
|
||||||
|
CHECK(`sieve_port` > 0 AND `sieve_port` <= 65535),
|
||||||
|
`sieve_auth`
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT 1,
|
||||||
|
`sieve_username`
|
||||||
|
varchar(64),
|
||||||
|
`sieve_password`
|
||||||
|
varchar(255),
|
||||||
|
`notify_check`
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT 1,
|
||||||
|
`notify_basic`
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL,
|
||||||
|
`notify_sound`
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL,
|
||||||
|
`notify_desktop`
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL,
|
||||||
|
`drafts_mbox`
|
||||||
|
varchar(64),
|
||||||
|
`sent_mbox`
|
||||||
|
varchar(64),
|
||||||
|
`junk_mbox`
|
||||||
|
varchar(64),
|
||||||
|
`trash_mbox`
|
||||||
|
varchar(64),
|
||||||
|
UNIQUE KEY `user_id_label` (`user_id`, `label`),
|
||||||
|
CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `fk_identity_id` FOREIGN KEY (`iid`) REFERENCES `identities`(`identity_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
PRIMARY KEY(`id`),
|
||||||
|
INDEX `IX_ident_switch_user_id`(`user_id`),
|
||||||
|
INDEX `IX_ident_switch_iid`(`iid`),
|
||||||
|
INDEX `IX_ident_switch_parent_id`(`parent_id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `system` (`name`, `value`) VALUES ('ident_switch-version', '2026021000');
|
||||||
86
SQL/mysql/2026021000.sql
Normal file
86
SQL/mysql/2026021000.sql
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
-- Upgrade from v4.x to v5.x
|
||||||
|
-- Increase password column for encrypted values
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
MODIFY `password` varchar(255);
|
||||||
|
|
||||||
|
-- Add unique constraint on identity ID
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD UNIQUE (`iid`);
|
||||||
|
|
||||||
|
-- Add Sieve support
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD `sieve_host` varchar(64) AFTER `smtp_auth`;
|
||||||
|
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD `sieve_port`
|
||||||
|
int
|
||||||
|
CHECK(`sieve_port` > 0 AND `sieve_port` <= 65535)
|
||||||
|
AFTER `sieve_host`;
|
||||||
|
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD `sieve_auth`
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT 1
|
||||||
|
AFTER `sieve_port`;
|
||||||
|
|
||||||
|
-- Add custom SMTP/Sieve credentials
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD `smtp_username`
|
||||||
|
varchar(64)
|
||||||
|
DEFAULT NULL
|
||||||
|
AFTER `smtp_auth`;
|
||||||
|
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD `smtp_password`
|
||||||
|
varchar(255)
|
||||||
|
DEFAULT NULL
|
||||||
|
AFTER `smtp_username`;
|
||||||
|
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD `sieve_username`
|
||||||
|
varchar(64)
|
||||||
|
DEFAULT NULL
|
||||||
|
AFTER `sieve_auth`;
|
||||||
|
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD `sieve_password`
|
||||||
|
varchar(255)
|
||||||
|
DEFAULT NULL
|
||||||
|
AFTER `sieve_username`;
|
||||||
|
|
||||||
|
-- Add notification settings
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD `notify_check`
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT 1
|
||||||
|
AFTER `sieve_password`;
|
||||||
|
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD `notify_basic`
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL
|
||||||
|
AFTER `notify_check`;
|
||||||
|
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD `notify_sound`
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL
|
||||||
|
AFTER `notify_basic`;
|
||||||
|
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD `notify_desktop`
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL
|
||||||
|
AFTER `notify_sound`;
|
||||||
|
|
||||||
|
-- Add alias support: parent_id links an alias identity to a parent account
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD `parent_id`
|
||||||
|
int UNSIGNED
|
||||||
|
DEFAULT NULL
|
||||||
|
AFTER `iid`;
|
||||||
|
|
||||||
|
ALTER TABLE `ident_switch`
|
||||||
|
ADD INDEX `IX_ident_switch_parent_id` (`parent_id`);
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
CREATE TABLE ident_switch
|
|
||||||
(
|
|
||||||
id
|
|
||||||
serial
|
|
||||||
PRIMARY KEY,
|
|
||||||
user_id
|
|
||||||
integer
|
|
||||||
NOT NULL
|
|
||||||
REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE,
|
|
||||||
iid
|
|
||||||
integer
|
|
||||||
NOT NULL
|
|
||||||
REFERENCES identities(identity_id) ON DELETE CASCADE ON UPDATE CASCADE
|
|
||||||
UNIQUE,
|
|
||||||
username
|
|
||||||
varchar(64)
|
|
||||||
NOT NULL,
|
|
||||||
password
|
|
||||||
varchar(64),
|
|
||||||
host
|
|
||||||
varchar(64)
|
|
||||||
NOT NULL,
|
|
||||||
port
|
|
||||||
integer
|
|
||||||
CHECK(port > 0 AND port <= 65535),
|
|
||||||
delimiter
|
|
||||||
char(1),
|
|
||||||
label
|
|
||||||
varchar(32)
|
|
||||||
NOT NULL,
|
|
||||||
flags
|
|
||||||
integer
|
|
||||||
NOT NULL
|
|
||||||
DEFAULT(0),
|
|
||||||
UNIQUE (user_id, label)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX
|
|
||||||
IX_ident_switch_user_id
|
|
||||||
ON
|
|
||||||
ident_switch(user_id);
|
|
||||||
99
SQL/postgres.initial.sql
Normal file
99
SQL/postgres.initial.sql
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
-- Patched version of SQL/postgres.initial.sql for gecka/ident-switch.
|
||||||
|
-- Commit this into your Gitea fork at the same path, replacing the
|
||||||
|
-- upstream file. Only two changes from upstream:
|
||||||
|
-- 1. CREATE TABLE / CREATE INDEX -> IF NOT EXISTS
|
||||||
|
-- 2. INSERT INTO system -> upsert via ON CONFLICT
|
||||||
|
-- This makes the script safe to re-run against an already-migrated
|
||||||
|
-- database, which happens on every container restart unless
|
||||||
|
-- /var/www/html is persisted (composer re-installs "fresh" each time).
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ident_switch
|
||||||
|
(
|
||||||
|
id
|
||||||
|
serial
|
||||||
|
PRIMARY KEY,
|
||||||
|
user_id
|
||||||
|
integer
|
||||||
|
NOT NULL
|
||||||
|
REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
iid
|
||||||
|
integer
|
||||||
|
NOT NULL
|
||||||
|
REFERENCES identities(identity_id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
UNIQUE,
|
||||||
|
parent_id
|
||||||
|
integer
|
||||||
|
DEFAULT NULL,
|
||||||
|
username
|
||||||
|
varchar(64),
|
||||||
|
password
|
||||||
|
varchar(255),
|
||||||
|
imap_host
|
||||||
|
varchar(64),
|
||||||
|
imap_port
|
||||||
|
integer
|
||||||
|
CHECK(imap_port > 0 AND imap_port <= 65535),
|
||||||
|
imap_delimiter
|
||||||
|
char(1),
|
||||||
|
label
|
||||||
|
varchar(32),
|
||||||
|
flags
|
||||||
|
integer
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT(0),
|
||||||
|
smtp_host
|
||||||
|
varchar(64),
|
||||||
|
smtp_port
|
||||||
|
integer
|
||||||
|
CHECK(smtp_port > 0 AND smtp_port <= 65535),
|
||||||
|
smtp_auth
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT(1),
|
||||||
|
smtp_username
|
||||||
|
varchar(64),
|
||||||
|
smtp_password
|
||||||
|
varchar(255),
|
||||||
|
sieve_host
|
||||||
|
varchar(64),
|
||||||
|
sieve_port
|
||||||
|
integer
|
||||||
|
CHECK(sieve_port > 0 AND sieve_port <= 65535),
|
||||||
|
sieve_auth
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT(1),
|
||||||
|
sieve_username
|
||||||
|
varchar(64),
|
||||||
|
sieve_password
|
||||||
|
varchar(255),
|
||||||
|
notify_check
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT(1),
|
||||||
|
notify_basic
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL,
|
||||||
|
notify_sound
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL,
|
||||||
|
notify_desktop
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL,
|
||||||
|
drafts_mbox
|
||||||
|
varchar(64),
|
||||||
|
sent_mbox
|
||||||
|
varchar(64),
|
||||||
|
junk_mbox
|
||||||
|
varchar(64),
|
||||||
|
trash_mbox
|
||||||
|
varchar(64),
|
||||||
|
UNIQUE (user_id, label)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_ident_switch_user_id ON ident_switch(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_ident_switch_iid ON ident_switch(iid);
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_ident_switch_parent_id ON ident_switch(parent_id);
|
||||||
|
|
||||||
|
INSERT INTO system (name, value) VALUES ('ident_switch-version', '2026021000')
|
||||||
|
ON CONFLICT (name) DO UPDATE SET value = EXCLUDED.value;
|
||||||
59
SQL/postgres/2026021000.sql
Normal file
59
SQL/postgres/2026021000.sql
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
-- Upgrade from v4.x to v5.x
|
||||||
|
-- Increase password column for encrypted values
|
||||||
|
ALTER TABLE ident_switch
|
||||||
|
ALTER COLUMN password TYPE varchar(255);
|
||||||
|
|
||||||
|
-- Add unique constraint on identity ID
|
||||||
|
ALTER TABLE ident_switch
|
||||||
|
ADD CONSTRAINT ident_switch_iid_unique UNIQUE (iid);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_ident_switch_iid ON ident_switch(iid);
|
||||||
|
|
||||||
|
-- Add Sieve support
|
||||||
|
ALTER TABLE ident_switch
|
||||||
|
ADD sieve_host varchar(64);
|
||||||
|
|
||||||
|
ALTER TABLE ident_switch
|
||||||
|
ADD sieve_port
|
||||||
|
integer
|
||||||
|
CHECK(sieve_port > 0 AND sieve_port <= 65535);
|
||||||
|
|
||||||
|
ALTER TABLE ident_switch
|
||||||
|
ADD sieve_auth
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT(1);
|
||||||
|
|
||||||
|
-- Add custom SMTP/Sieve credentials
|
||||||
|
ALTER TABLE ident_switch ADD COLUMN smtp_username varchar(64) DEFAULT NULL;
|
||||||
|
ALTER TABLE ident_switch ADD COLUMN smtp_password varchar(255) DEFAULT NULL;
|
||||||
|
ALTER TABLE ident_switch ADD COLUMN sieve_username varchar(64) DEFAULT NULL;
|
||||||
|
ALTER TABLE ident_switch ADD COLUMN sieve_password varchar(255) DEFAULT NULL;
|
||||||
|
|
||||||
|
-- Add notification settings
|
||||||
|
ALTER TABLE ident_switch
|
||||||
|
ADD notify_check
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT 1;
|
||||||
|
|
||||||
|
ALTER TABLE ident_switch
|
||||||
|
ADD notify_basic
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE ident_switch
|
||||||
|
ADD notify_sound
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE ident_switch
|
||||||
|
ADD notify_desktop
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL;
|
||||||
|
|
||||||
|
-- Add alias support: parent_id links an alias identity to a parent account
|
||||||
|
ALTER TABLE ident_switch
|
||||||
|
ADD parent_id integer DEFAULT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IX_ident_switch_parent_id ON ident_switch(parent_id);
|
||||||
88
SQL/sqlite.initial.sql
Normal file
88
SQL/sqlite.initial.sql
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
CREATE TABLE ident_switch
|
||||||
|
(
|
||||||
|
id
|
||||||
|
integer
|
||||||
|
PRIMARY KEY,
|
||||||
|
user_id
|
||||||
|
integer
|
||||||
|
NOT NULL
|
||||||
|
REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
iid
|
||||||
|
integer
|
||||||
|
NOT NULL
|
||||||
|
REFERENCES identities(identity_id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
UNIQUE,
|
||||||
|
parent_id
|
||||||
|
integer
|
||||||
|
DEFAULT NULL,
|
||||||
|
username
|
||||||
|
varchar(64),
|
||||||
|
password
|
||||||
|
varchar(255),
|
||||||
|
imap_host
|
||||||
|
varchar(64),
|
||||||
|
imap_port
|
||||||
|
integer
|
||||||
|
CHECK(imap_port > 0 AND imap_port <= 65535),
|
||||||
|
imap_delimiter
|
||||||
|
char(1),
|
||||||
|
label
|
||||||
|
varchar(32),
|
||||||
|
flags
|
||||||
|
integer
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT(0),
|
||||||
|
smtp_host
|
||||||
|
varchar(64),
|
||||||
|
smtp_port
|
||||||
|
integer
|
||||||
|
CHECK(smtp_port > 0 AND smtp_port <= 65535),
|
||||||
|
smtp_auth
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT 1,
|
||||||
|
smtp_username
|
||||||
|
varchar(64),
|
||||||
|
smtp_password
|
||||||
|
varchar(255),
|
||||||
|
sieve_host
|
||||||
|
varchar(64),
|
||||||
|
sieve_port
|
||||||
|
integer
|
||||||
|
CHECK(sieve_port > 0 AND sieve_port <= 65535),
|
||||||
|
sieve_auth
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT 1,
|
||||||
|
sieve_username
|
||||||
|
varchar(64),
|
||||||
|
sieve_password
|
||||||
|
varchar(255),
|
||||||
|
notify_check
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT 1,
|
||||||
|
notify_basic
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL,
|
||||||
|
notify_sound
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL,
|
||||||
|
notify_desktop
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL,
|
||||||
|
drafts_mbox
|
||||||
|
varchar(64),
|
||||||
|
sent_mbox
|
||||||
|
varchar(64),
|
||||||
|
junk_mbox
|
||||||
|
varchar(64),
|
||||||
|
trash_mbox
|
||||||
|
varchar(64),
|
||||||
|
UNIQUE (user_id, label)
|
||||||
|
);
|
||||||
|
CREATE INDEX IX_ident_switch_user_id ON ident_switch(user_id);
|
||||||
|
CREATE INDEX IX_ident_switch_iid ON ident_switch(iid);
|
||||||
|
CREATE INDEX IX_ident_switch_parent_id ON ident_switch(parent_id);
|
||||||
|
|
||||||
|
INSERT INTO system (name, value) VALUES ('ident_switch-version', '2026021000');
|
||||||
111
SQL/sqlite/2026021000.sql
Normal file
111
SQL/sqlite/2026021000.sql
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
-- Upgrade from v4.x to v5.x
|
||||||
|
-- SQLite: recreate table with all new columns
|
||||||
|
PRAGMA foreign_keys=off;
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE ident_switch RENAME TO ident_switch_old;
|
||||||
|
|
||||||
|
CREATE TABLE ident_switch
|
||||||
|
(
|
||||||
|
id
|
||||||
|
integer
|
||||||
|
PRIMARY KEY,
|
||||||
|
user_id
|
||||||
|
integer
|
||||||
|
NOT NULL
|
||||||
|
REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
iid
|
||||||
|
integer
|
||||||
|
NOT NULL
|
||||||
|
REFERENCES identities(identity_id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
UNIQUE,
|
||||||
|
parent_id
|
||||||
|
integer
|
||||||
|
DEFAULT NULL,
|
||||||
|
username
|
||||||
|
varchar(64),
|
||||||
|
password
|
||||||
|
varchar(255),
|
||||||
|
imap_host
|
||||||
|
varchar(64),
|
||||||
|
imap_port
|
||||||
|
integer
|
||||||
|
CHECK(imap_port > 0 AND imap_port <= 65535),
|
||||||
|
imap_delimiter
|
||||||
|
char(1),
|
||||||
|
label
|
||||||
|
varchar(32),
|
||||||
|
flags
|
||||||
|
integer
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT(0),
|
||||||
|
smtp_host
|
||||||
|
varchar(64),
|
||||||
|
smtp_port
|
||||||
|
integer
|
||||||
|
CHECK(smtp_port > 0 AND smtp_port <= 65535),
|
||||||
|
smtp_auth
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT 1,
|
||||||
|
smtp_username
|
||||||
|
varchar(64),
|
||||||
|
smtp_password
|
||||||
|
varchar(255),
|
||||||
|
sieve_host
|
||||||
|
varchar(64),
|
||||||
|
sieve_port
|
||||||
|
integer
|
||||||
|
CHECK(sieve_port > 0 AND sieve_port <= 65535),
|
||||||
|
sieve_auth
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT 1,
|
||||||
|
sieve_username
|
||||||
|
varchar(64),
|
||||||
|
sieve_password
|
||||||
|
varchar(255),
|
||||||
|
notify_check
|
||||||
|
smallint
|
||||||
|
NOT NULL
|
||||||
|
DEFAULT 1,
|
||||||
|
notify_basic
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL,
|
||||||
|
notify_sound
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL,
|
||||||
|
notify_desktop
|
||||||
|
smallint
|
||||||
|
DEFAULT NULL,
|
||||||
|
drafts_mbox
|
||||||
|
varchar(64),
|
||||||
|
sent_mbox
|
||||||
|
varchar(64),
|
||||||
|
junk_mbox
|
||||||
|
varchar(64),
|
||||||
|
trash_mbox
|
||||||
|
varchar(64),
|
||||||
|
UNIQUE (user_id, label)
|
||||||
|
);
|
||||||
|
CREATE INDEX IX_ident_switch_user_id ON ident_switch(user_id);
|
||||||
|
CREATE INDEX IX_ident_switch_iid ON ident_switch(iid);
|
||||||
|
CREATE INDEX IX_ident_switch_parent_id ON ident_switch(parent_id);
|
||||||
|
|
||||||
|
INSERT OR ROLLBACK INTO
|
||||||
|
ident_switch (
|
||||||
|
id, user_id, iid, username, password, imap_host, imap_port,
|
||||||
|
imap_delimiter, label, flags, smtp_host, smtp_port, smtp_auth,
|
||||||
|
drafts_mbox, sent_mbox, junk_mbox, trash_mbox
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id, user_id, iid, username, password, imap_host, imap_port,
|
||||||
|
imap_delimiter, label, flags, smtp_host, smtp_port, smtp_auth,
|
||||||
|
drafts_mbox, sent_mbox, junk_mbox, trash_mbox
|
||||||
|
FROM
|
||||||
|
ident_switch_old;
|
||||||
|
|
||||||
|
DROP TABLE ident_switch_old;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
PRAGMA foreign_keys=on;
|
||||||
@@ -1,34 +1,67 @@
|
|||||||
{
|
{
|
||||||
"name": "boressoft/ident_switch",
|
"name": "gecka/ident-switch",
|
||||||
"type": "roundcube-plugin",
|
"type": "roundcube-plugin",
|
||||||
"description": "This plugin allows users to switch between different accounts (including remote) in single Roundcube session.",
|
"description": "This plugin allows users to switch between different accounts (including remote) in single Roundcube session.",
|
||||||
"homepage": "https://bitbucket.org/BoresExpress/ident_switch",
|
"homepage": "https://github.com/Gecka-Apps/roundcube-ident_switch",
|
||||||
"keywords": ["identity", "imap", "mail", "switch"],
|
"keywords": ["identity", "imap", "mail", "switch"],
|
||||||
"license": "GPL-3.0+",
|
"license": "AGPL-3.0+",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Boris Gulay",
|
"name": "Boris Gulay",
|
||||||
"email": "boris@boressoft.ru",
|
"email": "boris@gulay.name",
|
||||||
"role": "Developer"
|
"role": "Original Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Christian Landvogt",
|
||||||
|
"email": "chris@lvgt.de",
|
||||||
|
"role": "Contributor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gergely Papp",
|
||||||
|
"email": "telirium@gmail.com",
|
||||||
|
"role": "Contributor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Laurent Dinclaux",
|
||||||
|
"email": "laurent@gecka.nc",
|
||||||
|
"role": "Maintainer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"repositories": [
|
"repositories": [
|
||||||
{
|
{
|
||||||
"type": "composer",
|
"type": "composer",
|
||||||
"url": "http://plugins.roundcube.net"
|
"url": "https://plugins.roundcube.net"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.3.0",
|
"php": ">=8.2",
|
||||||
"roundcube/plugin-installer": ">=0.1.3"
|
"roundcube/plugin-installer": ">=0.3.0",
|
||||||
|
"ext-ctype": "*"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"squizlabs/php_codesniffer": "^3.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"boressoft/ident_switch": "*",
|
||||||
|
"elm/identity_smtp": "*",
|
||||||
|
"toteph42/identity_switch": "*"
|
||||||
},
|
},
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://bitbucket.org/BoresExpress/ident_switch/issues"
|
"issues": "https://github.com/Gecka-Apps/roundcube-ident_switch/issues"
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"roundcube": {
|
"roundcube": {
|
||||||
"min-version": "1.1",
|
"min-version": "1.6",
|
||||||
"sql-dir": "SQL"
|
"sql-dir": "SQL"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"phpcs": "phpcs",
|
||||||
|
"phpcbf": "phpcbf"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"roundcube/plugin-installer": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
158
composer.lock
generated
Normal file
158
composer.lock
generated
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
{
|
||||||
|
"_readme": [
|
||||||
|
"This file locks the dependencies of your project to a known state",
|
||||||
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
|
"This file is @generated automatically"
|
||||||
|
],
|
||||||
|
"content-hash": "c1389b5e002c6014eeddc8105bf77db1",
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "roundcube/plugin-installer",
|
||||||
|
"version": "0.3.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/roundcube/plugin-installer.git",
|
||||||
|
"reference": "c4335e20b86cfe3a184ccf24d675c6a0338a372a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/roundcube/plugin-installer/zipball/c4335e20b86cfe3a184ccf24d675c6a0338a372a",
|
||||||
|
"reference": "c4335e20b86cfe3a184ccf24d675c6a0338a372a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"composer-plugin-api": "^1.0 || ^2.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"composer/composer": "*"
|
||||||
|
},
|
||||||
|
"type": "composer-plugin",
|
||||||
|
"extra": {
|
||||||
|
"class": [
|
||||||
|
"Roundcube\\Composer\\RoundcubeInstaller"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-0": {
|
||||||
|
"Roundcube\\Composer": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"GPL-3.0+"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Thomas Bruederli",
|
||||||
|
"email": "thomas@roundcube.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Till Klampaeckel",
|
||||||
|
"email": "till@php.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Philip Weir",
|
||||||
|
"email": "roundcube@tehinterweb.co.uk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A composer-installer for Roundcube plugins and skins.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/roundcube/plugin-installer/issues",
|
||||||
|
"source": "https://github.com/roundcube/plugin-installer/tree/0.3.2"
|
||||||
|
},
|
||||||
|
"time": "2022-06-24T09:08:18+00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"packages-dev": [
|
||||||
|
{
|
||||||
|
"name": "squizlabs/php_codesniffer",
|
||||||
|
"version": "3.13.5",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
|
||||||
|
"reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4",
|
||||||
|
"reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"ext-tokenizer": "*",
|
||||||
|
"ext-xmlwriter": "*",
|
||||||
|
"php": ">=5.4.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/phpcbf",
|
||||||
|
"bin/phpcs"
|
||||||
|
],
|
||||||
|
"type": "library",
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Greg Sherwood",
|
||||||
|
"role": "Former lead"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Juliette Reinders Folmer",
|
||||||
|
"role": "Current lead"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Contributors",
|
||||||
|
"homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
|
||||||
|
"homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
|
||||||
|
"keywords": [
|
||||||
|
"phpcs",
|
||||||
|
"standards",
|
||||||
|
"static analysis"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
|
||||||
|
"security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
|
||||||
|
"source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
|
||||||
|
"wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/PHPCSStandards",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/jrfnl",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://opencollective.com/php_codesniffer",
|
||||||
|
"type": "open_collective"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://thanks.dev/u/gh/phpcsstandards",
|
||||||
|
"type": "thanks_dev"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-11-04T16:30:35+00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aliases": [],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"stability-flags": {},
|
||||||
|
"prefer-stable": false,
|
||||||
|
"prefer-lowest": false,
|
||||||
|
"platform": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"ext-ctype": "*"
|
||||||
|
},
|
||||||
|
"platform-dev": {},
|
||||||
|
"plugin-api-version": "2.9.0"
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
/* Password encryption:
|
|
||||||
'rcmail': encrypt passwords by default Roundcube methods.
|
|
||||||
'secure': encrypt passwords by using the IMAP password as encryption key.
|
|
||||||
NOTE: When using 'secure' encryption, If IMAP passwords are changed
|
|
||||||
using methods other than Roundcube Webmail interface
|
|
||||||
(hmail_password or password plugin) then IMAP server identities
|
|
||||||
passwords must be re-entered by users.
|
|
||||||
*/
|
|
||||||
$rcmail_config['identities_imap_crypt'] = 'rcmail';
|
|
||||||
|
|
||||||
/* password encryption salt (only used for secure encryption) */
|
|
||||||
$rcmail_config['identities_imap_salt'] = '!kQm*fF3pXe1Kbm%9';
|
|
||||||
|
|
||||||
/* predefined imap hosts (associated with the domain part of the identity email property) */
|
|
||||||
$rcmail_config['identities_imap_external'] = array(
|
|
||||||
);
|
|
||||||
|
|
||||||
$rcmail_config['identities_imap_internal'] = array(
|
|
||||||
'BoresSoft Webmail' => array(
|
|
||||||
'host' =>'localhost',
|
|
||||||
'delimiter' => '/',
|
|
||||||
'readonly' => false, // on match prevent field editing
|
|
||||||
),
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
@@ -1,44 +1,101 @@
|
|||||||
<?php
|
<?php
|
||||||
/* Password encryption:
|
/**
|
||||||
'rcmail': encrypt passwords by default Roundcube methods.
|
* ident_switch - Configuration file.
|
||||||
'secure': encrypt passwords by using the IMAP password as encryption key.
|
*
|
||||||
NOTE: When using 'secure' encryption, If IMAP passwords are changed
|
* Copyright (C) 2016 Boris Gulay
|
||||||
using methods other than Roundcube Webmail interface
|
* Copyright (C) 2026 Gecka
|
||||||
(hmail_password or password plugin) then IMAP server identities
|
*
|
||||||
passwords must be re-entered by users.
|
* Original code licensed under GPL-3.0+.
|
||||||
|
* New contributions licensed under AGPL-3.0+.
|
||||||
|
*
|
||||||
|
* @url https://github.com/Gecka-apps/ident_switch
|
||||||
*/
|
*/
|
||||||
$rcmail_config['identities_imap_crypt'] = 'rcmail';
|
|
||||||
|
|
||||||
/* password encryption salt (only used for secure encryption) */
|
/*
|
||||||
$rcmail_config['identities_imap_salt'] = '!kQm*fF3pXe1Kbm%9';
|
* Preconfigured settings for different mail domains.
|
||||||
|
* Appropriate set of values is searched by mapping domain of email (from identity) to array key.
|
||||||
|
* Use '*' as a wildcard to match any domain not explicitly listed.
|
||||||
|
*/
|
||||||
|
$config['ident_switch.preconfig'] = [
|
||||||
|
|
||||||
/* predefined imap hosts (associated with the domain part of the identity email property) */
|
// Domain part of email address
|
||||||
$rcmail_config['identities_imap_external'] = array(
|
'domain.tld' => [
|
||||||
'gmail.com' => array(
|
|
||||||
'host' =>'ssl://imap.gmail.com:993',
|
|
||||||
'delimiter' => '/',
|
|
||||||
'readonly' => true, // on match prevent field editing
|
|
||||||
),
|
|
||||||
'googlemail.com' => array(
|
|
||||||
'host' =>'ssl://imap.gmail.com:993',
|
|
||||||
'delimiter' => '/',
|
|
||||||
'readonly' => true, // on match prevent field editing
|
|
||||||
),
|
|
||||||
'freenet.de' => array(
|
|
||||||
'host' =>'mx.freenet.de',
|
|
||||||
'delimiter' => '.',
|
|
||||||
'readonly' => false, // on match prevent field editing
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
$rcmail_config['identities_imap_internal'] = array(
|
// IMAP connection: scheme://host:port
|
||||||
'mydomain.tld' => array(
|
// Schemes: ssl:// for IMAPS (port 993), tls:// for STARTTLS (port 143).
|
||||||
'host' =>'ssl://imap.mydomain.tld:993',
|
// Required.
|
||||||
'delimiter' => '/',
|
'imap_host' => 'ssl://mail.domain.tld:993',
|
||||||
'readonly' => true, // on match prevent field editing
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
/* auto-detect IMAP server */
|
// SMTP connection: scheme://host:port
|
||||||
$rcmail_config['identities_imap_autodetect'] = false;
|
// Schemes: tls:// for STARTTLS (port 587), ssl:// for SMTPS (port 465).
|
||||||
?>
|
// Falls back to imap_host if not specified.
|
||||||
|
'smtp_host' => 'tls://mail.domain.tld:587',
|
||||||
|
|
||||||
|
// Sieve (managesieve) connection: scheme://host:port
|
||||||
|
// Schemes: tls:// for STARTTLS (port 4190), ssl:// for implicit TLS.
|
||||||
|
// Optional. Only needed if the managesieve plugin is installed.
|
||||||
|
'sieve_host' => 'tls://mail.domain.tld:4190',
|
||||||
|
|
||||||
|
// Login name, can be 'email' (full address from identity), 'mbox' (only mailbox part).
|
||||||
|
// Any other value is treated as 'not specified' (default).
|
||||||
|
'user' => 'email',
|
||||||
|
|
||||||
|
// IMAP folder hierarchy delimiter.
|
||||||
|
// Omit or set to null for auto-detect (recommended).
|
||||||
|
// 'delimiter' => '/',
|
||||||
|
|
||||||
|
// Are specified settings locked in interface or not.
|
||||||
|
// Default - false.
|
||||||
|
'readonly' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'another.tld' => [
|
||||||
|
'imap_host' => 'tls://mail.another.tld:143',
|
||||||
|
'smtp_host' => 'tls://mail.another.tld:587',
|
||||||
|
'user' => 'mbox',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Wildcard: default settings for any domain not listed above.
|
||||||
|
// '*' => [
|
||||||
|
// 'imap_host' => 'ssl://mail.example.com:993',
|
||||||
|
// 'smtp_host' => 'tls://mail.example.com:587',
|
||||||
|
// 'user' => 'email',
|
||||||
|
// ],
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enable background mail checking across secondary accounts.
|
||||||
|
* Shows unread counts in the account switcher and a badge for new messages.
|
||||||
|
* Default: true.
|
||||||
|
*/
|
||||||
|
$config['ident_switch.check_mail'] = true;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Round-robin mode: check one identity per refresh cycle instead of all.
|
||||||
|
* Useful with many accounts to reduce IMAP connections per refresh.
|
||||||
|
* Default: false (all identities checked every cycle).
|
||||||
|
*/
|
||||||
|
$config['ident_switch.round_robin'] = false;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hide the warning shown to users when the newmail_notifier plugin is not active.
|
||||||
|
* Default: false (warning is displayed).
|
||||||
|
*/
|
||||||
|
$config['ident_switch.hide_notifier_warning'] = false;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Restrict account switching to preconfigured domains only.
|
||||||
|
* When enabled, the ident_switch form is hidden for identities whose email
|
||||||
|
* domain does not match any entry in 'ident_switch.preconfig'.
|
||||||
|
* Users can still create Roundcube identities (name, signature, etc.) but
|
||||||
|
* cannot configure server connections for non-preconfigured domains.
|
||||||
|
* Default: false (all domains allowed).
|
||||||
|
*/
|
||||||
|
$config['ident_switch.preconfig_only'] = false;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enable debug logging to logs/ident_switch.log.
|
||||||
|
* Useful for troubleshooting SMTP/Sieve routing and alias resolution.
|
||||||
|
* Default: false.
|
||||||
|
*/
|
||||||
|
$config['ident_switch.debug'] = false;
|
||||||
|
|||||||
372
ident_switch-form.js
Normal file
372
ident_switch-form.js
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
/**
|
||||||
|
* ident_switch - Identity settings form handler.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2018 Boris Gulay
|
||||||
|
* Copyright (C) 2026 Gecka
|
||||||
|
*
|
||||||
|
* Original code licensed under GPL-3.0+.
|
||||||
|
* New contributions licensed under AGPL-3.0+.
|
||||||
|
*
|
||||||
|
* @url https://github.com/Gecka-apps/ident_switch
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default ports per protocol and security type.
|
||||||
|
*/
|
||||||
|
var ident_switch_portDefaults = {
|
||||||
|
imap: { '': 143, tls: 143, ssl: 993 },
|
||||||
|
smtp: { '': 25, tls: 587, ssl: 465 },
|
||||||
|
sieve: { '': 4190, tls: 4190, ssl: 4190 }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to ensure delegated event handlers are registered only once.
|
||||||
|
*/
|
||||||
|
var ident_switch_delegated = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the identity form: apply mode visibility, preconfig, placeholders.
|
||||||
|
* Called on each form render (initial page load + AJAX identity loads).
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_init() {
|
||||||
|
// Register delegated event handlers once (survive DOM replacements)
|
||||||
|
if (!ident_switch_delegated) {
|
||||||
|
ident_switch_delegated = true;
|
||||||
|
plugin_switchIdent_bindDelegatedEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply initial mode visibility
|
||||||
|
var initialMode = $("SELECT[name='_ident_switch.form.common.mode']").val() || 'primary';
|
||||||
|
plugin_switchIdent_mode_onChange(initialMode);
|
||||||
|
|
||||||
|
// Apply initial auth visibility for SMTP and Sieve
|
||||||
|
$.each(['smtp', 'sieve'], function(i, proto) {
|
||||||
|
var authVal = $("SELECT[name='_ident_switch.form." + proto + ".auth']").val();
|
||||||
|
plugin_switchIdent_onAuthChange(proto, authVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set initial placeholders from current field values
|
||||||
|
var initialEmail = $("INPUT[name='_email']").val();
|
||||||
|
if (initialEmail) {
|
||||||
|
$("INPUT[name='_ident_switch.form.common.label']").attr('placeholder', initialEmail);
|
||||||
|
}
|
||||||
|
var initialImapHost = $("INPUT[name='_ident_switch.form.imap.host']").val();
|
||||||
|
if (initialImapHost) {
|
||||||
|
$("INPUT[name='_ident_switch.form.smtp.host']").attr('placeholder', initialImapHost);
|
||||||
|
$("INPUT[name='_ident_switch.form.sieve.host']").attr('placeholder', initialImapHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable mode select until a valid email is entered
|
||||||
|
var $modeSelect = $("SELECT[name='_ident_switch.form.common.mode']");
|
||||||
|
if (initialEmail && initialEmail.indexOf('@') > 0) {
|
||||||
|
$modeSelect.prop('disabled', false);
|
||||||
|
plugin_switchIdent_onEmailChange(initialEmail);
|
||||||
|
} else {
|
||||||
|
$modeSelect.prop('disabled', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register delegated event handlers on the document.
|
||||||
|
* These survive DOM replacements (AJAX form reloads).
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_bindDelegatedEvents() {
|
||||||
|
// Security change handlers
|
||||||
|
$.each(['imap', 'smtp', 'sieve'], function(i, proto) {
|
||||||
|
$(document).on('change', "SELECT[name='_ident_switch.form." + proto + ".security']", function() {
|
||||||
|
plugin_switchIdent_onSecurityChange(proto, $(this).val());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Blur handlers for smart placeholder clearing
|
||||||
|
$.each(['imap', 'smtp', 'sieve'], function(i, proto) {
|
||||||
|
$(document).on('blur', "INPUT[name='_ident_switch.form." + proto + ".port']", function() {
|
||||||
|
plugin_switchIdent_clearIfDefault($(this));
|
||||||
|
});
|
||||||
|
$(document).on('blur', "INPUT[name='_ident_switch.form." + proto + ".host']", function() {
|
||||||
|
plugin_switchIdent_clearIfDefault($(this));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// IMAP host → update SMTP/Sieve host placeholders
|
||||||
|
$(document).on('change blur', "INPUT[name='_ident_switch.form.imap.host']", function() {
|
||||||
|
var imapHost = $(this).val() || 'localhost';
|
||||||
|
$("INPUT[name='_ident_switch.form.smtp.host']").attr('placeholder', imapHost);
|
||||||
|
$("INPUT[name='_ident_switch.form.sieve.host']").attr('placeholder', imapHost);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auth change handlers for SMTP and Sieve custom credentials
|
||||||
|
$.each(['smtp', 'sieve'], function(i, proto) {
|
||||||
|
$(document).on('change', "SELECT[name='_ident_switch.form." + proto + ".auth']", function() {
|
||||||
|
plugin_switchIdent_onAuthChange(proto, $(this).val());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delimiter mode handler
|
||||||
|
$(document).on('change', "SELECT[name='_ident_switch.form.imap.delimiter_mode']", function() {
|
||||||
|
if ($(this).val() === 'manual') {
|
||||||
|
$('#ident-switch-delimiter-input').show();
|
||||||
|
} else {
|
||||||
|
$('#ident-switch-delimiter-input').hide();
|
||||||
|
$("INPUT[name='_ident_switch.form.imap.delimiter']").val('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch email field for dynamic preconfig application
|
||||||
|
$(document).on('change blur', "INPUT[name='_email']", function() {
|
||||||
|
plugin_switchIdent_onEmailChange($(this).val());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run init on initial page load
|
||||||
|
$(function() {
|
||||||
|
plugin_switchIdent_init();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle mode select change: show/hide form sections based on selected mode.
|
||||||
|
* @param {string} mode - 'primary', 'alias:N', or 'separate'.
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_mode_onChange(mode) {
|
||||||
|
// Find the fieldsets/legends for each section
|
||||||
|
var $modeFld = $("SELECT[name='_ident_switch.form.common.mode']");
|
||||||
|
var $allFieldsets = $modeFld.closest('form').find('fieldset');
|
||||||
|
var $readonlyRow = $("INPUT[name='_ident_switch.form.common.readonly']").parentsUntil('FIELDSET', 'TR, .row');
|
||||||
|
|
||||||
|
// Separate account fieldsets (label, IMAP, SMTP, Sieve, Notify)
|
||||||
|
var separateFieldsets = [];
|
||||||
|
$allFieldsets.each(function() {
|
||||||
|
var $fs = $(this);
|
||||||
|
if ($fs.find("INPUT[name='_ident_switch.form.common.label']").length ||
|
||||||
|
$fs.find("INPUT[name='_ident_switch.form.imap.host']").length ||
|
||||||
|
$fs.find("INPUT[name='_ident_switch.form.smtp.host']").length ||
|
||||||
|
$fs.find("INPUT[name='_ident_switch.form.sieve.host']").length ||
|
||||||
|
$fs.find("INPUT[name='_ident_switch.form.notify.check']").length) {
|
||||||
|
separateFieldsets.push($fs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mode === 'separate') {
|
||||||
|
// Show all separate account fieldsets
|
||||||
|
$.each(separateFieldsets, function(_, $fs) { $fs.show(); });
|
||||||
|
plugin_switchIdent_processPreconfig();
|
||||||
|
} else {
|
||||||
|
// Hide all separate account fieldsets for primary and alias modes
|
||||||
|
$.each(separateFieldsets, function(_, $fs) { $fs.hide(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always hide readonly row
|
||||||
|
$readonlyRow.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle security dropdown change: update port placeholder and show/hide warning.
|
||||||
|
* @param {string} proto - Protocol name (imap, smtp, sieve).
|
||||||
|
* @param {string} security - Selected security value ('', 'tls', 'ssl').
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_onSecurityChange(proto, security) {
|
||||||
|
var portFld = $("INPUT[name='_ident_switch.form." + proto + ".port']");
|
||||||
|
var defaults = ident_switch_portDefaults[proto];
|
||||||
|
// Check if port value matches any known default for this protocol
|
||||||
|
var portVal = portFld.val();
|
||||||
|
var isDefault = !portVal;
|
||||||
|
if (portVal) {
|
||||||
|
$.each(defaults, function(_, v) {
|
||||||
|
if (parseInt(portVal) === v) {
|
||||||
|
isDefault = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update placeholder to new default
|
||||||
|
var newDefault = defaults[security] || defaults[''];
|
||||||
|
portFld.attr('placeholder', newDefault);
|
||||||
|
|
||||||
|
// If port was empty or matched a known default, clear it
|
||||||
|
if (isDefault) {
|
||||||
|
portFld.val('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide security warning
|
||||||
|
var warningId = '#ident-switch-security-warning-' + proto;
|
||||||
|
if (security === '') {
|
||||||
|
$(warningId).show();
|
||||||
|
} else {
|
||||||
|
$(warningId).hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On blur: clear field if value matches its placeholder.
|
||||||
|
* @param {jQuery} $field - The input field.
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_clearIfDefault($field) {
|
||||||
|
var val = $.trim($field.val());
|
||||||
|
var placeholder = $field.attr('placeholder') || '';
|
||||||
|
if (val !== '' && val === String(placeholder)) {
|
||||||
|
$field.val('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle auth dropdown change: show/hide custom credential fields.
|
||||||
|
* @param {string} proto - Protocol name (smtp, sieve).
|
||||||
|
* @param {string} authVal - Selected auth value.
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_onAuthChange(proto, authVal) {
|
||||||
|
var isCustom = (authVal === '3'); // SMTP_AUTH_CUSTOM / SIEVE_AUTH_CUSTOM
|
||||||
|
var userFld = $("INPUT[name='_ident_switch.form." + proto + ".username']");
|
||||||
|
var passFld = $("INPUT[name='_ident_switch.form." + proto + ".password']");
|
||||||
|
if (isCustom) {
|
||||||
|
userFld.parentsUntil("TABLE", "TR").show();
|
||||||
|
passFld.parentsUntil("TABLE", "TR").show();
|
||||||
|
} else {
|
||||||
|
userFld.parentsUntil("TABLE", "TR").hide();
|
||||||
|
passFld.parentsUntil("TABLE", "TR").hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply or remove preconfig readonly state on form fields.
|
||||||
|
* Reads the hidden readonly field value and enables/disables fields accordingly.
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_processPreconfig() {
|
||||||
|
var disFld = $("INPUT[name='_ident_switch.form.common.readonly']");
|
||||||
|
disFld.parentsUntil("TABLE", "TR").hide();
|
||||||
|
|
||||||
|
var disVal = parseInt(disFld.val(), 10) || 0;
|
||||||
|
|
||||||
|
// All fields that can be locked by preconfig
|
||||||
|
var lockedFields = [
|
||||||
|
"INPUT[name='_ident_switch.form.imap.host']",
|
||||||
|
"SELECT[name='_ident_switch.form.imap.security']",
|
||||||
|
"INPUT[name='_ident_switch.form.imap.port']",
|
||||||
|
"INPUT[name='_ident_switch.form.smtp.host']",
|
||||||
|
"SELECT[name='_ident_switch.form.smtp.security']",
|
||||||
|
"INPUT[name='_ident_switch.form.smtp.port']",
|
||||||
|
"SELECT[name='_ident_switch.form.smtp.auth']",
|
||||||
|
"INPUT[name='_ident_switch.form.smtp.username']",
|
||||||
|
"INPUT[name='_ident_switch.form.smtp.password']",
|
||||||
|
"SELECT[name='_ident_switch.form.imap.delimiter_mode']",
|
||||||
|
"INPUT[name='_ident_switch.form.imap.delimiter']",
|
||||||
|
"INPUT[name='_ident_switch.form.sieve.host']",
|
||||||
|
"SELECT[name='_ident_switch.form.sieve.security']",
|
||||||
|
"INPUT[name='_ident_switch.form.sieve.port']",
|
||||||
|
"SELECT[name='_ident_switch.form.sieve.auth']",
|
||||||
|
"INPUT[name='_ident_switch.form.sieve.username']",
|
||||||
|
"INPUT[name='_ident_switch.form.sieve.password']"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Reset all to enabled first (handles navigation from readonly to non-readonly)
|
||||||
|
$.each(lockedFields, function(_, sel) { $(sel).prop("disabled", false); });
|
||||||
|
$("INPUT[name='_ident_switch.form.imap.username']").prop("disabled", false);
|
||||||
|
|
||||||
|
if (disVal > 0) {
|
||||||
|
$.each(lockedFields, function(_, sel) { $(sel).prop("disabled", true); });
|
||||||
|
}
|
||||||
|
if (disVal === 2) {
|
||||||
|
$("INPUT[name='_ident_switch.form.imap.username']").prop("disabled", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle email field change: apply preconfig and manage domain restriction.
|
||||||
|
* @param {string} email - The email address entered by the user.
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_onEmailChange(email) {
|
||||||
|
var atPos = email.indexOf('@');
|
||||||
|
if (atPos < 0) return;
|
||||||
|
|
||||||
|
var domain = email.substring(atPos + 1).toLowerCase();
|
||||||
|
if (!domain) return;
|
||||||
|
|
||||||
|
// Enable mode select now that we have a valid email
|
||||||
|
$("SELECT[name='_ident_switch.form.common.mode']").prop('disabled', false);
|
||||||
|
|
||||||
|
// Update label and username placeholders to match current email
|
||||||
|
$("INPUT[name='_ident_switch.form.common.label']").attr('placeholder', email);
|
||||||
|
|
||||||
|
var preconfig = rcmail.env.ident_switch_preconfig || {};
|
||||||
|
var preconfigOnly = rcmail.env.ident_switch_preconfig_only || false;
|
||||||
|
var cfg = preconfig[domain] || preconfig['*'] || null;
|
||||||
|
|
||||||
|
// Update username placeholder to match current email
|
||||||
|
$("INPUT[name='_ident_switch.form.imap.username']").attr('placeholder', email);
|
||||||
|
|
||||||
|
// Show/hide domain warning and restrict "separate" option
|
||||||
|
var $modeSelect = $("SELECT[name='_ident_switch.form.common.mode']");
|
||||||
|
if (preconfigOnly && !cfg) {
|
||||||
|
var tpl = rcmail.env.ident_switch_warning_tpl || '';
|
||||||
|
$('#ident-switch-domain-warning').text(tpl.replace('%s', domain)).show();
|
||||||
|
// Disable "separate" option but keep alias options available
|
||||||
|
$modeSelect.find('option[value="separate"]').prop('disabled', true);
|
||||||
|
if ($modeSelect.val() === 'separate') {
|
||||||
|
$modeSelect.val('primary');
|
||||||
|
plugin_switchIdent_mode_onChange('primary');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#ident-switch-domain-warning').hide();
|
||||||
|
$modeSelect.find('option[value="separate"]').prop('disabled', false);
|
||||||
|
|
||||||
|
// Only auto-fill for identities without an existing DB record
|
||||||
|
if (!rcmail.env.ident_switch_has_record && cfg) {
|
||||||
|
plugin_switchIdent_applyJsPreconfig(cfg, email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply preconfig values from JS environment to the form fields.
|
||||||
|
* @param {object} cfg - Preconfig entry for the matched domain.
|
||||||
|
* @param {string} email - The full email address.
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_applyJsPreconfig(cfg, email) {
|
||||||
|
// Pre-fill server fields silently without changing mode.
|
||||||
|
// The user must explicitly select "Separate account" to see them.
|
||||||
|
|
||||||
|
// Apply protocol settings
|
||||||
|
$.each(['imap', 'smtp', 'sieve'], function(_, proto) {
|
||||||
|
if (!cfg[proto]) return;
|
||||||
|
|
||||||
|
// Set security first (updates port placeholder)
|
||||||
|
var security = cfg[proto].security || '';
|
||||||
|
$("SELECT[name='_ident_switch.form." + proto + ".security']").val(security);
|
||||||
|
plugin_switchIdent_onSecurityChange(proto, security);
|
||||||
|
|
||||||
|
// Set host
|
||||||
|
$("INPUT[name='_ident_switch.form." + proto + ".host']").val(cfg[proto].host || '');
|
||||||
|
|
||||||
|
// Set port (empty if matches default, so placeholder shows)
|
||||||
|
var port = cfg[proto].port;
|
||||||
|
var defaultPort = ident_switch_portDefaults[proto][security] || '';
|
||||||
|
$("INPUT[name='_ident_switch.form." + proto + ".port']").val(
|
||||||
|
(port && parseInt(port) !== defaultPort) ? port : ''
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply username from preconfig user mode
|
||||||
|
if (cfg.user) {
|
||||||
|
var username = '';
|
||||||
|
if (cfg.user.toUpperCase() === 'EMAIL') {
|
||||||
|
username = email;
|
||||||
|
} else if (cfg.user.toUpperCase() === 'MBOX') {
|
||||||
|
username = email.split('@')[0];
|
||||||
|
}
|
||||||
|
if (username) {
|
||||||
|
$("INPUT[name='_ident_switch.form.imap.username']").val(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply readonly level
|
||||||
|
var readonlyLevel = 0;
|
||||||
|
if (cfg.readonly) {
|
||||||
|
var hasUser = cfg.user && ['EMAIL', 'MBOX'].indexOf(cfg.user.toUpperCase()) >= 0;
|
||||||
|
readonlyLevel = hasUser ? 2 : 1;
|
||||||
|
}
|
||||||
|
$("INPUT[name='_ident_switch.form.common.readonly']").val(readonlyLevel);
|
||||||
|
|
||||||
|
// Re-apply preconfig readonly state
|
||||||
|
plugin_switchIdent_processPreconfig();
|
||||||
|
}
|
||||||
306
ident_switch-switch.js
Normal file
306
ident_switch-switch.js
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
/**
|
||||||
|
* ident_switch - Account switcher UI and new mail notifications.
|
||||||
|
*
|
||||||
|
* Places the hidden <select> from the footer into the appropriate
|
||||||
|
* skin location (Larry, Classic, Elastic, Workbench), shows it, and registers
|
||||||
|
* notification listeners for background mail checking.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2016-2018 Boris Gulay
|
||||||
|
* Copyright (C) 2026 Gecka
|
||||||
|
*
|
||||||
|
* Original code licensed under GPL-3.0+.
|
||||||
|
* New contributions licensed under AGPL-3.0+.
|
||||||
|
*
|
||||||
|
* @url https://github.com/Gecka-apps/ident_switch
|
||||||
|
*/
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
var $wrapper = $('#ident-switch-wrapper');
|
||||||
|
if (!$wrapper.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var $sw = $wrapper.find('#plugin-ident_switch-account');
|
||||||
|
var placed = false;
|
||||||
|
switch (rcmail.env.skin) {
|
||||||
|
case 'larry':
|
||||||
|
placed = plugin_switchIdent_addCbLarry($wrapper, $sw);
|
||||||
|
break;
|
||||||
|
case 'classic':
|
||||||
|
placed = plugin_switchIdent_addCbClassic($wrapper, $sw);
|
||||||
|
break;
|
||||||
|
case 'elastic':
|
||||||
|
placed = plugin_switchIdent_addCbElastic($wrapper, $sw);
|
||||||
|
break;
|
||||||
|
case 'workbench':
|
||||||
|
placed = plugin_switchIdent_addCbElastic($wrapper, $sw);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!placed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sw.show();
|
||||||
|
|
||||||
|
// Store original option texts for badge appending
|
||||||
|
$sw.find('option').each(function() {
|
||||||
|
$(this).data('orig-text', $(this).text());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register server-side notification listeners
|
||||||
|
rcmail.addEventListener('plugin.ident_switch.update_counts', ident_switch_updateCounts);
|
||||||
|
rcmail.addEventListener('plugin.ident_switch.notify', ident_switch_onNotify);
|
||||||
|
|
||||||
|
// Apply initial counts from page load
|
||||||
|
if (rcmail.env.ident_switch_initial_counts) {
|
||||||
|
ident_switch_updateCounts(rcmail.env.ident_switch_initial_counts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place switcher in Larry skin: replace username in top-right corner of #topline.
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_addCbLarry($wrapper, $sw) {
|
||||||
|
var $topRight = $('#topline .topright');
|
||||||
|
if (!$topRight.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$topRight.find('.username').hide();
|
||||||
|
$topRight.prepend($wrapper);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place switcher in Classic skin: prepend to task bar.
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_addCbClassic($wrapper) {
|
||||||
|
var $taskBar = $('#taskbar');
|
||||||
|
if (!$taskBar.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$taskBar.prepend($wrapper);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place switcher in Elastic skin: replace username in header.
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_addCbElastic($wrapper, $sw) {
|
||||||
|
var $target = $('.header-title.username');
|
||||||
|
if (!$target.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sw.css({
|
||||||
|
'background-color': 'transparent',
|
||||||
|
'border': 'none',
|
||||||
|
'font-weight': 'bold',
|
||||||
|
'color': 'inherit',
|
||||||
|
'box-shadow': 'none',
|
||||||
|
'text-overflow': 'ellipsis',
|
||||||
|
'padding': '0 1.2em 0 0.25em'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide original username text and elements
|
||||||
|
$target.contents().filter(function() {
|
||||||
|
return this.nodeType === 3;
|
||||||
|
}).remove();
|
||||||
|
$target.children().not('#ident-switch-wrapper').hide();
|
||||||
|
|
||||||
|
$target.prepend($wrapper);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform account switch via AJAX (called from <select> onchange).
|
||||||
|
*/
|
||||||
|
var ident_switch_switching = false;
|
||||||
|
|
||||||
|
function plugin_switchIdent_switch(val) {
|
||||||
|
if (ident_switch_switching) return;
|
||||||
|
ident_switch_switching = true;
|
||||||
|
rcmail.env.unread_counts = {};
|
||||||
|
rcmail.http_post('plugin.ident_switch.switch', {
|
||||||
|
'_ident-id': val,
|
||||||
|
'_mbox': rcmail.env.mailbox
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update per-account counts in select options and total badge.
|
||||||
|
*
|
||||||
|
* Each entry in data is {unseen: N, baseline: B}.
|
||||||
|
* - Option text: "account (baseline+delta)" when delta > 0, else "account (unseen)"
|
||||||
|
* - Badge: sum of deltas across all accounts (new messages not yet visited).
|
||||||
|
*
|
||||||
|
* @param {Object} data - Map of iid to {unseen, baseline}.
|
||||||
|
*/
|
||||||
|
function ident_switch_updateCounts(data) {
|
||||||
|
var map = rcmail.env.ident_switch_iid_map || {};
|
||||||
|
var $select = $('#plugin-ident_switch-account');
|
||||||
|
var totalDelta = 0;
|
||||||
|
|
||||||
|
// Reset all options to original text
|
||||||
|
$select.find('option').each(function() {
|
||||||
|
var orig = $(this).data('orig-text');
|
||||||
|
if (orig) {
|
||||||
|
$(this).text(orig);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// On mail task, skip count for the active account (already shown in folder list)
|
||||||
|
var selectedVal = rcmail.env.task === 'mail' ? $select.val() : null;
|
||||||
|
|
||||||
|
// Update each option with its count
|
||||||
|
for (var iid in data) {
|
||||||
|
if (!data.hasOwnProperty(iid)) continue;
|
||||||
|
var info = data[iid];
|
||||||
|
var unseen = parseInt(info.unseen) || 0;
|
||||||
|
var baseline = parseInt(info.baseline) || 0;
|
||||||
|
var delta = Math.max(0, unseen - baseline);
|
||||||
|
|
||||||
|
totalDelta += delta;
|
||||||
|
|
||||||
|
if (map[iid] === undefined) continue;
|
||||||
|
var selectVal = '' + map[iid];
|
||||||
|
var $opt = $select.find('option').filter(function() { return $(this).val() === selectVal; });
|
||||||
|
if (!$opt.length) continue;
|
||||||
|
|
||||||
|
// Skip active account on mail task
|
||||||
|
if (selectVal === selectedVal) continue;
|
||||||
|
|
||||||
|
var suffix;
|
||||||
|
if (delta > 0) {
|
||||||
|
suffix = ' (' + baseline + '+' + delta + ')';
|
||||||
|
} else if (unseen > 0) {
|
||||||
|
suffix = ' (' + unseen + ')';
|
||||||
|
} else {
|
||||||
|
suffix = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suffix) {
|
||||||
|
$opt.text($opt.data('orig-text') + suffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update total badge (only new messages not yet visited)
|
||||||
|
var $badge = $('#ident-switch-badge');
|
||||||
|
if (totalDelta > 0) {
|
||||||
|
$badge.text(totalDelta).show();
|
||||||
|
} else {
|
||||||
|
$badge.hide().text('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle new mail notification from server.
|
||||||
|
* @param {Object} data - {iid, label, count, basic, sound, desktop}
|
||||||
|
*/
|
||||||
|
function ident_switch_onNotify(data) {
|
||||||
|
if (data.basic) {
|
||||||
|
ident_switch_notifyBasic();
|
||||||
|
}
|
||||||
|
if (data.sound) {
|
||||||
|
ident_switch_notifySound();
|
||||||
|
}
|
||||||
|
if (data.desktop) {
|
||||||
|
ident_switch_notifyDesktop(data.label, data.count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic notification: change page title.
|
||||||
|
*/
|
||||||
|
function ident_switch_notifyBasic() {
|
||||||
|
var marker = '(*) ';
|
||||||
|
if (document.title.indexOf(marker) !== 0) {
|
||||||
|
document.title = marker + document.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sound notification: play newmail_notifier sound.
|
||||||
|
*/
|
||||||
|
function ident_switch_notifySound() {
|
||||||
|
var src = rcmail.assets_path('plugins/newmail_notifier/sound');
|
||||||
|
try {
|
||||||
|
new Audio(src + '.mp3').play().catch(function() {
|
||||||
|
new Audio(src + '.wav').play().catch(function() {});
|
||||||
|
});
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Desktop notification via Notification API.
|
||||||
|
*/
|
||||||
|
function ident_switch_notifyDesktop(label, count) {
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Notification.permission === 'default') {
|
||||||
|
Notification.requestPermission();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Notification.permission !== 'granted') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = count + ' unread message' + (count > 1 ? 's' : '');
|
||||||
|
var popup = new Notification('New mail — ' + label, {
|
||||||
|
body: body,
|
||||||
|
tag: 'ident_switch_' + label,
|
||||||
|
icon: rcmail.assets_path('plugins/ident_switch/mail.png')
|
||||||
|
});
|
||||||
|
|
||||||
|
var timeout = (rcmail.env.newmail_notifier_timeout || 10) * 1000;
|
||||||
|
setTimeout(function() { popup.close(); }, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix identity selection in compose view when impersonating.
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_fixIdent(iid) {
|
||||||
|
if (parseInt(iid) > 0) {
|
||||||
|
$('#_from').val(iid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter compose From dropdown to only show identities for the active account.
|
||||||
|
* Removes options whose identity_id is not in ident_switch_allowed_identities.
|
||||||
|
*/
|
||||||
|
function plugin_switchIdent_filterFrom() {
|
||||||
|
var allowed = rcmail.env.ident_switch_allowed_identities;
|
||||||
|
if (!allowed || !allowed.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var $from = $('#_from');
|
||||||
|
if (!$from.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentVal = parseInt($from.val());
|
||||||
|
|
||||||
|
$from.find('option').each(function() {
|
||||||
|
if (allowed.indexOf(parseInt($(this).val())) === -1) {
|
||||||
|
$(this).remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If selected option was removed, select the first remaining
|
||||||
|
var $remaining = $from.find('option:first');
|
||||||
|
if (allowed.indexOf(currentVal) === -1 && $remaining.length) {
|
||||||
|
$from.val($remaining.val()).trigger('change');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter compose From dropdown on page load
|
||||||
|
$(function() {
|
||||||
|
plugin_switchIdent_filterFrom();
|
||||||
|
});
|
||||||
104
ident_switch.css
Normal file
104
ident_switch.css
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* ident_switch - Account switcher styles.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2026 Gecka
|
||||||
|
*
|
||||||
|
* Licensed under AGPL-3.0+.
|
||||||
|
*
|
||||||
|
* @url https://github.com/Gecka-apps/ident_switch
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Wrapper around <select> + badge */
|
||||||
|
.ident-switch-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Account switcher <select>: hide native arrow, show SVG chevron.
|
||||||
|
Scoped to Elastic (.header-title.username) and Larry (#topline .topright) where
|
||||||
|
the switcher replaces a styled header label; Classic uses Roundcube's native
|
||||||
|
select rendering instead (see #taskbar rule below). */
|
||||||
|
.header-title.username .ident-switch-wrapper select,
|
||||||
|
#topline .topright .ident-switch-wrapper select {
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23ccc'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.3em center;
|
||||||
|
background-size: 10px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Prevent flash of original username before JS prepends the switcher.
|
||||||
|
* The JS hides these elements at DOMReady, but the CSS rules below hide them
|
||||||
|
* earlier (during initial render) — only loaded when the switcher will actually
|
||||||
|
* be shown, so users without secondary accounts keep the default username display.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Elastic skin: switcher is prepended inside .header-title.username.
|
||||||
|
Hide the original username via visibility on the parent (affects the text
|
||||||
|
node), then re-show the wrapper. Unlike color/font-size, this preserves the
|
||||||
|
parent's flex/line-height context, keeping the wrapper vertically centered. */
|
||||||
|
.header-title.username {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.header-title.username > *:not(.ident-switch-wrapper) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.header-title.username > .ident-switch-wrapper {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Larry skin: switcher is prepended next to .username inside #topline .topright */
|
||||||
|
#topline .topright > .username {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Larry skin: blend the <select> into the dark #topline header (Larry styles a
|
||||||
|
<span> here, not a <select>, so without these the native white control sticks
|
||||||
|
out on the dark background). */
|
||||||
|
#topline .topright .ident-switch-wrapper {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
#topline .topright .ident-switch-wrapper select {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
color: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0 1.2em 0 0.25em;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Classic skin: switcher is prepended inside #taskbar (right-aligned, nowrap, 24px tall).
|
||||||
|
Drop width:100% so it sits inline before the menu links instead of pushing them
|
||||||
|
off-screen. The select keeps Roundcube's default native rendering. */
|
||||||
|
#taskbar > .ident-switch-wrapper {
|
||||||
|
width: auto;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
#taskbar > .ident-switch-wrapper select {
|
||||||
|
max-width: 180px;
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Unread badge */
|
||||||
|
.ident-switch-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 1.4em;
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
font-size: 0.7em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #e74c3c;
|
||||||
|
border-radius: 1em;
|
||||||
|
margin-left: 0.3em;
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/*
|
|
||||||
* This is part of identity_imap plugin
|
|
||||||
*/
|
|
||||||
$(function() {
|
|
||||||
if ($('#plugin-ident_switch-account').size() > 0) {
|
|
||||||
$('#plugin-ident_switch-account').prependTo('.topright');
|
|
||||||
$('.topright .username').hide();
|
|
||||||
$('#plugin-ident_switch-account').show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function plugin_switchIdent_switch(val) {
|
|
||||||
console.log("qwe1");
|
|
||||||
rcmail.http_post('plugin.ident_switch.switch', { '_ident-id': val });
|
|
||||||
}
|
|
||||||
717
ident_switch.php
717
ident_switch.php
@@ -1,291 +1,528 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identities IMAP
|
* ident_switch - Roundcube plugin for fast switching between accounts.
|
||||||
*
|
*
|
||||||
* This plugin allows fast switching between accounts.
|
* Copyright (C) 2016-2022 Boris Gulay
|
||||||
|
* Copyright (C) 2019 Christian Landvogt
|
||||||
|
* Copyright (C) 2021 Gergely Papp
|
||||||
|
* Copyright (C) 2022 Mickael
|
||||||
|
* Copyright (C) 2026 Gecka
|
||||||
*
|
*
|
||||||
* @version 1.0
|
* Original code licensed under GPL-3.0+.
|
||||||
* @author Boris Gulay
|
* New contributions licensed under AGPL-3.0+.
|
||||||
* @url
|
*
|
||||||
|
* @url https://github.com/Gecka-apps/ident_switch
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/lib/IdentSwitchPreconfig.php';
|
||||||
|
require_once __DIR__ . '/lib/IdentSwitchForm.php';
|
||||||
|
require_once __DIR__ . '/lib/IdentSwitchSwitcher.php';
|
||||||
|
require_once __DIR__ . '/lib/IdentSwitchChecker.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roundcube plugin for fast switching between multiple IMAP accounts.
|
||||||
|
*
|
||||||
|
* Allows users to configure and switch between multiple mail accounts
|
||||||
|
* (including remote) within a single Roundcube session, with alias support.
|
||||||
|
*
|
||||||
|
* @url https://github.com/Gecka-apps/ident_switch
|
||||||
*/
|
*/
|
||||||
class ident_switch extends rcube_plugin
|
class ident_switch extends rcube_plugin
|
||||||
{
|
{
|
||||||
|
/** @var string Task regex: active on all tasks except login/logout. */
|
||||||
public $task = '?(?!login|logout).*';
|
public $task = '?(?!login|logout).*';
|
||||||
|
|
||||||
private $table = 'ident_switch';
|
/** @var string Database table name for this plugin. */
|
||||||
|
public const TABLE = 'ident_switch';
|
||||||
|
|
||||||
// Flags user in database
|
/** @var string Session variable suffix used to store/restore state. */
|
||||||
private $db_enabled = 1;
|
public const MY_POSTFIX = '_iswitch';
|
||||||
private $db_secure = 2;
|
|
||||||
|
|
||||||
function init()
|
/** @var int Flag: account switching is enabled. */
|
||||||
|
public const DB_ENABLED = 1;
|
||||||
|
|
||||||
|
/** @var int Legacy flag: use TLS for IMAP. Read-only for backward compat; new records store scheme in host. */
|
||||||
|
public const DB_SECURE_IMAP_TLS = 4;
|
||||||
|
|
||||||
|
/** @var int SMTP authentication: use same credentials as IMAP. */
|
||||||
|
public const SMTP_AUTH_IMAP = 1;
|
||||||
|
|
||||||
|
/** @var int SMTP authentication: no authentication required. */
|
||||||
|
public const SMTP_AUTH_NONE = 2;
|
||||||
|
|
||||||
|
/** @var int SMTP authentication: use custom credentials. */
|
||||||
|
public const SMTP_AUTH_CUSTOM = 3;
|
||||||
|
|
||||||
|
/** @var int Sieve authentication: use same credentials as IMAP. */
|
||||||
|
public const SIEVE_AUTH_IMAP = 1;
|
||||||
|
|
||||||
|
/** @var int Sieve authentication: no authentication required. */
|
||||||
|
public const SIEVE_AUTH_NONE = 2;
|
||||||
|
|
||||||
|
/** @var int Sieve authentication: use custom credentials. */
|
||||||
|
public const SIEVE_AUTH_CUSTOM = 3;
|
||||||
|
|
||||||
|
/** @var int Notification checking: enabled. */
|
||||||
|
public const NOTIFY_CHECK_ENABLED = 1;
|
||||||
|
|
||||||
|
/** @var int Notification checking: disabled. */
|
||||||
|
public const NOTIFY_CHECK_DISABLED = 0;
|
||||||
|
|
||||||
|
private IdentSwitchForm $form;
|
||||||
|
private IdentSwitchSwitcher $switcher;
|
||||||
|
private IdentSwitchPreconfig $preconfig;
|
||||||
|
private IdentSwitchChecker $checker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize plugin: register hooks, actions, and save default folder config.
|
||||||
|
*/
|
||||||
|
public function init(): void
|
||||||
{
|
{
|
||||||
$this->add_hook('startup', array($this, 'on_startup'));
|
$this->form = new IdentSwitchForm($this);
|
||||||
$this->add_hook('render_page', array($this, 'on_render_page'));
|
$this->switcher = new IdentSwitchSwitcher();
|
||||||
# $this->add_hook('smtp_connect', array($this, 'on_smtp_connect'));
|
$this->preconfig = new IdentSwitchPreconfig($this);
|
||||||
$this->add_hook('identity_form', array($this, 'on_identity_form'));
|
$this->checker = new IdentSwitchChecker();
|
||||||
$this->add_hook('identity_update', array($this, 'on_identity_update'));
|
|
||||||
$this->add_hook('identity_delete', array($this, 'on_identity_delete'));
|
|
||||||
|
|
||||||
$this->register_action('plugin.ident_switch.switch', array($this, 'on_switch'));
|
$this->add_hook('startup', [$this, 'on_startup']);
|
||||||
|
$this->add_hook('render_page', [$this, 'on_render_page']);
|
||||||
|
$this->add_hook('refresh', [$this, 'on_refresh']);
|
||||||
|
$this->add_hook('smtp_connect', [$this, 'on_smtp_connect']);
|
||||||
|
$this->add_hook('managesieve_connect', [$this, 'on_managesieve_connect']);
|
||||||
|
$this->add_hook('identity_form', [$this, 'on_identity_form']);
|
||||||
|
$this->add_hook('identity_update', [$this, 'on_identity_update']);
|
||||||
|
$this->add_hook('identity_create', [$this, 'on_identity_create']);
|
||||||
|
$this->add_hook('identity_create_after', [$this, 'on_identity_create_after']);
|
||||||
|
$this->add_hook('identity_delete', [$this, 'on_identity_delete']);
|
||||||
|
$this->add_hook('template_object_composeheaders', [$this, 'on_template_object_composeheaders']);
|
||||||
|
$this->add_hook('preferences_list', [$this, 'on_special_folders_form']);
|
||||||
|
$this->add_hook('preferences_save', [$this, 'on_special_folders_update']);
|
||||||
|
|
||||||
|
$this->register_action('plugin.ident_switch.switch', [$this, 'on_switch']);
|
||||||
|
|
||||||
|
$this->load_config();
|
||||||
|
|
||||||
|
$rc = rcmail::get_instance();
|
||||||
|
foreach (rcube_storage::$folder_types as $type) {
|
||||||
|
$key = $type . '_mbox_default' . self::MY_POSTFIX;
|
||||||
|
if (empty($_SESSION[$key])) {
|
||||||
|
$_SESSION[$key] = $rc->config->get($type . '_mbox');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function on_startup($args)
|
/**
|
||||||
|
* Handle startup hook: detect impersonation, disable caches, restore folder config.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing 'task' and other startup data.
|
||||||
|
* @return array Modified hook arguments.
|
||||||
|
*/
|
||||||
|
public function on_startup(array $args): array
|
||||||
{
|
{
|
||||||
$rc = rcmail::get_instance();
|
$rc = rcmail::get_instance();
|
||||||
|
|
||||||
if (strtolower($rc->user->data['username']) != strtolower($_SESSION['username']))
|
if (strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0) {
|
||||||
{ // We are impersonating
|
// We are impersonating
|
||||||
$rc->config->set('imap_cache', null);
|
$rc->config->set('imap_cache', null);
|
||||||
$rc->config->set('messages_cache', false);
|
$rc->config->set('messages_cache', false);
|
||||||
|
|
||||||
if ($args['task'] == 'mail')
|
if ($args['task'] === 'mail') {
|
||||||
{
|
|
||||||
$this->add_texts('localization/');
|
$this->add_texts('localization/');
|
||||||
$rc->config->set('create_default_folders', false);
|
$rc->config->set('create_default_folders', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (rcube_storage::$folder_types as $type) {
|
||||||
|
$defaultKey = $type . '_mbox_default' . self::MY_POSTFIX;
|
||||||
|
$otherKey = $type . '_mbox' . self::MY_POSTFIX;
|
||||||
|
$val = $_SESSION[$otherKey] ?? $_SESSION[$defaultKey];
|
||||||
|
$rc->config->set($type . '_mbox', $val);
|
||||||
|
}
|
||||||
|
|
||||||
return $args;
|
return $args;
|
||||||
}
|
}
|
||||||
|
|
||||||
function on_render_page($args)
|
/**
|
||||||
|
* Handle render_page hook: inject account switcher or settings form script.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing page rendering data.
|
||||||
|
* @return array Modified hook arguments.
|
||||||
|
*/
|
||||||
|
public function on_render_page(array $args): array
|
||||||
{
|
{
|
||||||
$rc = rcmail::get_instance();
|
$rc = rcmail::get_instance();
|
||||||
|
|
||||||
// Get list of alternative accounts
|
match ($rc->task) {
|
||||||
$sOpt = '';
|
'mail' => $this->render_switch($rc, $args),
|
||||||
$sql = 'SELECT id, label, username FROM ' . $rc->db->table_name($this->table) . ' WHERE user_id = ? AND flags & ? > 0';
|
'settings' => $this->include_script('ident_switch-form.js'),
|
||||||
$q = $rc->db->query($sql, $rc->user->data['user_id'], $this->db_enabled);
|
default => null,
|
||||||
while ($r = $rc->db->fetch_assoc($q))
|
};
|
||||||
{
|
|
||||||
$opts = array('value' => $r['id']);
|
|
||||||
if (strcasecmp($_SESSION['username'], $r['username']) === 0)
|
|
||||||
$opts['selected'] = 'selected';
|
|
||||||
|
|
||||||
$sOpt .= html::tag(
|
return $args;
|
||||||
'option',
|
|
||||||
$opts,
|
|
||||||
$r['label']
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render UI if user has extra accounts
|
/**
|
||||||
if (!empty($sOpt))
|
* Render the account switcher dropdown in the mail view.
|
||||||
|
*
|
||||||
|
* Queries the database for all enabled alternative accounts and generates
|
||||||
|
* an HTML select element with an unread badge, injected into the page footer.
|
||||||
|
*
|
||||||
|
* @param rcmail $rc Roundcube instance.
|
||||||
|
* @param array $args Hook arguments for page rendering.
|
||||||
|
*/
|
||||||
|
private function render_switch(rcmail $rc, array $args): void
|
||||||
{
|
{
|
||||||
|
// Currently selected identity
|
||||||
|
$iid_s = $_SESSION['iid' . self::MY_POSTFIX] ?? null;
|
||||||
|
|
||||||
// Add main account
|
$iid = 0;
|
||||||
$opts = array('value' => -1);
|
if (is_int($iid_s)) {
|
||||||
if (strcasecmp($_SESSION['username'], $rc->user->data['username']) === 0)
|
$iid = $iid_s;
|
||||||
$opts['selected'] = 'selected';
|
} elseif ($iid_s === '-1') {
|
||||||
|
$iid = -1;
|
||||||
|
} elseif (is_string($iid_s) && ctype_digit($iid_s)) {
|
||||||
|
$iid = intval($iid_s);
|
||||||
|
}
|
||||||
|
|
||||||
$sOpt = html::tag(
|
$primaryIdentity = $rc->user->get_identity();
|
||||||
'option',
|
$primaryName = !empty($primaryIdentity['name']) ? $primaryIdentity['name'] : null;
|
||||||
$opts,
|
$accNames = [$_SESSION['global_alias'] ?? $primaryName ?? $rc->user->data['username']];
|
||||||
$_SESSION['global_alias'] ? $_SESSION['global_alias'] : $rc->user->data['username']
|
$accValues = [-1];
|
||||||
) . $sOpt;
|
$accSelected = -1;
|
||||||
|
$iidMap = [0 => -1]; // primary account: iid 0 → select value -1
|
||||||
|
|
||||||
$this->include_script('ident_switch.js');
|
// Get list of alternative accounts
|
||||||
$sw = html::tag(
|
$sql = "SELECT "
|
||||||
'select',
|
. "isw.id, isw.iid, isw.label, isw.username, ii.email"
|
||||||
array(
|
. " FROM"
|
||||||
|
. " {$rc->db->table_name(self::TABLE)} isw"
|
||||||
|
. " INNER JOIN {$rc->db->table_name('identities')} ii ON isw.iid=ii.identity_id"
|
||||||
|
. " WHERE isw.user_id = ? AND isw.flags & ? > 0 AND isw.parent_id IS NULL";
|
||||||
|
$qRec = $rc->db->query($sql, $rc->user->data['user_id'], self::DB_ENABLED);
|
||||||
|
while ($r = $rc->db->fetch_assoc($qRec)) {
|
||||||
|
$accValues[] = $r['id'];
|
||||||
|
$iidMap[$r['iid']] = $r['id'];
|
||||||
|
if ($iid === (int)$r['iid']) {
|
||||||
|
$accSelected = $r['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$lbl = $r['label'] ?: $r['username'] ?: $r['email'];
|
||||||
|
$accNames[] = $lbl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($accValues) <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->include_stylesheet('ident_switch.css');
|
||||||
|
$this->include_script('ident_switch-switch.js');
|
||||||
|
|
||||||
|
// Pass config to JS environment
|
||||||
|
$rc->output->set_env('ident_switch_iid_map', $iidMap);
|
||||||
|
|
||||||
|
$select = new html_select([
|
||||||
'id' => 'plugin-ident_switch-account',
|
'id' => 'plugin-ident_switch-account',
|
||||||
'style' => 'display: none;',
|
'style' => 'display: none;',
|
||||||
'onchange' => 'plugin_switchIdent_switch(this.value);',
|
'onchange' => 'plugin_switchIdent_switch(this.value);',
|
||||||
),
|
]);
|
||||||
$sOpt
|
$select->add($accNames, $accValues);
|
||||||
);
|
|
||||||
$rc->output->add_footer($sw);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $args;
|
$html = '<span id="ident-switch-wrapper" class="ident-switch-wrapper">'
|
||||||
}
|
. $select->show([$accSelected])
|
||||||
|
. '<span id="ident-switch-badge" class="ident-switch-badge" style="display:none"></span>'
|
||||||
|
. '</span>';
|
||||||
|
|
||||||
function on_smtp_connect($args)
|
$rc->output->add_footer($html);
|
||||||
{
|
|
||||||
error_log('on_smtp_connect');
|
|
||||||
}
|
|
||||||
|
|
||||||
function on_identity_form($args)
|
if (!$rc->config->get('ident_switch.check_mail', true)) {
|
||||||
{
|
|
||||||
$rc = rcmail::get_instance();
|
|
||||||
$this->add_texts('localization');
|
|
||||||
|
|
||||||
// Create our field set
|
|
||||||
$args['form']['ident_switch'] = array(
|
|
||||||
'name' => $this->gettext('form.caption'),
|
|
||||||
'content' => array(
|
|
||||||
'ident_switch.form.enabled' => array('type' => 'checkbox'),
|
|
||||||
'ident_switch.form.label' => array('type' => 'text', 'size' => 32),
|
|
||||||
'ident_switch.form.host' => array('type' => 'text', 'size' => 64),
|
|
||||||
'ident_switch.form.secure' => array('type' => 'checkbox'),
|
|
||||||
'ident_switch.form.port' => array('type' => 'text', 'size' => 5),
|
|
||||||
'ident_switch.form.username' => array('type' => 'text', 'size' => 64),
|
|
||||||
'ident_switch.form.password' => array('type' => 'password', 'size' => 64),
|
|
||||||
'ident_switch.form.delimiter' => array('type' => 'text', 'size' => 1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load data if exists
|
|
||||||
if (isset($args['record']['identity_id']))
|
|
||||||
{
|
|
||||||
$sql = 'SELECT * FROM ' . $rc->db->table_name($this->table) . ' WHERE iid = ? AND user_id = ?';
|
|
||||||
$q = $rc->db->query($sql, $args['record']['identity_id'], $rc->user->ID);
|
|
||||||
$r = $rc->db->fetch_assoc($q);
|
|
||||||
if ($r)
|
|
||||||
{
|
|
||||||
foreach ($r as $k => $v)
|
|
||||||
$args['record']['ident_switch.form.' . $k] = $v;
|
|
||||||
|
|
||||||
// Parse flags
|
|
||||||
if ($r['flags'] & $this->db_enabled)
|
|
||||||
$args['record']['ident_switch.form.enabled'] = true;
|
|
||||||
if ($r['flags'] & $this->db_secure)
|
|
||||||
$args['record']['ident_switch.form.secure'] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $args;
|
|
||||||
}
|
|
||||||
|
|
||||||
function on_identity_update($args)
|
|
||||||
{
|
|
||||||
$rc = rcmail::get_instance();
|
|
||||||
|
|
||||||
// TODO: check field values
|
|
||||||
|
|
||||||
$sql = 'SELECT NULL FROM ' . $rc->db->table_name($this->table) . ' WHERE iid = ? AND user_id = ?';
|
|
||||||
$q = $rc->db->query($sql, $args['id'], $rc->user->ID);
|
|
||||||
$r = $rc->db->fetch_assoc($q);
|
|
||||||
if ($r)
|
|
||||||
{ // Record already exists, will update it
|
|
||||||
$sql = 'UPDATE ' .
|
|
||||||
$rc->db->table_name($this->table) .
|
|
||||||
' SET flags = ?, label = ?, host = ?, port = ?, username = ?, password = ?, delimiter = ?, user_id = ?, iid = ?';
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{ // No record exists, create new one
|
|
||||||
$sql = 'INSERT INTO ' .
|
|
||||||
$rc->db->table_name($this->table) .
|
|
||||||
'(flags, label, host, port, username, password, delimiter, user_id, iid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process boolean fields
|
|
||||||
$flags = 0;
|
|
||||||
if (get_input_value('_ident_switch_form_enabled', RCUBE_INPUT_POST))
|
|
||||||
$flags |= $this->db_enabled;
|
|
||||||
if (get_input_value('_ident_switch_form_secure', RCUBE_INPUT_POST))
|
|
||||||
$flags |= $this->db_secure;
|
|
||||||
|
|
||||||
$rc->db->query(
|
|
||||||
$sql,
|
|
||||||
$flags,
|
|
||||||
get_input_value('_ident_switch_form_label', RCUBE_INPUT_POST),
|
|
||||||
get_input_value('_ident_switch_form_host', RCUBE_INPUT_POST),
|
|
||||||
get_input_value('_ident_switch_form_port', RCUBE_INPUT_POST),
|
|
||||||
get_input_value('_ident_switch_form_username', RCUBE_INPUT_POST),
|
|
||||||
$rc->encrypt(get_input_value('_ident_switch_form_password', RCUBE_INPUT_POST)),
|
|
||||||
get_input_value('_ident_switch_form_delimiter', RCUBE_INPUT_POST),
|
|
||||||
$rc->user->ID,
|
|
||||||
$args['id']
|
|
||||||
);
|
|
||||||
|
|
||||||
return $args;
|
|
||||||
}
|
|
||||||
|
|
||||||
function on_identity_delete($args)
|
|
||||||
{
|
|
||||||
$rc = rcmail::get_instance();
|
|
||||||
|
|
||||||
$sql = 'DELETE FROM ' . $rc->db->table_name($this->table) . ' WHERE iid = ? AND user_id = ?';
|
|
||||||
$rc->db->query($sql, $args['id'], $rc->user->ID);
|
|
||||||
|
|
||||||
// TODO: Affected rows count for log
|
|
||||||
|
|
||||||
return $args;
|
|
||||||
}
|
|
||||||
|
|
||||||
function on_switch()
|
|
||||||
{
|
|
||||||
$rc = rcmail::get_instance();
|
|
||||||
|
|
||||||
$my_postfix = '_iswitch';
|
|
||||||
$my_postfix_len = strlen($my_postfix);
|
|
||||||
|
|
||||||
error_log('Switcing account!'); // TODO: Add nornal logging here!
|
|
||||||
|
|
||||||
$identId = rcube_utils::get_input_value('_ident-id', RCUBE_INPUT_POST);
|
|
||||||
error_log($identId);
|
|
||||||
|
|
||||||
if (-1 == $identId)
|
|
||||||
{ // Switch to main account
|
|
||||||
foreach ($_SESSION as $k => $v)
|
|
||||||
{
|
|
||||||
if (strncasecmp($k, 'storage', 7) === 0 && substr_compare($k, $my_postfix, -$my_postfix_len, $my_postfix_len) === 0)
|
|
||||||
{
|
|
||||||
error_log($k . '=>' . $v);
|
|
||||||
$_SESSION[$k] = $_SESSION[$k . $my_postfix];
|
|
||||||
$rc->session->remove($k . $my_postfix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$_SESSION['username'] = $rc->user->data['username'];
|
|
||||||
$_SESSION['password'] = $_SESSION['password' . $my_postfix];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$sql = 'SELECT host, flags, port, username, password FROM ' . $rc->db->table_name($this->table) . ' WHERE id = ? AND user_id = ?';
|
|
||||||
$q = $rc->db->query($sql, $identId ,$rc->user->ID);
|
|
||||||
$r = $rc->db->fetch_assoc($q);
|
|
||||||
if (is_array($r))
|
|
||||||
{
|
|
||||||
$port = $r['port'];
|
|
||||||
$ssl = false;
|
|
||||||
if ($r['flags'] & $this->db_secure)
|
|
||||||
{
|
|
||||||
$ssl = true;
|
|
||||||
if (!$port)
|
|
||||||
$port = 993; // Default SSL/TLS port here!
|
|
||||||
}
|
|
||||||
if (!$port)
|
|
||||||
$port = 143; // Default port here!
|
|
||||||
|
|
||||||
// If we are in default account now
|
|
||||||
// save everything with STORAGE
|
|
||||||
if ($_SESSION['username'] == $rc->user->data['username'])
|
|
||||||
{
|
|
||||||
foreach ($_SESSION as $k => $v)
|
|
||||||
{
|
|
||||||
if (strncasecmp($k, 'storage', 7) === 0 && substr_compare($k, $my_postfix, -$my_postfix_len, $my_postfix_len) !== 0)
|
|
||||||
{
|
|
||||||
error_log($k . '=>' . $v);
|
|
||||||
$_SESSION[$k . $my_postfix] = $_SESSION[$k];
|
|
||||||
$rc->session->remove($k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$_SESSION['password' . $my_postfix] = $_SESSION['password'];
|
|
||||||
|
|
||||||
$_SESSION['storage_host'] = $r['host'];
|
|
||||||
$_SESSION['storage_ssl'] = $ssl;
|
|
||||||
$_SESSION['storage_port'] = $port;
|
|
||||||
$_SESSION['username'] = $r['username'];
|
|
||||||
$_SESSION['password'] = $r['password'];
|
|
||||||
|
|
||||||
$rc->session->remove('folders');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// TODO: Show message in browser
|
|
||||||
error_log('Requested account not found!');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run initial check and pass counts via env
|
||||||
|
$this->checker->check_new_mail([]);
|
||||||
|
$counts = $_SESSION['ident_switch_counts'] ?? [];
|
||||||
|
$initialCounts = [];
|
||||||
|
foreach ($counts as $cIid => $info) {
|
||||||
|
$initialCounts[$cIid] = [
|
||||||
|
'unseen' => $info['unseen'],
|
||||||
|
'baseline' => $info['baseline'] ?? $info['unseen'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$rc->output->set_env('ident_switch_initial_counts', $initialCounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
$rc->output->redirect(
|
/**
|
||||||
array(
|
* Handle refresh hook: check new mail on secondary identities.
|
||||||
'_task' => 'mail',
|
*
|
||||||
'_mbox' => rcube_utils::get_input_value('_mbox', RCUBE_INPUT_GET),
|
* @param array $args Hook arguments (empty for refresh).
|
||||||
)
|
* @return array Unmodified hook arguments.
|
||||||
);
|
*/
|
||||||
|
public function on_refresh(array $args): array
|
||||||
|
{
|
||||||
|
$rc = rcmail::get_instance();
|
||||||
|
if (!$rc->config->get('ident_switch.check_mail', true)) {
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->checker->check_new_mail($args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle smtp_connect hook: configure SMTP settings for the active account.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing SMTP connection parameters.
|
||||||
|
* @return array Modified hook arguments with updated SMTP settings.
|
||||||
|
*/
|
||||||
|
public function on_smtp_connect(array $args): array
|
||||||
|
{
|
||||||
|
return $this->switcher->configure_smtp($args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle managesieve_connect hook: configure Sieve settings for the active account.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing Sieve connection parameters.
|
||||||
|
* @return array Modified hook arguments with updated Sieve settings.
|
||||||
|
*/
|
||||||
|
public function on_managesieve_connect(array $args): array
|
||||||
|
{
|
||||||
|
return $this->switcher->configure_managesieve($args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle identity_form hook: add plugin-specific fields to the identity editor.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing 'record' with identity data.
|
||||||
|
* @return array Modified hook arguments with added form sections.
|
||||||
|
*/
|
||||||
|
public function on_identity_form(array $args): array
|
||||||
|
{
|
||||||
|
return $this->form->on_identity_form($args, $this->preconfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle identity_update hook: validate and save plugin fields on identity edit.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing 'id' and 'record' with identity data.
|
||||||
|
* @return array Modified hook arguments, with 'abort' set on validation failure.
|
||||||
|
*/
|
||||||
|
public function on_identity_update(array $args): array
|
||||||
|
{
|
||||||
|
return $this->form->on_identity_update($args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle identity_create hook: validate plugin fields before identity creation.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing 'record' with identity data.
|
||||||
|
* @return array Modified hook arguments, with 'abort' set on validation failure.
|
||||||
|
*/
|
||||||
|
public function on_identity_create(array $args): array
|
||||||
|
{
|
||||||
|
return $this->form->on_identity_create($args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle identity_create_after hook: persist plugin data after identity creation.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing 'id' (new identity_id) and 'record'.
|
||||||
|
* @return array Unmodified hook arguments.
|
||||||
|
*/
|
||||||
|
public function on_identity_create_after(array $args): array
|
||||||
|
{
|
||||||
|
return $this->form->on_identity_create_after($args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle identity_delete hook: remove plugin data when an identity is deleted.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing 'id' of the identity being deleted.
|
||||||
|
* @return array Unmodified hook arguments.
|
||||||
|
*/
|
||||||
|
public function on_identity_delete(array $args): array
|
||||||
|
{
|
||||||
|
return $this->form->on_identity_delete($args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle template_object_composeheaders hook: filter and fix identity selection.
|
||||||
|
*
|
||||||
|
* Restricts the From dropdown to identities belonging to the active account
|
||||||
|
* (the account itself + its aliases). When impersonating, also pre-selects
|
||||||
|
* the correct identity.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing form element 'id'.
|
||||||
|
*/
|
||||||
|
public function on_template_object_composeheaders(array $args): void
|
||||||
|
{
|
||||||
|
if ($args['id'] !== '_from') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rc = rcmail::get_instance();
|
||||||
|
$userId = $rc->user->ID;
|
||||||
|
$isImpersonating = strcasecmp($_SESSION['username'], $rc->user->data['username']) !== 0;
|
||||||
|
|
||||||
|
// Pre-select the active identity when impersonating
|
||||||
|
if ($isImpersonating) {
|
||||||
|
if (isset($_SESSION['iid' . self::MY_POSTFIX])) {
|
||||||
|
$iid = $_SESSION['iid' . self::MY_POSTFIX];
|
||||||
|
$rc->output->add_script("plugin_switchIdent_fixIdent({$iid});", 'docready');
|
||||||
|
} else {
|
||||||
|
self::write_log('Special session variable with active identity ID not found.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute allowed identity IDs for the From dropdown
|
||||||
|
$iidSession = $_SESSION['iid' . self::MY_POSTFIX] ?? null;
|
||||||
|
|
||||||
|
if ($isImpersonating && is_numeric($iidSession) && (int)$iidSession > 0) {
|
||||||
|
// Switched to a secondary account: allow its identity + aliases
|
||||||
|
$sql = 'SELECT id FROM ' . $rc->db->table_name(self::TABLE)
|
||||||
|
. ' WHERE iid = ? AND user_id = ? AND parent_id IS NULL';
|
||||||
|
$q = $rc->db->query($sql, (int)$iidSession, $userId);
|
||||||
|
$r = $rc->db->fetch_assoc($q);
|
||||||
|
|
||||||
|
if ($r) {
|
||||||
|
$accountId = (int)$r['id'];
|
||||||
|
$sql = 'SELECT iid FROM ' . $rc->db->table_name(self::TABLE)
|
||||||
|
. ' WHERE (id = ? OR parent_id = ?) AND user_id = ? AND flags & ? > 0';
|
||||||
|
$q = $rc->db->query($sql, $accountId, $accountId, $userId, self::DB_ENABLED);
|
||||||
|
$allowed = [];
|
||||||
|
while ($row = $rc->db->fetch_assoc($q)) {
|
||||||
|
$allowed[] = (int)$row['iid'];
|
||||||
|
}
|
||||||
|
$rc->output->set_env('ident_switch_allowed_identities', $allowed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Primary account: exclude identities belonging to enabled accounts/aliases
|
||||||
|
$sql = 'SELECT iid FROM ' . $rc->db->table_name(self::TABLE)
|
||||||
|
. ' WHERE user_id = ? AND flags & ? > 0';
|
||||||
|
$q = $rc->db->query($sql, $userId, self::DB_ENABLED);
|
||||||
|
$excluded = [];
|
||||||
|
while ($row = $rc->db->fetch_assoc($q)) {
|
||||||
|
$excluded[] = (int)$row['iid'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($excluded)) {
|
||||||
|
$sql = 'SELECT identity_id FROM ' . $rc->db->table_name('identities')
|
||||||
|
. ' WHERE user_id = ? AND del = 0';
|
||||||
|
$q = $rc->db->query($sql, $userId);
|
||||||
|
$allowed = [];
|
||||||
|
while ($row = $rc->db->fetch_assoc($q)) {
|
||||||
|
$iid = (int)$row['identity_id'];
|
||||||
|
if (!in_array($iid, $excluded)) {
|
||||||
|
$allowed[] = $iid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$rc->output->set_env('ident_switch_allowed_identities', $allowed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle preferences_list hook: customize special folders form for remote accounts.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing 'section' and 'blocks' with form data.
|
||||||
|
* @return array Modified hook arguments with updated folder selections.
|
||||||
|
*/
|
||||||
|
public function on_special_folders_form(array $args): array
|
||||||
|
{
|
||||||
|
return $this->switcher->get_special_folders_form($args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle preferences_save hook: persist special folder assignments for remote accounts.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing 'section' and 'prefs' with folder data.
|
||||||
|
* @return array Modified hook arguments, with 'abort' set to prevent default save.
|
||||||
|
*/
|
||||||
|
public function on_special_folders_update(array $args): array
|
||||||
|
{
|
||||||
|
return $this->switcher->save_special_folders($args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the account switch action (AJAX).
|
||||||
|
*/
|
||||||
|
public function on_switch(): void
|
||||||
|
{
|
||||||
|
$this->switcher->switch_account();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trim a string, returning null if the result is empty.
|
||||||
|
*
|
||||||
|
* @param string|null $str Input string.
|
||||||
|
* @return string|null Trimmed string or null if empty.
|
||||||
|
*/
|
||||||
|
public static function ntrim(?string $str): ?string
|
||||||
|
{
|
||||||
|
if ($str === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$s = trim($str);
|
||||||
|
return $s !== '' ? $s : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse scheme prefix (ssl://, tls://) from a host string.
|
||||||
|
*
|
||||||
|
* @param string $host Host string, optionally prefixed with ssl:// or tls://.
|
||||||
|
* @return array{scheme: string, host: string} Parsed scheme and bare host.
|
||||||
|
*/
|
||||||
|
public static function parse_host_scheme(string $host): array
|
||||||
|
{
|
||||||
|
$lower = strtolower($host);
|
||||||
|
if (str_starts_with($lower, 'ssl://')) {
|
||||||
|
return ['scheme' => 'ssl', 'host' => substr($host, 6)];
|
||||||
|
}
|
||||||
|
if (str_starts_with($lower, 'tls://')) {
|
||||||
|
return ['scheme' => 'tls', 'host' => substr($host, 6)];
|
||||||
|
}
|
||||||
|
return ['scheme' => '', 'host' => $host];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve username for an identity: use stored username or fall back to email.
|
||||||
|
*
|
||||||
|
* @param integer $iid Identity ID.
|
||||||
|
* @param string|null $username Stored username (may be empty).
|
||||||
|
* @return string Resolved username.
|
||||||
|
*/
|
||||||
|
public static function resolve_username(int $iid, ?string $username): string
|
||||||
|
{
|
||||||
|
if (!empty($username)) {
|
||||||
|
return $username;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rc = rcmail::get_instance();
|
||||||
|
$sql = 'SELECT email FROM ' . $rc->db->table_name('identities') . ' WHERE identity_id = ?';
|
||||||
|
$q = $rc->db->query($sql, $iid);
|
||||||
|
$r = $rc->db->fetch_assoc($q);
|
||||||
|
return $r['email'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a message to the plugin's log file.
|
||||||
|
*
|
||||||
|
* @param string $txt Log message.
|
||||||
|
*/
|
||||||
|
public static function write_log(string $txt): void
|
||||||
|
{
|
||||||
|
rcmail::get_instance()->write_log('ident_switch', $txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a debug message (only when ident_switch.debug is enabled).
|
||||||
|
*
|
||||||
|
* @param string $txt Log message.
|
||||||
|
*/
|
||||||
|
public static function debug_log(string $txt): void
|
||||||
|
{
|
||||||
|
if (rcmail::get_instance()->config->get('ident_switch.debug', false)) {
|
||||||
|
rcmail::get_instance()->write_log('ident_switch', '[DEBUG] ' . $txt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
267
lib/IdentSwitchChecker.php
Normal file
267
lib/IdentSwitchChecker.php
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ident_switch - Background mail checker.
|
||||||
|
*
|
||||||
|
* Checks new mail across secondary identities during the refresh
|
||||||
|
* cycle and sends unread counts + notifications to the client.
|
||||||
|
* When impersonating a secondary account, also checks the primary account.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2026 Gecka
|
||||||
|
*
|
||||||
|
* Licensed under AGPL-3.0+.
|
||||||
|
*
|
||||||
|
* @url https://github.com/Gecka-apps/ident_switch
|
||||||
|
*/
|
||||||
|
class IdentSwitchChecker
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Called on each refresh cycle.
|
||||||
|
*
|
||||||
|
* Builds the list of identities to check (excluding the currently active one,
|
||||||
|
* including the primary account when impersonating), then checks them
|
||||||
|
* in round-robin or all-at-once mode.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments (empty for refresh hook).
|
||||||
|
* @return array Unmodified hook arguments.
|
||||||
|
*/
|
||||||
|
public function check_new_mail(array $args): array
|
||||||
|
{
|
||||||
|
$rc = rcmail::get_instance();
|
||||||
|
|
||||||
|
$identities = $this->get_checkable_identities($rc);
|
||||||
|
|
||||||
|
// Exclude the currently active secondary identity (RC already checks it)
|
||||||
|
$activeIid = (int)($_SESSION['iid' . ident_switch::MY_POSTFIX] ?? -1);
|
||||||
|
$identities = array_values(array_filter($identities, function ($id) use ($activeIid) {
|
||||||
|
return (int)$id['iid'] !== $activeIid;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// When impersonating, also check the primary account
|
||||||
|
$isImpersonating = strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0;
|
||||||
|
if ($isImpersonating) {
|
||||||
|
$primary = $this->get_primary_identity($rc);
|
||||||
|
if ($primary) {
|
||||||
|
$identities[] = $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($identities)) {
|
||||||
|
$this->send_counts($rc);
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rc->config->get('ident_switch.round_robin', false)) {
|
||||||
|
// Round-robin: check one identity per refresh cycle
|
||||||
|
$index = ($_SESSION['ident_switch_check_index'] ?? -1) + 1;
|
||||||
|
if ($index >= count($identities)) {
|
||||||
|
$index = 0;
|
||||||
|
}
|
||||||
|
$_SESSION['ident_switch_check_index'] = $index;
|
||||||
|
$this->check_identity($rc, $identities[$index]);
|
||||||
|
} else {
|
||||||
|
foreach ($identities as $identity) {
|
||||||
|
$this->check_identity($rc, $identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->send_counts($rc);
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check a single identity for unseen messages and notify if new mail.
|
||||||
|
*
|
||||||
|
* @param rcmail $rc Roundcube instance.
|
||||||
|
* @param array $identity Identity record from the database.
|
||||||
|
*/
|
||||||
|
private function check_identity(rcmail $rc, array $identity): void
|
||||||
|
{
|
||||||
|
$counts = $_SESSION['ident_switch_counts'] ?? [];
|
||||||
|
$previousCount = $counts[$identity['iid']]['unseen'] ?? 0;
|
||||||
|
|
||||||
|
$count = $this->check_unseen($rc, $identity, $previousCount);
|
||||||
|
|
||||||
|
ident_switch::write_log("Check identity {$identity['iid']} ({$identity['email']}): unseen={$count}, previous={$previousCount}");
|
||||||
|
|
||||||
|
// Set baseline on first check; preserve it across subsequent checks
|
||||||
|
$baseline = $counts[$identity['iid']]['baseline'] ?? $count;
|
||||||
|
|
||||||
|
$counts[$identity['iid']] = [
|
||||||
|
'unseen' => $count,
|
||||||
|
'baseline' => $baseline,
|
||||||
|
'checked_at' => time(),
|
||||||
|
];
|
||||||
|
$_SESSION['ident_switch_counts'] = $counts;
|
||||||
|
|
||||||
|
if ($count > $previousCount) {
|
||||||
|
$this->send_notification($rc, $identity, $count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to an identity's IMAP server and return INBOX unseen count.
|
||||||
|
*
|
||||||
|
* @param rcmail $rc Roundcube instance.
|
||||||
|
* @param array $identity Identity DB record.
|
||||||
|
* @param integer $previousCount Previous unseen count (returned on error).
|
||||||
|
* @return integer Unseen message count.
|
||||||
|
*/
|
||||||
|
private function check_unseen(rcmail $rc, array $identity, int $previousCount): int
|
||||||
|
{
|
||||||
|
$imap = new rcube_imap_generic();
|
||||||
|
|
||||||
|
$parsed = ident_switch::parse_host_scheme($identity['imap_host'] ?: 'localhost');
|
||||||
|
$host = $parsed['host'];
|
||||||
|
$ssl = $parsed['scheme'] ?: null;
|
||||||
|
|
||||||
|
if (!$ssl && !empty($identity['flags']) && ($identity['flags'] & ident_switch::DB_SECURE_IMAP_TLS)) {
|
||||||
|
$ssl = 'tls'; // Backward compat: old records without scheme in host
|
||||||
|
}
|
||||||
|
|
||||||
|
$def_port = ($ssl === 'ssl') ? 993 : 143;
|
||||||
|
$port = $identity['imap_port'] ?: $def_port;
|
||||||
|
|
||||||
|
$username = $identity['username'] ?: $identity['email'];
|
||||||
|
$password = $rc->decrypt($identity['password']);
|
||||||
|
if ($password === false) {
|
||||||
|
ident_switch::write_log("Failed to decrypt password for identity {$identity['iid']}");
|
||||||
|
return $previousCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $imap->connect($host, $username, $password, [
|
||||||
|
'port' => $port,
|
||||||
|
'ssl_mode' => $ssl,
|
||||||
|
'timeout' => 5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
ident_switch::write_log("Failed to check mail for identity {$identity['iid']}: " . $imap->error);
|
||||||
|
return $previousCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = $imap->status('INBOX', ['UNSEEN']);
|
||||||
|
$unseen = $status['UNSEEN'] ?? 0;
|
||||||
|
|
||||||
|
$imap->closeConnection();
|
||||||
|
|
||||||
|
return $unseen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a virtual identity record for the primary account.
|
||||||
|
*
|
||||||
|
* When the user has switched to a secondary account, the primary account's
|
||||||
|
* connection details are saved in session with the MY_POSTFIX suffix.
|
||||||
|
*
|
||||||
|
* @param rcmail $rc Roundcube instance.
|
||||||
|
* @return array|null Identity-like array, or null if session data is missing.
|
||||||
|
*/
|
||||||
|
private function get_primary_identity(rcmail $rc): ?array
|
||||||
|
{
|
||||||
|
$postfix = ident_switch::MY_POSTFIX;
|
||||||
|
|
||||||
|
if (!isset($_SESSION['password' . $postfix])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = $_SESSION['storage_host' . $postfix] ?? 'localhost';
|
||||||
|
$port = $_SESSION['storage_port' . $postfix] ?? 143;
|
||||||
|
$ssl = $_SESSION['storage_ssl' . $postfix] ?? null;
|
||||||
|
|
||||||
|
// Prepend protocol prefix so check_unseen() can parse it
|
||||||
|
if ($ssl === 'ssl' && !str_starts_with(strtolower($host), 'ssl://')) {
|
||||||
|
$host = 'ssl://' . $host;
|
||||||
|
} elseif ($ssl === 'tls' && !str_starts_with(strtolower($host), 'tls://')) {
|
||||||
|
$host = 'tls://' . $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
$primaryIdentity = $rc->user->get_identity();
|
||||||
|
$primaryName = !empty($primaryIdentity['name']) ? $primaryIdentity['name'] : null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'iid' => 0,
|
||||||
|
'imap_host' => $host,
|
||||||
|
'imap_port' => $port,
|
||||||
|
'flags' => 0,
|
||||||
|
'username' => $rc->user->data['username'],
|
||||||
|
'password' => $_SESSION['password' . $postfix],
|
||||||
|
'email' => $rc->user->data['username'],
|
||||||
|
'label' => $_SESSION['global_alias'] ?? $primaryName ?? $rc->user->data['username'],
|
||||||
|
'notify_basic' => null,
|
||||||
|
'notify_sound' => null,
|
||||||
|
'notify_desktop' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all enabled identities that have mail checking enabled.
|
||||||
|
*
|
||||||
|
* @param rcmail $rc Roundcube instance.
|
||||||
|
* @return array List of identity records.
|
||||||
|
*/
|
||||||
|
private function get_checkable_identities(rcmail $rc): array
|
||||||
|
{
|
||||||
|
$sql = 'SELECT isw.iid, isw.imap_host, isw.imap_port, isw.flags, '
|
||||||
|
. 'isw.username, isw.password, isw.label, '
|
||||||
|
. 'isw.notify_basic, isw.notify_sound, isw.notify_desktop, '
|
||||||
|
. 'ii.email '
|
||||||
|
. 'FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' isw '
|
||||||
|
. 'INNER JOIN ' . $rc->db->table_name('identities') . ' ii ON isw.iid = ii.identity_id '
|
||||||
|
. 'WHERE isw.user_id = ? AND isw.flags & ? > 0 AND isw.notify_check = ? AND isw.parent_id IS NULL';
|
||||||
|
|
||||||
|
$q = $rc->db->query($sql, $rc->user->ID, ident_switch::DB_ENABLED, ident_switch::NOTIFY_CHECK_ENABLED);
|
||||||
|
|
||||||
|
$identities = [];
|
||||||
|
while ($r = $rc->db->fetch_assoc($q)) {
|
||||||
|
$identities[] = $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $identities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send all cached unseen counts to client JS.
|
||||||
|
*
|
||||||
|
* @param rcmail $rc Roundcube instance.
|
||||||
|
*/
|
||||||
|
private function send_counts(rcmail $rc): void
|
||||||
|
{
|
||||||
|
$counts = $_SESSION['ident_switch_counts'] ?? [];
|
||||||
|
$data = [];
|
||||||
|
foreach ($counts as $iid => $info) {
|
||||||
|
$data[$iid] = [
|
||||||
|
'unseen' => $info['unseen'],
|
||||||
|
'baseline' => $info['baseline'] ?? $info['unseen'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rc->output->command('plugin.ident_switch.update_counts', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification command to client for a specific identity.
|
||||||
|
*
|
||||||
|
* @param rcmail $rc Roundcube instance.
|
||||||
|
* @param array $identity Identity record.
|
||||||
|
* @param integer $count New unseen count.
|
||||||
|
*/
|
||||||
|
private function send_notification(rcmail $rc, array $identity, int $count): void
|
||||||
|
{
|
||||||
|
$basic = $identity['notify_basic'] ?? $rc->config->get('newmail_notifier_basic', false);
|
||||||
|
$sound = $identity['notify_sound'] ?? $rc->config->get('newmail_notifier_sound', false);
|
||||||
|
$desktop = $identity['notify_desktop'] ?? $rc->config->get('newmail_notifier_desktop', false);
|
||||||
|
|
||||||
|
$label = $identity['label'] ?: $identity['email'];
|
||||||
|
|
||||||
|
$rc->output->command('plugin.ident_switch.notify', [
|
||||||
|
'iid' => $identity['iid'],
|
||||||
|
'label' => $label,
|
||||||
|
'count' => $count,
|
||||||
|
'basic' => (bool)$basic,
|
||||||
|
'sound' => (bool)$sound,
|
||||||
|
'desktop' => (bool)$desktop,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
1337
lib/IdentSwitchForm.php
Normal file
1337
lib/IdentSwitchForm.php
Normal file
File diff suppressed because it is too large
Load Diff
131
lib/IdentSwitchPreconfig.php
Normal file
131
lib/IdentSwitchPreconfig.php
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ident_switch - Preconfiguration handler.
|
||||||
|
*
|
||||||
|
* Loads and applies domain-based preconfigured mail settings
|
||||||
|
* from the plugin configuration file.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2016-2022 Boris Gulay
|
||||||
|
* Copyright (C) 2026 Gecka
|
||||||
|
*
|
||||||
|
* Original code licensed under GPL-3.0+.
|
||||||
|
* New contributions licensed under AGPL-3.0+.
|
||||||
|
*
|
||||||
|
* @url https://github.com/Gecka-apps/ident_switch
|
||||||
|
*/
|
||||||
|
class IdentSwitchPreconfig
|
||||||
|
{
|
||||||
|
private ident_switch $plugin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param ident_switch $plugin Parent plugin instance.
|
||||||
|
*/
|
||||||
|
public function __construct(ident_switch $plugin)
|
||||||
|
{
|
||||||
|
$this->plugin = $plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load preconfigured settings for a domain from config.
|
||||||
|
*
|
||||||
|
* @param string $email Email address to extract domain from.
|
||||||
|
* @return array|false Preconfig array for the domain, or false if not found.
|
||||||
|
*/
|
||||||
|
public function get(string $email): array|false
|
||||||
|
{
|
||||||
|
$dom = substr(strstr($email, '@'), 1);
|
||||||
|
if (!$dom) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->plugin->load_config();
|
||||||
|
|
||||||
|
$cfg = rcmail::get_instance()->config->get('ident_switch.preconfig', []);
|
||||||
|
$cfg = $cfg[$dom] ?? $cfg['*'] ?? null;
|
||||||
|
|
||||||
|
if ($cfg) {
|
||||||
|
if (empty($cfg['imap_host']) && empty($cfg['host'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $cfg ?: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply preconfigured settings to an identity form record.
|
||||||
|
*
|
||||||
|
* Parses IMAP, SMTP, and Sieve host URLs to extract scheme, host,
|
||||||
|
* and port, then sets the username and delimiter based on config values.
|
||||||
|
*
|
||||||
|
* @param array $record Identity record to modify (passed by reference).
|
||||||
|
* @return boolean True if the preconfig is readonly, false otherwise.
|
||||||
|
*/
|
||||||
|
public function apply(array &$record): bool
|
||||||
|
{
|
||||||
|
$email = $record['email'] ?? '';
|
||||||
|
if (empty($email)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = $this->get($email);
|
||||||
|
if (is_array($cfg)) {
|
||||||
|
ident_switch::write_log("Applying predefined configuration for '{$email}'.");
|
||||||
|
|
||||||
|
// Parse each protocol URL into host, security, and port
|
||||||
|
$protocols = [
|
||||||
|
'imap' => $cfg['imap_host'] ?? $cfg['host'] ?? '',
|
||||||
|
'smtp' => $cfg['smtp_host'] ?? $cfg['host'] ?? '',
|
||||||
|
'sieve' => $cfg['sieve_host'] ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($protocols as $proto => $url) {
|
||||||
|
if (empty($url)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$urlArr = parse_url($url);
|
||||||
|
$host = !empty($urlArr['host']) ? rcube::Q($urlArr['host'], 'url') : '';
|
||||||
|
$scheme = strtolower($urlArr['scheme'] ?? '');
|
||||||
|
|
||||||
|
$record["ident_switch.form.{$proto}.host"] = $host;
|
||||||
|
$record["ident_switch.form.{$proto}.security"] = in_array($scheme, ['ssl', 'tls']) ? $scheme : '';
|
||||||
|
$record["ident_switch.form.{$proto}.port"] = !empty($urlArr['port']) ? intval($urlArr['port']) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$loginSet = false;
|
||||||
|
if (!empty($cfg['user'])) {
|
||||||
|
match (strtoupper($cfg['user'])) {
|
||||||
|
'EMAIL' => ($record['ident_switch.form.imap.username'] = $email) && ($loginSet = true),
|
||||||
|
'MBOX' => ($record['ident_switch.form.imap.username'] = strstr($email, '@', true)) && ($loginSet = true),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($cfg['readonly'])) {
|
||||||
|
$record['ident_switch.form.common.readonly'] = $loginSet ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP folder hierarchy delimiter (null or absent = auto-detect)
|
||||||
|
if (isset($cfg['delimiter'])) {
|
||||||
|
$record['ident_switch.form.imap.delimiter'] = $cfg['delimiter'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification defaults from preconfig
|
||||||
|
if (isset($cfg['notify_check'])) {
|
||||||
|
$record['ident_switch.form.notify.check'] = $cfg['notify_check'] ? 1 : 0;
|
||||||
|
}
|
||||||
|
foreach (['notify_basic', 'notify_sound', 'notify_desktop'] as $key) {
|
||||||
|
if (isset($cfg[$key])) {
|
||||||
|
$formKey = 'ident_switch.form.notify.' . substr($key, 7);
|
||||||
|
$record[$formKey] = $cfg[$key] === null ? '' : ($cfg[$key] ? '1' : '0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool)($cfg['readonly'] ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
411
lib/IdentSwitchSwitcher.php
Normal file
411
lib/IdentSwitchSwitcher.php
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ident_switch - Account switching handler.
|
||||||
|
*
|
||||||
|
* Manages switching between mail accounts, SMTP connection
|
||||||
|
* configuration, and special folder assignments.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2016-2022 Boris Gulay
|
||||||
|
* Copyright (C) 2019 Christian Landvogt
|
||||||
|
* Copyright (C) 2022 Mickael
|
||||||
|
* Copyright (C) 2026 Gecka
|
||||||
|
*
|
||||||
|
* Original code licensed under GPL-3.0+.
|
||||||
|
* New contributions licensed under AGPL-3.0+.
|
||||||
|
*
|
||||||
|
* @url https://github.com/Gecka-apps/ident_switch
|
||||||
|
*/
|
||||||
|
class IdentSwitchSwitcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the account switch action (AJAX).
|
||||||
|
*
|
||||||
|
* Saves current account state to session, loads the target account's
|
||||||
|
* IMAP/SMTP configuration, and redirects to INBOX.
|
||||||
|
* When switching back to default (id=-1), restores the original session state.
|
||||||
|
*/
|
||||||
|
public function switch_account(): void
|
||||||
|
{
|
||||||
|
$rc = rcmail::get_instance();
|
||||||
|
|
||||||
|
$my_postfix_len = strlen(ident_switch::MY_POSTFIX);
|
||||||
|
$identId = (int)rcube_utils::get_input_value('_ident-id', rcube_utils::INPUT_POST);
|
||||||
|
|
||||||
|
$rc->session->remove('folders');
|
||||||
|
$rc->session->remove('unseen_count');
|
||||||
|
|
||||||
|
// Reset baseline for the target account so delta goes back to 0
|
||||||
|
$this->reset_baseline($identId === -1 ? 0 : null, $rc, $identId);
|
||||||
|
|
||||||
|
if ($identId === -1) {
|
||||||
|
// Switch to main account
|
||||||
|
ident_switch::write_log('Switching mailbox back to default.');
|
||||||
|
|
||||||
|
// Restore everything with STORAGE*my_postfix
|
||||||
|
foreach ($_SESSION as $k => $v) {
|
||||||
|
if (str_starts_with(strtolower($k), 'storage') && str_ends_with($k, ident_switch::MY_POSTFIX)) {
|
||||||
|
$realKey = substr($k, 0, -$my_postfix_len);
|
||||||
|
$_SESSION[$realKey] = $_SESSION[$k];
|
||||||
|
$rc->session->remove($k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['imap_delimiter'] = $_SESSION['imap_delimiter' . ident_switch::MY_POSTFIX] ?? null;
|
||||||
|
$_SESSION['username'] = $rc->user->data['username'];
|
||||||
|
$_SESSION['password'] = $_SESSION['password' . ident_switch::MY_POSTFIX];
|
||||||
|
$_SESSION['iid' . ident_switch::MY_POSTFIX] = -1;
|
||||||
|
|
||||||
|
foreach (rcube_storage::$folder_types as $type) {
|
||||||
|
$otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX;
|
||||||
|
if (isset($_SESSION[$otherKey])) {
|
||||||
|
$rc->session->remove($otherKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$sql = 'SELECT imap_host, flags, imap_port, imap_delimiter, drafts_mbox, sent_mbox, junk_mbox, trash_mbox, username, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE id = ? AND user_id = ?';
|
||||||
|
$q = $rc->db->query($sql, $identId, $rc->user->ID);
|
||||||
|
$r = $rc->db->fetch_assoc($q);
|
||||||
|
if (is_array($r)) {
|
||||||
|
$r['username'] = ident_switch::resolve_username((int)$r['iid'], $r['username']);
|
||||||
|
|
||||||
|
ident_switch::write_log("Switching mailbox to one for identity with ID = {$r['iid']} (username = '{$r['username']}').");
|
||||||
|
|
||||||
|
if ($_SESSION['username'] === $rc->user->data['username']) {
|
||||||
|
// If we are in default account now - save values
|
||||||
|
foreach ($_SESSION as $k => $v) {
|
||||||
|
if (str_starts_with(strtolower($k), 'storage') && !str_ends_with($k, ident_switch::MY_POSTFIX)) {
|
||||||
|
if (!isset($_SESSION[$k . ident_switch::MY_POSTFIX])) {
|
||||||
|
$_SESSION[$k . ident_switch::MY_POSTFIX] = $_SESSION[$k];
|
||||||
|
}
|
||||||
|
$rc->session->remove($k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['password', 'imap_delimiter'] as $k) {
|
||||||
|
if (!isset($_SESSION[$k . ident_switch::MY_POSTFIX])) {
|
||||||
|
$_SESSION[$k . ident_switch::MY_POSTFIX] = $_SESSION[$k];
|
||||||
|
}
|
||||||
|
$rc->session->remove($k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsed = ident_switch::parse_host_scheme($r['imap_host'] ?: 'localhost');
|
||||||
|
$host = $parsed['host'];
|
||||||
|
$ssl = $parsed['scheme'] ?: null;
|
||||||
|
|
||||||
|
if (!$ssl && ($r['flags'] & ident_switch::DB_SECURE_IMAP_TLS)) {
|
||||||
|
$ssl = 'tls'; // Backward compat: old records without scheme in host
|
||||||
|
}
|
||||||
|
|
||||||
|
$def_port = ($ssl === 'ssl') ? 993 : 143;
|
||||||
|
$port = $r['imap_port'] ?: $def_port;
|
||||||
|
|
||||||
|
$delimiter = $r['imap_delimiter'] ?: null;
|
||||||
|
|
||||||
|
$_SESSION['storage_host'] = $host;
|
||||||
|
$_SESSION['storage_ssl'] = $ssl;
|
||||||
|
$_SESSION['storage_port'] = $port;
|
||||||
|
$_SESSION['imap_delimiter'] = $delimiter;
|
||||||
|
$_SESSION['username'] = $r['username'];
|
||||||
|
$_SESSION['password'] = $r['password'];
|
||||||
|
$_SESSION['iid' . ident_switch::MY_POSTFIX] = $r['iid'];
|
||||||
|
|
||||||
|
foreach (rcube_storage::$folder_types as $type) {
|
||||||
|
if (!empty($r[$type . '_mbox'])) {
|
||||||
|
$otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX;
|
||||||
|
$_SESSION[$otherKey] = $r[$type . '_mbox'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ident_switch::write_log("Requested remote mailbox with ID = {$identId} not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$rc->output->redirect([
|
||||||
|
'_task' => 'mail',
|
||||||
|
'_mbox' => 'INBOX',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle smtp_connect hook: configure SMTP settings for the active account.
|
||||||
|
*
|
||||||
|
* Loads SMTP host, port, credentials, and TLS settings from the database
|
||||||
|
* for the currently selected identity.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing SMTP connection parameters.
|
||||||
|
* @return array Modified hook arguments with updated SMTP settings.
|
||||||
|
*/
|
||||||
|
public function configure_smtp(array $args): array
|
||||||
|
{
|
||||||
|
$iid = $_SESSION['iid' . ident_switch::MY_POSTFIX] ?? null;
|
||||||
|
if (!is_numeric($iid) || (int)$iid === -1) {
|
||||||
|
ident_switch::debug_log('SMTP: no active switch, resolving from _from header');
|
||||||
|
$requestFrom = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST);
|
||||||
|
if (empty($requestFrom)) {
|
||||||
|
ident_switch::debug_log('SMTP: no _from parameter, using default config');
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
$iid = intval($requestFrom);
|
||||||
|
if ($iid === 0) {
|
||||||
|
ident_switch::debug_log('SMTP: _from is not an integer, using default config');
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$rc = rcmail::get_instance();
|
||||||
|
|
||||||
|
$sql = 'SELECT parent_id, smtp_host, smtp_port, username, smtp_auth, smtp_username, smtp_password, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?';
|
||||||
|
$q = $rc->db->query($sql, $iid, $rc->user->ID);
|
||||||
|
$r = $rc->db->fetch_assoc($q);
|
||||||
|
if (is_array($r)) {
|
||||||
|
// If this is an alias, follow parent_id to get the parent's SMTP config
|
||||||
|
if (!empty($r['parent_id'])) {
|
||||||
|
ident_switch::debug_log("SMTP: identity {$iid} is alias, following parent_id={$r['parent_id']}");
|
||||||
|
$sql = 'SELECT smtp_host, smtp_port, username, smtp_auth, smtp_username, smtp_password, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE id = ? AND user_id = ?';
|
||||||
|
$q = $rc->db->query($sql, $r['parent_id'], $rc->user->ID);
|
||||||
|
$r = $rc->db->fetch_assoc($q);
|
||||||
|
if (!is_array($r)) {
|
||||||
|
ident_switch::debug_log("SMTP: parent account not found, using default config");
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
$iid = $r['iid'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$r['username'] = ident_switch::resolve_username($iid, $r['username']);
|
||||||
|
|
||||||
|
$authMode = (int)$r['smtp_auth'];
|
||||||
|
if ($authMode === ident_switch::SMTP_AUTH_CUSTOM) {
|
||||||
|
$args['smtp_user'] = $r['smtp_username'] ?: '';
|
||||||
|
$args['smtp_pass'] = $r['smtp_password'] ? ($rc->decrypt($r['smtp_password']) ?: '') : '';
|
||||||
|
} elseif ($authMode === ident_switch::SMTP_AUTH_IMAP) {
|
||||||
|
$args['smtp_user'] = $r['username'];
|
||||||
|
$args['smtp_pass'] = $rc->decrypt($r['password']) ?: '';
|
||||||
|
} else {
|
||||||
|
$args['smtp_user'] = '';
|
||||||
|
$args['smtp_pass'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host already contains scheme (ssl:// or tls://) from form
|
||||||
|
$smtpHost = $r['smtp_host'] ?: 'localhost';
|
||||||
|
$smtpPort = $r['smtp_port'] ?: 587;
|
||||||
|
$args['smtp_host'] = $smtpHost . ':' . $smtpPort;
|
||||||
|
|
||||||
|
$authLabel = match ($authMode) {
|
||||||
|
ident_switch::SMTP_AUTH_IMAP => 'imap',
|
||||||
|
ident_switch::SMTP_AUTH_NONE => 'none',
|
||||||
|
ident_switch::SMTP_AUTH_CUSTOM => 'custom',
|
||||||
|
default => "unknown({$authMode})",
|
||||||
|
};
|
||||||
|
ident_switch::debug_log("SMTP: iid={$iid}, host={$args['smtp_host']}, user={$args['smtp_user']}, auth={$authLabel}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle managesieve_connect hook: configure Sieve settings for the active account.
|
||||||
|
*
|
||||||
|
* Loads Sieve host, port, and credentials from the database
|
||||||
|
* for the currently selected identity.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing Sieve connection parameters.
|
||||||
|
* @return array Modified hook arguments with updated Sieve settings.
|
||||||
|
*/
|
||||||
|
public function configure_managesieve(array $args): array
|
||||||
|
{
|
||||||
|
$iid = $_SESSION['iid' . ident_switch::MY_POSTFIX] ?? null;
|
||||||
|
if (!is_numeric($iid) || (int)$iid === -1) {
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rc = rcmail::get_instance();
|
||||||
|
|
||||||
|
$sql = 'SELECT parent_id, sieve_host, sieve_port, sieve_auth, sieve_username, sieve_password, username, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?';
|
||||||
|
$q = $rc->db->query($sql, $iid, $rc->user->ID);
|
||||||
|
$r = $rc->db->fetch_assoc($q);
|
||||||
|
if (is_array($r)) {
|
||||||
|
// If this is an alias, follow parent_id to get the parent's Sieve config
|
||||||
|
if (!empty($r['parent_id'])) {
|
||||||
|
ident_switch::debug_log("Sieve: identity {$iid} is alias, following parent_id={$r['parent_id']}");
|
||||||
|
$sql = 'SELECT sieve_host, sieve_port, sieve_auth, sieve_username, sieve_password, username, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE id = ? AND user_id = ?';
|
||||||
|
$q = $rc->db->query($sql, $r['parent_id'], $rc->user->ID);
|
||||||
|
$r = $rc->db->fetch_assoc($q);
|
||||||
|
if (!is_array($r)) {
|
||||||
|
ident_switch::debug_log("Sieve: parent account not found, using default config");
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
$iid = $r['iid'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($r['sieve_host'])) {
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
$r['username'] = ident_switch::resolve_username($iid, $r['username']);
|
||||||
|
|
||||||
|
$sieveHost = $r['sieve_host'];
|
||||||
|
$sievePort = $r['sieve_port'] ?: 4190;
|
||||||
|
$args['host'] = $sieveHost . ':' . $sievePort;
|
||||||
|
|
||||||
|
$authMode = (int)$r['sieve_auth'];
|
||||||
|
if ($authMode === ident_switch::SIEVE_AUTH_CUSTOM) {
|
||||||
|
$args['user'] = $r['sieve_username'] ?: '';
|
||||||
|
$args['password'] = $r['sieve_password'] ? ($rc->decrypt($r['sieve_password']) ?: '') : '';
|
||||||
|
} elseif ($authMode === ident_switch::SIEVE_AUTH_IMAP) {
|
||||||
|
$args['user'] = $r['username'];
|
||||||
|
$args['password'] = $rc->decrypt($r['password']) ?: '';
|
||||||
|
} else {
|
||||||
|
$args['user'] = '';
|
||||||
|
$args['password'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
ident_switch::debug_log("Sieve: iid={$iid}, host={$args['host']}, user={$args['user']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle preferences_list hook: customize special folders form for remote accounts.
|
||||||
|
*
|
||||||
|
* When viewing folder preferences while impersonating, shows the remote account's
|
||||||
|
* special folder assignments instead of the default ones.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing 'section' and 'blocks' with form data.
|
||||||
|
* @return array Modified hook arguments with updated folder selections.
|
||||||
|
*/
|
||||||
|
public function get_special_folders_form(array $args): array
|
||||||
|
{
|
||||||
|
$rc = rcmail::get_instance();
|
||||||
|
|
||||||
|
if (
|
||||||
|
$args['section'] === 'folders'
|
||||||
|
&& strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0
|
||||||
|
) {
|
||||||
|
$no_override = array_flip((array)$rc->config->get('dont_override'));
|
||||||
|
$onchange = "if ($(this).val() == 'INBOX') $(this).val('')";
|
||||||
|
$select = $rc->folder_selector([
|
||||||
|
'noselection' => '---',
|
||||||
|
'realnames' => true,
|
||||||
|
'maxlength' => 30,
|
||||||
|
'folder_filter' => 'mail',
|
||||||
|
'folder_rights' => 'w',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sql = 'SELECT label FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?';
|
||||||
|
$q = $rc->db->query($sql, $_SESSION['iid' . ident_switch::MY_POSTFIX], $rc->user->ID);
|
||||||
|
$r = $rc->db->fetch_assoc($q);
|
||||||
|
$args['blocks']['main']['name'] .= ' (' . ($r['label'] ? rcube::Q($rc->gettext('server')) . ': ' . rcube::Q($r['label']) : 'remote') . ')';
|
||||||
|
|
||||||
|
foreach (rcube_storage::$folder_types as $type) {
|
||||||
|
if (isset($no_override[$type . '_mbox'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultKey = $type . '_mbox_default' . ident_switch::MY_POSTFIX;
|
||||||
|
$otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX;
|
||||||
|
$selected = $_SESSION[$otherKey] ?? $_SESSION[$defaultKey] ?? '';
|
||||||
|
$attr = ['id' => '_' . $type . '_mbox', 'name' => '_' . $type . '_mbox', 'onchange' => $onchange];
|
||||||
|
$args['blocks']['main']['options'][$type . '_mbox']['content'] = $select->show($selected, $attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle preferences_save hook: persist special folder assignments for remote accounts.
|
||||||
|
*
|
||||||
|
* Saves folder preferences to the plugin's database table instead of the default
|
||||||
|
* Roundcube preferences when impersonating a remote account.
|
||||||
|
*
|
||||||
|
* @param array $args Hook arguments containing 'section' and 'prefs' with folder data.
|
||||||
|
* @return array Modified hook arguments, with 'abort' set to prevent default save.
|
||||||
|
*/
|
||||||
|
public function save_special_folders(array $args): array
|
||||||
|
{
|
||||||
|
$rc = rcmail::get_instance();
|
||||||
|
|
||||||
|
if (
|
||||||
|
$args['section'] === 'folders'
|
||||||
|
&& strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0
|
||||||
|
) {
|
||||||
|
$sql = 'SELECT id FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?';
|
||||||
|
$q = $rc->db->query($sql, $_SESSION['iid' . ident_switch::MY_POSTFIX], $rc->user->ID);
|
||||||
|
$r = $rc->db->fetch_assoc($q);
|
||||||
|
if ($r) {
|
||||||
|
$sql = 'UPDATE ' .
|
||||||
|
$rc->db->table_name(ident_switch::TABLE) .
|
||||||
|
' SET drafts_mbox = ?, sent_mbox = ?, junk_mbox = ?, trash_mbox = ?' .
|
||||||
|
' WHERE id = ?';
|
||||||
|
|
||||||
|
$rc->db->query(
|
||||||
|
$sql,
|
||||||
|
$args['prefs']['drafts_mbox'],
|
||||||
|
$args['prefs']['sent_mbox'],
|
||||||
|
$args['prefs']['junk_mbox'],
|
||||||
|
$args['prefs']['trash_mbox'],
|
||||||
|
$r['id']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Abort to prevent RC from saving prefs to default storage
|
||||||
|
$args['abort'] = true;
|
||||||
|
$args['result'] = true;
|
||||||
|
|
||||||
|
foreach (rcube_storage::$folder_types as $type) {
|
||||||
|
if (!empty($args['prefs'][$type . '_mbox'])) {
|
||||||
|
$otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX;
|
||||||
|
$_SESSION[$otherKey] = $args['prefs'][$type . '_mbox'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
$args['abort'] = true;
|
||||||
|
$args['result'] = false;
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (rcube_storage::$folder_types as $type) {
|
||||||
|
if (!empty($args['prefs'][$type . '_mbox'])) {
|
||||||
|
$key = $type . '_mbox_default' . ident_switch::MY_POSTFIX;
|
||||||
|
$_SESSION[$key] = $args['prefs'][$type . '_mbox'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the baseline for a target account so delta display resets to 0.
|
||||||
|
*
|
||||||
|
* For primary account (identId=-1), iid is 0.
|
||||||
|
* For secondary accounts, look up iid from the ident_switch table.
|
||||||
|
*
|
||||||
|
* @param integer|null $iid Known iid (0 for primary), or null to look up.
|
||||||
|
* @param rcmail $rc Roundcube instance.
|
||||||
|
* @param mixed $identId The ident_switch.id value for secondary accounts.
|
||||||
|
*/
|
||||||
|
private function reset_baseline(?int $iid, rcmail $rc, mixed $identId): void
|
||||||
|
{
|
||||||
|
if ($iid === null) {
|
||||||
|
// Look up iid from ident_switch table for secondary account
|
||||||
|
$sql = 'SELECT iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE id = ? AND user_id = ?';
|
||||||
|
$q = $rc->db->query($sql, $identId, $rc->user->ID);
|
||||||
|
$r = $rc->db->fetch_assoc($q);
|
||||||
|
if (!$r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$iid = (int)$r['iid'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$counts = $_SESSION['ident_switch_counts'] ?? [];
|
||||||
|
if (isset($counts[$iid])) {
|
||||||
|
unset($counts[$iid]['baseline']);
|
||||||
|
$_SESSION['ident_switch_counts'] = $counts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
178
localization/de_DE.inc
Normal file
178
localization/de_DE.inc
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Localization file for ident_switch plugin
|
||||||
|
*/
|
||||||
|
$labels = array();
|
||||||
|
|
||||||
|
// Separate Account
|
||||||
|
$labels['form.caption'] = 'Separates Konto';
|
||||||
|
$labels['form.description'] = 'Konfigurieren Sie diese Identität als separates Konto mit eigenen Mailserver-Verbindungen.';
|
||||||
|
$labels['form.preconfig_only_warning'] = 'Die Konfiguration als separates Konto ist für die Domain %s nicht verfügbar.';
|
||||||
|
|
||||||
|
// General
|
||||||
|
$labels['form.common.general'] = 'Allgemein';
|
||||||
|
|
||||||
|
// Account mode
|
||||||
|
$labels['form.common.mode'] = 'Kontomodus';
|
||||||
|
$labels['form.common.mode.primary'] = 'Hauptkonto';
|
||||||
|
$labels['form.common.mode.separate'] = 'Separates Konto';
|
||||||
|
$labels['form.common.mode.hint'] = 'Verknüpfen Sie diese Identität mit einem bestehenden Konto oder konfigurieren Sie sie als separates Konto mit eigenem Mailserver.';
|
||||||
|
|
||||||
|
// Label
|
||||||
|
$labels['form.common.label'] = 'Bezeichnung';
|
||||||
|
$labels['form.common.label.hint'] = 'Name, der im Kontowechsler angezeigt wird. Wenn leer, wird die E-Mail-Adresse verwendet.';
|
||||||
|
|
||||||
|
// Value in \'Label\' field is too long (32 chars max).
|
||||||
|
$labels['err.label.long'] = 'Der Wert in Feld \'Bezeichnung\' ist zu lang (max. 32 Zeichen).';
|
||||||
|
|
||||||
|
|
||||||
|
// IMAP
|
||||||
|
$labels['form.imap.caption'] = 'IMAP';
|
||||||
|
|
||||||
|
// Incoming mail server
|
||||||
|
$labels['form.imap.host'] = 'Posteingangsserver';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.imap.security'] = 'Sicherheit';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.imap.port'] = 'Port';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.imap.username'] = 'Benutzername';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.imap.password'] = 'Passwort';
|
||||||
|
|
||||||
|
// Folder hierarchy delimiter
|
||||||
|
$labels['form.imap.delimiter'] = 'Ordnerhierarchie-Trennzeichen';
|
||||||
|
$labels['form.imap.delimiter.auto'] = 'Automatisch erkennen';
|
||||||
|
$labels['form.imap.delimiter.manual'] = 'Manuell';
|
||||||
|
|
||||||
|
// Value in \'Username\' field is too long (64 chars max).
|
||||||
|
$labels['err.user.long'] = 'Der Wert in Feld \'Benutzername\' ist zu lang (max. 64 Zeichen).';
|
||||||
|
|
||||||
|
|
||||||
|
// SMTP
|
||||||
|
$labels['form.smtp.caption'] = 'SMTP';
|
||||||
|
|
||||||
|
// Outgoing mail server
|
||||||
|
$labels['form.smtp.host'] = 'Postausgangsserver';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.smtp.security'] = 'Sicherheit';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.smtp.port'] = 'Port';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.smtp.auth'] = 'Autorisierung';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.smtp.auth.imap'] = 'Wie IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.smtp.auth.none'] = 'Keine';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.smtp.auth.custom'] = 'Benutzerdefiniert';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.smtp.username'] = 'Benutzername';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.smtp.password'] = 'Passwort';
|
||||||
|
|
||||||
|
|
||||||
|
// Sieve
|
||||||
|
$labels['form.sieve.caption'] = 'Sieve';
|
||||||
|
|
||||||
|
// Server host name
|
||||||
|
$labels['form.sieve.host'] = 'Servername';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.sieve.security'] = 'Sicherheit';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.sieve.port'] = 'Port';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.sieve.auth'] = 'Autorisierung';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.sieve.auth.imap'] = 'Wie IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.sieve.auth.none'] = 'Keine';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.sieve.auth.custom'] = 'Benutzerdefiniert';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.sieve.username'] = 'Benutzername';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.sieve.password'] = 'Passwort';
|
||||||
|
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
$labels['form.notify.caption'] = 'Benachrichtigungen';
|
||||||
|
|
||||||
|
// Check for new mail
|
||||||
|
$labels['form.notify.check'] = 'Auf neue Nachrichten prüfen';
|
||||||
|
|
||||||
|
// Focus / favicon
|
||||||
|
$labels['form.notify.basic'] = 'Fokus / Favicon';
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
$labels['form.notify.sound'] = 'Ton';
|
||||||
|
|
||||||
|
// Desktop notification
|
||||||
|
$labels['form.notify.desktop'] = 'Desktop-Benachrichtigung';
|
||||||
|
|
||||||
|
// Use default
|
||||||
|
$labels['form.notify.default'] = 'Standard';
|
||||||
|
|
||||||
|
// On
|
||||||
|
$labels['form.notify.on'] = 'Ein';
|
||||||
|
|
||||||
|
// Off
|
||||||
|
$labels['form.notify.off'] = 'Aus';
|
||||||
|
|
||||||
|
// Requires newmail_notifier plugin
|
||||||
|
$labels['form.notify.requires_newmail_notifier'] = 'Benachrichtigungen erfordern das Plugin newmail_notifier. Bitte aktivieren Sie es oder kontaktieren Sie Ihren Administrator.';
|
||||||
|
|
||||||
|
|
||||||
|
// Security options
|
||||||
|
// None
|
||||||
|
$labels['form.security.none'] = 'Keine';
|
||||||
|
|
||||||
|
// STARTTLS
|
||||||
|
$labels['form.security.starttls'] = 'STARTTLS';
|
||||||
|
|
||||||
|
// SSL/TLS
|
||||||
|
$labels['form.security.ssl'] = 'SSL/TLS';
|
||||||
|
|
||||||
|
// None warning
|
||||||
|
$labels['form.security.none_warning'] = 'Unverschlüsselte Verbindungen sind unsicher. Ihre Anmeldedaten und E-Mails können abgefangen werden. Verwenden Sie nach Möglichkeit SSL/TLS oder STARTTLS.';
|
||||||
|
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
|
||||||
|
// Value in \'Server host name\' field is too long (64 chars max).
|
||||||
|
$labels['err.host.long'] = 'Der Wert in Feld \'Servername\' ist zu lang (max. 64 Zeichen).';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be a number.
|
||||||
|
$labels['err.port.num'] = 'Der Wert in Feld \'Port\' muss eine Zahl sein.';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be between 1 and 65535.
|
||||||
|
$labels['err.port.range'] = 'Der Wert in Feld \'Port\' muss zwischen 1 und 65535 liegen.';
|
||||||
|
|
||||||
|
// IMAP connection test failed.
|
||||||
|
$labels['err.imap.connect'] = 'IMAP-Verbindung fehlgeschlagen. Bitte überprüfen Sie Serveradresse, Port und Zugangsdaten.';
|
||||||
|
|
||||||
|
// SMTP connection test failed.
|
||||||
|
$labels['err.smtp.connect'] = 'SMTP-Verbindung fehlgeschlagen. Bitte überprüfen Sie Serveradresse, Port und Zugangsdaten.';
|
||||||
|
|
||||||
|
// Sieve connection test failed.
|
||||||
|
$labels['err.sieve.connect'] = 'Sieve-Verbindung fehlgeschlagen. Bitte überprüfen Sie Serveradresse, Port und Zugangsdaten.';
|
||||||
@@ -4,23 +4,169 @@
|
|||||||
*/
|
*/
|
||||||
$labels = array();
|
$labels = array();
|
||||||
|
|
||||||
// IMAP
|
// Separate Account
|
||||||
$labels['form.caption'] = 'IMAP';
|
$labels['form.caption'] = 'Separate Account';
|
||||||
|
$labels['form.description'] = 'Configure this identity as a separate account with its own mail server connections.';
|
||||||
|
$labels['form.preconfig_only_warning'] = 'Separate account configuration is not available for the domain %s.';
|
||||||
|
|
||||||
// Enabled
|
// General
|
||||||
$labels['form.enabled'] = 'Enabled';
|
$labels['form.common.general'] = 'General';
|
||||||
|
|
||||||
|
// Account mode
|
||||||
|
$labels['form.common.mode'] = 'Account mode';
|
||||||
|
$labels['form.common.mode.primary'] = 'Primary account';
|
||||||
|
$labels['form.common.mode.separate'] = 'Separate account';
|
||||||
|
$labels['form.common.mode.hint'] = 'Link this identity to an existing account, or configure it as a separate account with its own mail server.';
|
||||||
|
|
||||||
// Label
|
// Label
|
||||||
$labels['form.label'] = 'Label';
|
$labels['form.common.label'] = 'Label';
|
||||||
|
$labels['form.common.label.hint'] = 'Name displayed in the account switcher. If empty, the email address is used.';
|
||||||
|
|
||||||
// Server URI
|
// Value in \'Label\' field is too long (32 chars max).
|
||||||
$labels['form.server']='Server URI';
|
$labels['err.label.long'] = 'Value in \'Label\' field is too long (32 chars max).';
|
||||||
|
|
||||||
// Username
|
|
||||||
$labels['form.username'] = 'Username';
|
|
||||||
|
|
||||||
// Passwotrd
|
// IMAP
|
||||||
$labels['form.password'] = 'Password';
|
$labels['form.imap.caption'] = 'IMAP';
|
||||||
|
|
||||||
|
// Incoming mail server
|
||||||
|
$labels['form.imap.host'] = 'Incoming mail server';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.imap.security'] = 'Security';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.imap.port'] = 'Port';
|
||||||
|
|
||||||
// Folder hierarchy delimiter
|
// Folder hierarchy delimiter
|
||||||
$labels['form.delimiter'] = 'Folder hierarchy delimiter';
|
$labels['form.imap.delimiter'] = 'Folder hierarchy delimiter';
|
||||||
|
$labels['form.imap.delimiter.auto'] = 'Auto-detect';
|
||||||
|
$labels['form.imap.delimiter.manual'] = 'Manual';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.imap.username'] = 'Username';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.imap.password'] = 'Password';
|
||||||
|
|
||||||
|
// Value in \'Username\' field is too long (64 chars max).
|
||||||
|
$labels['err.user.long'] = 'Value in \'Username\' field is too long (64 chars max).';
|
||||||
|
|
||||||
|
|
||||||
|
// SMTP
|
||||||
|
$labels['form.smtp.caption'] = 'SMTP';
|
||||||
|
|
||||||
|
// Outgoing mail server
|
||||||
|
$labels['form.smtp.host'] = 'Outgoing mail server';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.smtp.security'] = 'Security';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.smtp.port'] = 'Port';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.smtp.auth'] = 'Authorization';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.smtp.auth.imap'] = 'As IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.smtp.auth.none'] = 'None';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.smtp.auth.custom'] = 'Custom';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.smtp.username'] = 'Username';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.smtp.password'] = 'Password';
|
||||||
|
|
||||||
|
|
||||||
|
// Sieve
|
||||||
|
$labels['form.sieve.caption'] = 'Sieve';
|
||||||
|
|
||||||
|
// Server host name
|
||||||
|
$labels['form.sieve.host'] = 'Server host name';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.sieve.security'] = 'Security';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.sieve.port'] = 'Port';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.sieve.auth'] = 'Authorization';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.sieve.auth.imap'] = 'As IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.sieve.auth.none'] = 'None';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.sieve.auth.custom'] = 'Custom';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.sieve.username'] = 'Username';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.sieve.password'] = 'Password';
|
||||||
|
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
$labels['form.notify.caption'] = 'Notifications';
|
||||||
|
|
||||||
|
// Check for new mail
|
||||||
|
$labels['form.notify.check'] = 'Check for new mail';
|
||||||
|
|
||||||
|
// Focus / favicon
|
||||||
|
$labels['form.notify.basic'] = 'Focus / favicon';
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
$labels['form.notify.sound'] = 'Sound';
|
||||||
|
|
||||||
|
// Desktop notification
|
||||||
|
$labels['form.notify.desktop'] = 'Desktop notification';
|
||||||
|
|
||||||
|
|
||||||
|
// Use default
|
||||||
|
$labels['form.notify.default'] = 'default';
|
||||||
|
|
||||||
|
// On
|
||||||
|
$labels['form.notify.on'] = 'On';
|
||||||
|
|
||||||
|
// Off
|
||||||
|
$labels['form.notify.off'] = 'Off';
|
||||||
|
|
||||||
|
// Requires newmail_notifier plugin
|
||||||
|
$labels['form.notify.requires_newmail_notifier'] = 'Notifications require the newmail_notifier plugin. Please enable it or contact your administrator.';
|
||||||
|
|
||||||
|
|
||||||
|
// Security options
|
||||||
|
$labels['form.security.none'] = 'None';
|
||||||
|
$labels['form.security.starttls'] = 'STARTTLS';
|
||||||
|
$labels['form.security.ssl'] = 'SSL/TLS';
|
||||||
|
$labels['form.security.none_warning'] = 'Unencrypted connections are insecure. Your credentials and emails may be intercepted. Use SSL/TLS or STARTTLS whenever possible.';
|
||||||
|
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
|
||||||
|
// Value in \'Server host name\' field is too long (64 chars max).
|
||||||
|
$labels['err.host.long'] = 'Value in \'Server host name\' field is too long (64 chars max).';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be a number.
|
||||||
|
$labels['err.port.num'] = 'Value in \'Port\' field must be a number.';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be between 1 and 65535.
|
||||||
|
$labels['err.port.range'] = 'Value in \'Port\' field must be between 1 and 65535.';
|
||||||
|
|
||||||
|
// IMAP connection test failed.
|
||||||
|
$labels['err.imap.connect'] = 'IMAP connection failed. Please check the server address, port, and credentials.';
|
||||||
|
|
||||||
|
// SMTP connection test failed.
|
||||||
|
$labels['err.smtp.connect'] = 'SMTP connection failed. Please check the server address, port, and credentials.';
|
||||||
|
|
||||||
|
// Sieve connection test failed.
|
||||||
|
$labels['err.sieve.connect'] = 'Sieve connection failed. Please check the server address, port, and credentials.';
|
||||||
|
|||||||
171
localization/fr_FR.inc
Normal file
171
localization/fr_FR.inc
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Localization file for ident_switch plugin
|
||||||
|
*/
|
||||||
|
$labels = array();
|
||||||
|
|
||||||
|
// Separate Account
|
||||||
|
$labels['form.caption'] = 'Compte séparé';
|
||||||
|
$labels['form.description'] = 'Configurez cette identité comme un compte séparé avec ses propres connexions aux serveurs de messagerie.';
|
||||||
|
$labels['form.preconfig_only_warning'] = 'La configuration de compte séparé n\'est pas disponible pour le domaine %s.';
|
||||||
|
|
||||||
|
// General
|
||||||
|
$labels['form.common.general'] = 'Général';
|
||||||
|
|
||||||
|
// Account mode
|
||||||
|
$labels['form.common.mode'] = 'Mode de compte';
|
||||||
|
$labels['form.common.mode.primary'] = 'Compte principal';
|
||||||
|
$labels['form.common.mode.separate'] = 'Compte séparé';
|
||||||
|
$labels['form.common.mode.hint'] = 'Rattachez cette identité à un compte existant, ou configurez-la comme un compte séparé avec son propre serveur de messagerie.';
|
||||||
|
|
||||||
|
// Label
|
||||||
|
$labels['form.common.label'] = 'Nom à afficher';
|
||||||
|
$labels['form.common.label.hint'] = 'Nom affiché dans le sélecteur de compte. Si vide, l\'adresse email est utilisée.';
|
||||||
|
|
||||||
|
// Value in \'Label\' field is too long (32 chars max).
|
||||||
|
$labels['err.label.long'] = 'La valeur du champ \'Nom à afficher\' est trop longue (32 caractères max).';
|
||||||
|
|
||||||
|
|
||||||
|
// IMAP
|
||||||
|
$labels['form.imap.caption'] = 'IMAP';
|
||||||
|
|
||||||
|
// Incoming mail server
|
||||||
|
$labels['form.imap.host'] = 'Serveur de courrier entrant';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.imap.security'] = 'Sécurité';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.imap.port'] = 'Port';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.imap.username'] = 'Nom d\'utilisateur';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.imap.password'] = 'Mot de passe';
|
||||||
|
|
||||||
|
// Folder hierarchy delimiter
|
||||||
|
$labels['form.imap.delimiter'] = 'Délimiteur de hiérarchie des dossiers';
|
||||||
|
$labels['form.imap.delimiter.auto'] = 'Auto-détection';
|
||||||
|
$labels['form.imap.delimiter.manual'] = 'Manuel';
|
||||||
|
|
||||||
|
// Value in \'Username\' field of IMAP section is too long (64 chars max).
|
||||||
|
$labels['err.user.long'] = 'La valeur du champ \'Nom d\'utilisateur\' de la section IMAP est trop longue (64 caractères max).';
|
||||||
|
|
||||||
|
|
||||||
|
// SMTP
|
||||||
|
$labels['form.smtp.caption'] = 'SMTP';
|
||||||
|
|
||||||
|
// Outgoing mail server
|
||||||
|
$labels['form.smtp.host'] = 'Serveur de courrier sortant';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.smtp.security'] = 'Sécurité';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.smtp.port'] = 'Port';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.smtp.auth'] = 'Autorisation';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.smtp.auth.imap'] = 'Identique à IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.smtp.auth.none'] = 'Aucune';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.smtp.auth.custom'] = 'Personnalisée';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.smtp.username'] = 'Nom d\'utilisateur';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.smtp.password'] = 'Mot de passe';
|
||||||
|
|
||||||
|
|
||||||
|
// Sieve
|
||||||
|
$labels['form.sieve.caption'] = 'Sieve';
|
||||||
|
|
||||||
|
// Server host name
|
||||||
|
$labels['form.sieve.host'] = 'Serveur';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.sieve.security'] = 'Sécurité';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.sieve.port'] = 'Port';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.sieve.auth'] = 'Autorisation';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.sieve.auth.imap'] = 'Identique à IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.sieve.auth.none'] = 'Aucune';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.sieve.auth.custom'] = 'Personnalisée';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.sieve.username'] = 'Nom d\'utilisateur';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.sieve.password'] = 'Mot de passe';
|
||||||
|
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
$labels['form.notify.caption'] = 'Notifications';
|
||||||
|
|
||||||
|
// Check for new mail
|
||||||
|
$labels['form.notify.check'] = 'Vérifier les nouveaux messages';
|
||||||
|
|
||||||
|
// Focus / favicon
|
||||||
|
$labels['form.notify.basic'] = 'Focus / favicon';
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
$labels['form.notify.sound'] = 'Son';
|
||||||
|
|
||||||
|
// Desktop notification
|
||||||
|
$labels['form.notify.desktop'] = 'Notification bureau';
|
||||||
|
|
||||||
|
// Use default
|
||||||
|
$labels['form.notify.default'] = 'par défaut';
|
||||||
|
|
||||||
|
// On
|
||||||
|
$labels['form.notify.on'] = 'Activé';
|
||||||
|
|
||||||
|
// Off
|
||||||
|
$labels['form.notify.off'] = 'Désactivé';
|
||||||
|
|
||||||
|
// Requires newmail_notifier plugin
|
||||||
|
$labels['form.notify.requires_newmail_notifier'] = 'Les notifications nécessitent le plugin newmail_notifier. Veuillez l\'activer ou contacter votre administrateur.';
|
||||||
|
|
||||||
|
|
||||||
|
// Security options
|
||||||
|
$labels['form.security.none'] = 'Aucune';
|
||||||
|
$labels['form.security.starttls'] = 'STARTTLS';
|
||||||
|
$labels['form.security.ssl'] = 'SSL/TLS';
|
||||||
|
$labels['form.security.none_warning'] = 'Les connexions non chiffrées sont vulnérables. Vos identifiants et emails peuvent être interceptés. Utilisez SSL/TLS ou STARTTLS autant que possible.';
|
||||||
|
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
|
||||||
|
// Value in \'Server host name\' field is too long (64 chars max).
|
||||||
|
$labels['err.host.long'] = 'La valeur du champ \'Serveur\' est trop longue (64 caractères max).';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be a number.
|
||||||
|
$labels['err.port.num'] = 'La valeur du champ \'Port\' doit être un nombre.';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be between 1 and 65535.
|
||||||
|
$labels['err.port.range'] = 'La valeur du champ \'Port\' doit être comprise entre 1 et 65535.';
|
||||||
|
|
||||||
|
// IMAP connection test failed.
|
||||||
|
$labels['err.imap.connect'] = 'La connexion IMAP a échoué. Vérifiez l\'adresse du serveur, le port et les identifiants.';
|
||||||
|
|
||||||
|
// SMTP connection test failed.
|
||||||
|
$labels['err.smtp.connect'] = 'La connexion SMTP a échoué. Vérifiez l\'adresse du serveur, le port et les identifiants.';
|
||||||
|
|
||||||
|
// Sieve connection test failed.
|
||||||
|
$labels['err.sieve.connect'] = 'La connexion Sieve a échoué. Vérifiez l\'adresse du serveur, le port et les identifiants.';
|
||||||
178
localization/it_IT.inc
Normal file
178
localization/it_IT.inc
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Localization file for ident_switch plugin
|
||||||
|
*/
|
||||||
|
$labels = array();
|
||||||
|
|
||||||
|
// Separate Account
|
||||||
|
$labels['form.caption'] = 'Account separato';
|
||||||
|
$labels['form.description'] = 'Configura questa identità come un account separato con le proprie connessioni al server di posta.';
|
||||||
|
$labels['form.preconfig_only_warning'] = 'La configurazione come account separato non è disponibile per il dominio %s.';
|
||||||
|
|
||||||
|
// General
|
||||||
|
$labels['form.common.general'] = 'Generale';
|
||||||
|
|
||||||
|
// Account mode
|
||||||
|
$labels['form.common.mode'] = 'Modalità account';
|
||||||
|
$labels['form.common.mode.primary'] = 'Account principale';
|
||||||
|
$labels['form.common.mode.separate'] = 'Account separato';
|
||||||
|
$labels['form.common.mode.hint'] = 'Collega questa identità a un account esistente o configurala come un account separato con il proprio server di posta.';
|
||||||
|
|
||||||
|
// Label
|
||||||
|
$labels['form.common.label'] = 'Nome visualizzato';
|
||||||
|
$labels['form.common.label.hint'] = 'Nome visualizzato nel selettore account. Se vuoto, viene utilizzato l\'indirizzo email.';
|
||||||
|
|
||||||
|
// Value in \'Label\' field is too long (32 chars max).
|
||||||
|
$labels['err.label.long'] = 'Il valore del campo \'Nome Visualizzato\' è troppo lungo (massimo 32 caratteri).';
|
||||||
|
|
||||||
|
|
||||||
|
// IMAP
|
||||||
|
$labels['form.imap.caption'] = 'IMAP';
|
||||||
|
|
||||||
|
// Incoming mail server
|
||||||
|
$labels['form.imap.host'] = 'Server posta in arrivo';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.imap.security'] = 'Sicurezza';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.imap.port'] = 'Porta';
|
||||||
|
|
||||||
|
// Folder hierarchy delimiter
|
||||||
|
$labels['form.imap.delimiter'] = 'Delimitatore gerarchico delle cartelle';
|
||||||
|
$labels['form.imap.delimiter.auto'] = 'Rilevamento automatico';
|
||||||
|
$labels['form.imap.delimiter.manual'] = 'Manuale';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.imap.username'] = 'Username';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.imap.password'] = 'Password';
|
||||||
|
|
||||||
|
// Value in \'Username\' field is too long (64 chars max).
|
||||||
|
$labels['err.user.long'] = 'Il valore del campo \'Username\' è troppo lungo (massimo 64 caratteri).';
|
||||||
|
|
||||||
|
|
||||||
|
// SMTP
|
||||||
|
$labels['form.smtp.caption'] = 'SMTP';
|
||||||
|
|
||||||
|
// Outgoing mail server
|
||||||
|
$labels['form.smtp.host'] = 'Server posta in uscita';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.smtp.security'] = 'Sicurezza';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.smtp.port'] = 'Porta';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.smtp.auth'] = 'Autorizzazione';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.smtp.auth.imap'] = 'Come IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.smtp.auth.none'] = 'Nessuna';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.smtp.auth.custom'] = 'Personalizzata';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.smtp.username'] = 'Username';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.smtp.password'] = 'Password';
|
||||||
|
|
||||||
|
|
||||||
|
// Sieve
|
||||||
|
$labels['form.sieve.caption'] = 'Sieve';
|
||||||
|
|
||||||
|
// Server host name
|
||||||
|
$labels['form.sieve.host'] = 'Nome del server';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.sieve.security'] = 'Sicurezza';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.sieve.port'] = 'Porta';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.sieve.auth'] = 'Autorizzazione';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.sieve.auth.imap'] = 'Come IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.sieve.auth.none'] = 'Nessuna';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.sieve.auth.custom'] = 'Personalizzata';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.sieve.username'] = 'Username';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.sieve.password'] = 'Password';
|
||||||
|
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
$labels['form.notify.caption'] = 'Notifiche';
|
||||||
|
|
||||||
|
// Check for new mail
|
||||||
|
$labels['form.notify.check'] = 'Controlla nuova posta';
|
||||||
|
|
||||||
|
// Focus / favicon
|
||||||
|
$labels['form.notify.basic'] = 'Focus / favicon';
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
$labels['form.notify.sound'] = 'Suono';
|
||||||
|
|
||||||
|
// Desktop notification
|
||||||
|
$labels['form.notify.desktop'] = 'Notifica desktop';
|
||||||
|
|
||||||
|
// Use default
|
||||||
|
$labels['form.notify.default'] = 'predefinito';
|
||||||
|
|
||||||
|
// On
|
||||||
|
$labels['form.notify.on'] = 'Attivo';
|
||||||
|
|
||||||
|
// Off
|
||||||
|
$labels['form.notify.off'] = 'Disattivo';
|
||||||
|
|
||||||
|
// Requires newmail_notifier plugin
|
||||||
|
$labels['form.notify.requires_newmail_notifier'] = 'Le notifiche richiedono il plugin newmail_notifier. Abilitarlo o contattare l\'amministratore.';
|
||||||
|
|
||||||
|
|
||||||
|
// Security options
|
||||||
|
// None
|
||||||
|
$labels['form.security.none'] = 'Nessuna';
|
||||||
|
|
||||||
|
// STARTTLS
|
||||||
|
$labels['form.security.starttls'] = 'STARTTLS';
|
||||||
|
|
||||||
|
// SSL/TLS
|
||||||
|
$labels['form.security.ssl'] = 'SSL/TLS';
|
||||||
|
|
||||||
|
// None warning
|
||||||
|
$labels['form.security.none_warning'] = 'Le connessioni non crittografate non sono sicure. Le credenziali e le email possono essere intercettate. Utilizzare SSL/TLS o STARTTLS quando possibile.';
|
||||||
|
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
|
||||||
|
// Value in \'Server host name\' field is too long (64 chars max).
|
||||||
|
$labels['err.host.long'] = 'Il valore del campo \'Nome del server\' è troppo lungo (massimo 64 caratteri).';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be a number.
|
||||||
|
$labels['err.port.num'] = 'Il valore del campo \'Porta\' deve essere un numero.';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be between 1 and 65535.
|
||||||
|
$labels['err.port.range'] = 'Il valore del campo \'Porta\' deve essere compreso tra 1 e 65535.';
|
||||||
|
|
||||||
|
// IMAP connection test failed.
|
||||||
|
$labels['err.imap.connect'] = 'Connessione IMAP fallita. Verificare l\'indirizzo del server, la porta e le credenziali.';
|
||||||
|
|
||||||
|
// SMTP connection test failed.
|
||||||
|
$labels['err.smtp.connect'] = 'Connessione SMTP fallita. Verificare l\'indirizzo del server, la porta e le credenziali.';
|
||||||
|
|
||||||
|
// Sieve connection test failed.
|
||||||
|
$labels['err.sieve.connect'] = 'Connessione Sieve fallita. Verificare l\'indirizzo del server, la porta e le credenziali.';
|
||||||
178
localization/nl_NL.inc
Normal file
178
localization/nl_NL.inc
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Localization file for ident_switch plugin
|
||||||
|
*/
|
||||||
|
$labels = array();
|
||||||
|
|
||||||
|
// Separate Account
|
||||||
|
$labels['form.caption'] = 'Afzonderlijk account';
|
||||||
|
$labels['form.description'] = 'Configureer deze identiteit als een afzonderlijk account met eigen mailserververbindingen.';
|
||||||
|
$labels['form.preconfig_only_warning'] = 'Configuratie als afzonderlijk account is niet beschikbaar voor het domein %s.';
|
||||||
|
|
||||||
|
// General
|
||||||
|
$labels['form.common.general'] = 'Algemeen';
|
||||||
|
|
||||||
|
// Account mode
|
||||||
|
$labels['form.common.mode'] = 'Accountmodus';
|
||||||
|
$labels['form.common.mode.primary'] = 'Hoofdaccount';
|
||||||
|
$labels['form.common.mode.separate'] = 'Afzonderlijk account';
|
||||||
|
$labels['form.common.mode.hint'] = 'Koppel deze identiteit aan een bestaand account of configureer het als een afzonderlijk account met een eigen mailserver.';
|
||||||
|
|
||||||
|
// Label
|
||||||
|
$labels['form.common.label'] = 'Label';
|
||||||
|
$labels['form.common.label.hint'] = 'Naam weergegeven in de accountkiezer. Indien leeg, wordt het e-mailadres gebruikt.';
|
||||||
|
|
||||||
|
// Value in \'Label\' field is too long (32 chars max).
|
||||||
|
$labels['err.label.long'] = 'De waarde in het veld \'Label\' is te lang (maximaal 32 karakter is toegestaan).';
|
||||||
|
|
||||||
|
|
||||||
|
// IMAP
|
||||||
|
$labels['form.imap.caption'] = 'IMAP';
|
||||||
|
|
||||||
|
// Incoming mail server
|
||||||
|
$labels['form.imap.host'] = 'Inkomende mailserver';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.imap.security'] = 'Beveiliging';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.imap.port'] = 'Poortnummer';
|
||||||
|
|
||||||
|
// Folder hierarchy delimiter
|
||||||
|
$labels['form.imap.delimiter'] = 'Map hiërarchie scheidingsteken';
|
||||||
|
$labels['form.imap.delimiter.auto'] = 'Automatisch detecteren';
|
||||||
|
$labels['form.imap.delimiter.manual'] = 'Handmatig';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.imap.username'] = 'Gebruikersnaam';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.imap.password'] = 'Wachtwoord';
|
||||||
|
|
||||||
|
// Value in \'Username\' field is too long (64 chars max).
|
||||||
|
$labels['err.user.long'] = 'De waarde in het veld \'Gebruikersnaam\' is te lang (maximaal 64 karakter is toegestaan).';
|
||||||
|
|
||||||
|
|
||||||
|
// SMTP
|
||||||
|
$labels['form.smtp.caption'] = 'SMTP';
|
||||||
|
|
||||||
|
// Outgoing mail server
|
||||||
|
$labels['form.smtp.host'] = 'Uitgaande mailserver';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.smtp.security'] = 'Beveiliging';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.smtp.port'] = 'Poortnummer';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.smtp.auth'] = 'Autorisatie';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.smtp.auth.imap'] = 'Zoals IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.smtp.auth.none'] = 'Geen';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.smtp.auth.custom'] = 'Aangepast';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.smtp.username'] = 'Gebruikersnaam';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.smtp.password'] = 'Wachtwoord';
|
||||||
|
|
||||||
|
|
||||||
|
// Sieve
|
||||||
|
$labels['form.sieve.caption'] = 'Sieve';
|
||||||
|
|
||||||
|
// Server host name
|
||||||
|
$labels['form.sieve.host'] = 'Servernaam';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.sieve.security'] = 'Beveiliging';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.sieve.port'] = 'Poortnummer';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.sieve.auth'] = 'Autorisatie';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.sieve.auth.imap'] = 'Zoals IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.sieve.auth.none'] = 'Geen';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.sieve.auth.custom'] = 'Aangepast';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.sieve.username'] = 'Gebruikersnaam';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.sieve.password'] = 'Wachtwoord';
|
||||||
|
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
$labels['form.notify.caption'] = 'Meldingen';
|
||||||
|
|
||||||
|
// Check for new mail
|
||||||
|
$labels['form.notify.check'] = 'Controleer op nieuwe berichten';
|
||||||
|
|
||||||
|
// Focus / favicon
|
||||||
|
$labels['form.notify.basic'] = 'Focus / favicon';
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
$labels['form.notify.sound'] = 'Geluid';
|
||||||
|
|
||||||
|
// Desktop notification
|
||||||
|
$labels['form.notify.desktop'] = 'Bureaubladmelding';
|
||||||
|
|
||||||
|
// Use default
|
||||||
|
$labels['form.notify.default'] = 'standaard';
|
||||||
|
|
||||||
|
// On
|
||||||
|
$labels['form.notify.on'] = 'Aan';
|
||||||
|
|
||||||
|
// Off
|
||||||
|
$labels['form.notify.off'] = 'Uit';
|
||||||
|
|
||||||
|
// Requires newmail_notifier plugin
|
||||||
|
$labels['form.notify.requires_newmail_notifier'] = 'Meldingen vereisen de newmail_notifier-plug-in. Schakel deze in of neem contact op met uw beheerder.';
|
||||||
|
|
||||||
|
|
||||||
|
// Security options
|
||||||
|
// None
|
||||||
|
$labels['form.security.none'] = 'Geen';
|
||||||
|
|
||||||
|
// STARTTLS
|
||||||
|
$labels['form.security.starttls'] = 'STARTTLS';
|
||||||
|
|
||||||
|
// SSL/TLS
|
||||||
|
$labels['form.security.ssl'] = 'SSL/TLS';
|
||||||
|
|
||||||
|
// None warning
|
||||||
|
$labels['form.security.none_warning'] = 'Onversleutelde verbindingen zijn onveilig. Uw inloggegevens en e-mails kunnen worden onderschept. Gebruik waar mogelijk SSL/TLS of STARTTLS.';
|
||||||
|
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
|
||||||
|
// Value in \'Server host name\' field is too long (64 chars max).
|
||||||
|
$labels['err.host.long'] = 'De waarde in het veld \'Servernaam\' is te lang (maximaal 64 karakter is toegestaan).';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be a number.
|
||||||
|
$labels['err.port.num'] = 'De waarde in het veld \'Poortnummer\' moet een nummer zijn.';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be between 1 and 65535.
|
||||||
|
$labels['err.port.range'] = 'De waarde in het veld \'Poortnummer\' moet tussen de 1 en 65535 zijn.';
|
||||||
|
|
||||||
|
// IMAP connection test failed.
|
||||||
|
$labels['err.imap.connect'] = 'IMAP-verbinding mislukt. Controleer het serveradres, de poort en de inloggegevens.';
|
||||||
|
|
||||||
|
// SMTP connection test failed.
|
||||||
|
$labels['err.smtp.connect'] = 'SMTP-verbinding mislukt. Controleer het serveradres, de poort en de inloggegevens.';
|
||||||
|
|
||||||
|
// Sieve connection test failed.
|
||||||
|
$labels['err.sieve.connect'] = 'Sieve-verbinding mislukt. Controleer het serveradres, de poort en de inloggegevens.';
|
||||||
178
localization/ru_RU.inc
Normal file
178
localization/ru_RU.inc
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Localization file for ident_switch plugin
|
||||||
|
*/
|
||||||
|
$labels = array();
|
||||||
|
|
||||||
|
// Separate Account
|
||||||
|
$labels['form.caption'] = 'Отдельный аккаунт';
|
||||||
|
$labels['form.description'] = 'Настройте эту учётную запись как отдельный аккаунт с собственными подключениями к почтовому серверу.';
|
||||||
|
$labels['form.preconfig_only_warning'] = 'Настройка отдельного аккаунта недоступна для домена %s.';
|
||||||
|
|
||||||
|
// General
|
||||||
|
$labels['form.common.general'] = 'Общие';
|
||||||
|
|
||||||
|
// Account mode
|
||||||
|
$labels['form.common.mode'] = 'Режим аккаунта';
|
||||||
|
$labels['form.common.mode.primary'] = 'Основной аккаунт';
|
||||||
|
$labels['form.common.mode.separate'] = 'Отдельный аккаунт';
|
||||||
|
$labels['form.common.mode.hint'] = 'Привяжите эту учётную запись к существующему аккаунту или настройте её как отдельный аккаунт с собственным почтовым сервером.';
|
||||||
|
|
||||||
|
// Label
|
||||||
|
$labels['form.common.label'] = 'Название';
|
||||||
|
$labels['form.common.label.hint'] = 'Имя, отображаемое в переключателе аккаунтов. Если пусто, используется адрес электронной почты.';
|
||||||
|
|
||||||
|
// Value in \'Label\' field is too long (32 chars max).
|
||||||
|
$labels['err.label.long'] = '\'Название\' должно быть не длинее 32 символов.';
|
||||||
|
|
||||||
|
|
||||||
|
// IMAP
|
||||||
|
$labels['form.imap.caption'] = 'IMAP';
|
||||||
|
|
||||||
|
// Incoming mail server
|
||||||
|
$labels['form.imap.host'] = 'Сервер входящей почты';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.imap.security'] = 'Безопасность';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.imap.port'] = 'Порт';
|
||||||
|
|
||||||
|
// Folder hierarchy delimiter
|
||||||
|
$labels['form.imap.delimiter'] = 'Разделитель в иерархии папок';
|
||||||
|
$labels['form.imap.delimiter.auto'] = 'Автоопределение';
|
||||||
|
$labels['form.imap.delimiter.manual'] = 'Вручную';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.imap.username'] = 'Имя пользователя';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.imap.password'] = 'Пароль';
|
||||||
|
|
||||||
|
// Value in \'Username\' field is too long (64 chars max).
|
||||||
|
$labels['err.user.long'] = '\'Имя пользователя\' должно быть не длинее 64 символов.';
|
||||||
|
|
||||||
|
|
||||||
|
// SMTP
|
||||||
|
$labels['form.smtp.caption'] = 'SMTP';
|
||||||
|
|
||||||
|
// Outgoing mail server
|
||||||
|
$labels['form.smtp.host'] = 'Сервер исходящей почты';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.smtp.security'] = 'Безопасность';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.smtp.port'] = 'Порт';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.smtp.auth'] = 'Авторизация';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.smtp.auth.imap'] = 'Как IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.smtp.auth.none'] = 'Нет';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.smtp.auth.custom'] = 'Другие';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.smtp.username'] = 'Имя пользователя';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.smtp.password'] = 'Пароль';
|
||||||
|
|
||||||
|
|
||||||
|
// Sieve
|
||||||
|
$labels['form.sieve.caption'] = 'Sieve';
|
||||||
|
|
||||||
|
// Server host name
|
||||||
|
$labels['form.sieve.host'] = 'Адрес сервера';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.sieve.security'] = 'Безопасность';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.sieve.port'] = 'Порт';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.sieve.auth'] = 'Авторизация';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.sieve.auth.imap'] = 'Как IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.sieve.auth.none'] = 'Нет';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.sieve.auth.custom'] = 'Другие';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.sieve.username'] = 'Имя пользователя';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.sieve.password'] = 'Пароль';
|
||||||
|
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
$labels['form.notify.caption'] = 'Уведомления';
|
||||||
|
|
||||||
|
// Check for new mail
|
||||||
|
$labels['form.notify.check'] = 'Проверять новую почту';
|
||||||
|
|
||||||
|
// Focus / favicon
|
||||||
|
$labels['form.notify.basic'] = 'Фокус / фавикон';
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
$labels['form.notify.sound'] = 'Звук';
|
||||||
|
|
||||||
|
// Desktop notification
|
||||||
|
$labels['form.notify.desktop'] = 'Уведомление на рабочем столе';
|
||||||
|
|
||||||
|
// Use default
|
||||||
|
$labels['form.notify.default'] = 'по умолчанию';
|
||||||
|
|
||||||
|
// On
|
||||||
|
$labels['form.notify.on'] = 'Включено';
|
||||||
|
|
||||||
|
// Off
|
||||||
|
$labels['form.notify.off'] = 'Выключено';
|
||||||
|
|
||||||
|
// Requires newmail_notifier plugin
|
||||||
|
$labels['form.notify.requires_newmail_notifier'] = 'Уведомления требуют плагин newmail_notifier. Пожалуйста, включите его или обратитесь к администратору.';
|
||||||
|
|
||||||
|
|
||||||
|
// Security options
|
||||||
|
// None
|
||||||
|
$labels['form.security.none'] = 'Нет';
|
||||||
|
|
||||||
|
// STARTTLS
|
||||||
|
$labels['form.security.starttls'] = 'STARTTLS';
|
||||||
|
|
||||||
|
// SSL/TLS
|
||||||
|
$labels['form.security.ssl'] = 'SSL/TLS';
|
||||||
|
|
||||||
|
// None warning
|
||||||
|
$labels['form.security.none_warning'] = 'Незашифрованные соединения небезопасны. Ваши учётные данные и письма могут быть перехвачены. По возможности используйте SSL/TLS или STARTTLS.';
|
||||||
|
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
|
||||||
|
// Value in \'Server host name\' field is too long (64 chars max).
|
||||||
|
$labels['err.host.long'] = '\'Адрес сервера\' должен быть не длинее 64 символов.';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be a number.
|
||||||
|
$labels['err.port.num'] = '\'Порт\' должен быть числом.';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be between 1 and 65535.
|
||||||
|
$labels['err.port.range'] = '\'Порт\' должен быть в диапазоне от 1 до 65535.';
|
||||||
|
|
||||||
|
// IMAP connection test failed.
|
||||||
|
$labels['err.imap.connect'] = 'Ошибка подключения к IMAP. Проверьте адрес сервера, порт и учётные данные.';
|
||||||
|
|
||||||
|
// SMTP connection test failed.
|
||||||
|
$labels['err.smtp.connect'] = 'Ошибка подключения к SMTP. Проверьте адрес сервера, порт и учётные данные.';
|
||||||
|
|
||||||
|
// Sieve connection test failed.
|
||||||
|
$labels['err.sieve.connect'] = 'Ошибка подключения к Sieve. Проверьте адрес сервера, порт и учётные данные.';
|
||||||
178
localization/sl_SI.inc
Normal file
178
localization/sl_SI.inc
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Localization file for ident_switch plugin
|
||||||
|
*/
|
||||||
|
$labels = array();
|
||||||
|
|
||||||
|
// Separate Account
|
||||||
|
$labels['form.caption'] = 'Ločen račun';
|
||||||
|
$labels['form.description'] = 'Konfigurirajte to identiteto kot ločen račun z lastnimi povezavami do poštnega strežnika.';
|
||||||
|
$labels['form.preconfig_only_warning'] = 'Konfiguracija ločenega računa za domeno %s ni na voljo.';
|
||||||
|
|
||||||
|
// General
|
||||||
|
$labels['form.common.general'] = 'Splošno';
|
||||||
|
|
||||||
|
// Account mode
|
||||||
|
$labels['form.common.mode'] = 'Način računa';
|
||||||
|
$labels['form.common.mode.primary'] = 'Glavni račun';
|
||||||
|
$labels['form.common.mode.separate'] = 'Ločen račun';
|
||||||
|
$labels['form.common.mode.hint'] = 'Povežite to identiteto z obstoječim računom ali jo konfigurirajte kot ločen račun z lastnim poštnim strežnikom.';
|
||||||
|
|
||||||
|
// Label
|
||||||
|
$labels['form.common.label'] = 'Oznaka';
|
||||||
|
$labels['form.common.label.hint'] = 'Ime, prikazano v izbirniku računov. Če je prazno, se uporabi e-poštni naslov.';
|
||||||
|
|
||||||
|
// Value in \'Label\' field is too long (32 chars max).
|
||||||
|
$labels['err.label.long'] = 'Število znakov v polju \'Oznaka\' presega maksimalno število znakov (max 32 znakov).';
|
||||||
|
|
||||||
|
|
||||||
|
// IMAP
|
||||||
|
$labels['form.imap.caption'] = 'IMAP';
|
||||||
|
|
||||||
|
// Incoming mail server
|
||||||
|
$labels['form.imap.host'] = 'Strežnik dohodne pošte';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.imap.security'] = 'Varnost';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.imap.port'] = 'Vrata';
|
||||||
|
|
||||||
|
// Folder hierarchy delimiter
|
||||||
|
$labels['form.imap.delimiter'] = 'Ločilo hierarhije map';
|
||||||
|
$labels['form.imap.delimiter.auto'] = 'Samodejno zaznavanje';
|
||||||
|
$labels['form.imap.delimiter.manual'] = 'Ročno';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.imap.username'] = 'Uporabniško ime';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.imap.password'] = 'Geslo';
|
||||||
|
|
||||||
|
// Value in \'Username\' field is too long (64 chars max).
|
||||||
|
$labels['err.user.long'] = 'Število znakov v polju \'Uporabniško ime\' presega maksimalno število znakov (max 64 znakov).';
|
||||||
|
|
||||||
|
|
||||||
|
// SMTP
|
||||||
|
$labels['form.smtp.caption'] = 'SMTP';
|
||||||
|
|
||||||
|
// Outgoing mail server
|
||||||
|
$labels['form.smtp.host'] = 'Strežnik odhodne pošte';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.smtp.security'] = 'Varnost';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.smtp.port'] = 'Vrata';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.smtp.auth'] = 'Avtorizacija';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.smtp.auth.imap'] = 'Tako kot IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.smtp.auth.none'] = 'Brez';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.smtp.auth.custom'] = 'Po meri';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.smtp.username'] = 'Uporabniško ime';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.smtp.password'] = 'Geslo';
|
||||||
|
|
||||||
|
|
||||||
|
// Sieve
|
||||||
|
$labels['form.sieve.caption'] = 'Sieve';
|
||||||
|
|
||||||
|
// Server host name
|
||||||
|
$labels['form.sieve.host'] = 'Ime oz. naslov Sieve strežnika';
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$labels['form.sieve.security'] = 'Varnost';
|
||||||
|
|
||||||
|
// Port
|
||||||
|
$labels['form.sieve.port'] = 'Vrata';
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
$labels['form.sieve.auth'] = 'Avtorizacija';
|
||||||
|
|
||||||
|
// As IMAP
|
||||||
|
$labels['form.sieve.auth.imap'] = 'Tako kot IMAP';
|
||||||
|
|
||||||
|
// None
|
||||||
|
$labels['form.sieve.auth.none'] = 'Brez';
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
$labels['form.sieve.auth.custom'] = 'Po meri';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
$labels['form.sieve.username'] = 'Uporabniško ime';
|
||||||
|
|
||||||
|
// Password
|
||||||
|
$labels['form.sieve.password'] = 'Geslo';
|
||||||
|
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
$labels['form.notify.caption'] = 'Obvestila';
|
||||||
|
|
||||||
|
// Check for new mail
|
||||||
|
$labels['form.notify.check'] = 'Preveri nova sporočila';
|
||||||
|
|
||||||
|
// Focus / favicon
|
||||||
|
$labels['form.notify.basic'] = 'Fokus / favicon';
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
$labels['form.notify.sound'] = 'Zvok';
|
||||||
|
|
||||||
|
// Desktop notification
|
||||||
|
$labels['form.notify.desktop'] = 'Namizno obvestilo';
|
||||||
|
|
||||||
|
// Use default
|
||||||
|
$labels['form.notify.default'] = 'privzeto';
|
||||||
|
|
||||||
|
// On
|
||||||
|
$labels['form.notify.on'] = 'Vključeno';
|
||||||
|
|
||||||
|
// Off
|
||||||
|
$labels['form.notify.off'] = 'Izključeno';
|
||||||
|
|
||||||
|
// Requires newmail_notifier plugin
|
||||||
|
$labels['form.notify.requires_newmail_notifier'] = 'Obvestila zahtevajo vtičnik newmail_notifier. Prosimo, omogočite ga ali se obrnite na skrbnika.';
|
||||||
|
|
||||||
|
|
||||||
|
// Security options
|
||||||
|
// None
|
||||||
|
$labels['form.security.none'] = 'Brez';
|
||||||
|
|
||||||
|
// STARTTLS
|
||||||
|
$labels['form.security.starttls'] = 'STARTTLS';
|
||||||
|
|
||||||
|
// SSL/TLS
|
||||||
|
$labels['form.security.ssl'] = 'SSL/TLS';
|
||||||
|
|
||||||
|
// None warning
|
||||||
|
$labels['form.security.none_warning'] = 'Nešifrirane povezave so nevarne. Vaše poverilnice in e-pošta so lahko prestreženi. Kadar koli je mogoče, uporabite SSL/TLS ali STARTTLS.';
|
||||||
|
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
|
||||||
|
// Value in \'Server host name\' field is too long (64 chars max).
|
||||||
|
$labels['err.host.long'] = 'Število znakov v polju \'Ime oz. naslov IMAP strežnika\' presega maksimalno število znakov (max 64 znakov).';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be a number.
|
||||||
|
$labels['err.port.num'] = 'Vrednost \'Vrata\' mora biti številka.';
|
||||||
|
|
||||||
|
// Value in \'Port\' field must be between 1 and 65535.
|
||||||
|
$labels['err.port.range'] = 'Vrednost v polju \'Vrata\' mora biti med 1 in 65535.';
|
||||||
|
|
||||||
|
// IMAP connection test failed.
|
||||||
|
$labels['err.imap.connect'] = 'Povezava IMAP ni uspela. Preverite naslov strežnika, vrata in poverilnice.';
|
||||||
|
|
||||||
|
// SMTP connection test failed.
|
||||||
|
$labels['err.smtp.connect'] = 'Povezava SMTP ni uspela. Preverite naslov strežnika, vrata in poverilnice.';
|
||||||
|
|
||||||
|
// Sieve connection test failed.
|
||||||
|
$labels['err.sieve.connect'] = 'Povezava Sieve ni uspela. Preverite naslov strežnika, vrata in poverilnice.';
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/***************************************************************************
|
|
||||||
* This file is part of Roundcube "identities_imap" plugin.
|
|
||||||
*
|
|
||||||
* Your are not allowed to distribute this file or parts of it.
|
|
||||||
*
|
|
||||||
* This file is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2012 - 2014 Roland 'Rosali' Liebl - all rights reserved
|
|
||||||
* dev-team [at] myroundcube [dot] com
|
|
||||||
* http://myroundcube.com
|
|
||||||
***************************************************************************/
|
|
||||||
|
|
||||||
#accounts{position:absolute; top:63px; left:18px;}.remotehint{z-index:-1; position:absolute; top:7px; margin-left:auto; left:210px; width:400px; border-radius:5px; padding:4px; -moz-border-radius:5px; border:1px solid #DEEEFC; background:#F2F9FF; text-align:center; color:#3F70BA; display:inline;}.classic{top:2px;}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/***************************************************************************
|
|
||||||
* This file is part of Roundcube "identities_imap" plugin.
|
|
||||||
*
|
|
||||||
* Your are not allowed to distribute this file or parts of it.
|
|
||||||
*
|
|
||||||
* This file is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2012 - 2014 Roland 'Rosali' Liebl - all rights reserved
|
|
||||||
* dev-team [at] myroundcube [dot] com
|
|
||||||
* http://myroundcube.com
|
|
||||||
***************************************************************************/
|
|
||||||
|
|
||||||
select.deco{height:18px; font-size:12px; border-radius:3px; -moz-border-radius:3px; -webkit-border-radius:3px; color:#fff; background:#444; border:1px solid #ababab;}.remotehint{z-index:1; position:absolute; top:1px; margin-left:auto; left:50%; margin-left:-200px; width:400px; border-radius:5px; padding:4px; -moz-border-radius:5px; border:1px solid #DEEEFC; background:#F2F9FF; text-align:center; color:#3F70BA; display:inline;}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/***************************************************************************
|
|
||||||
* This file is part of Roundcube "##PLUGIN##" plugin.
|
|
||||||
*
|
|
||||||
* Your are not allowed to distribute this file or parts of it.
|
|
||||||
*
|
|
||||||
* This file is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2012 - ##YEAR## Roland 'Rosali' Liebl - all rights reserved
|
|
||||||
* dev-team [at] myroundcube [dot] com
|
|
||||||
* http://myroundcube.com
|
|
||||||
***************************************************************************/
|
|
||||||
|
|
||||||
select.deco{}#taskbar .remotehint{position:absolute; top:52px; left:50%; margin-left:-200px; width:400px; padding:0px; border:1px solid #DEEEFC; background-color:#F2F9FF; text-align:center; font-size:10px; color:#3F70BA;}
|
|
||||||
Reference in New Issue
Block a user