Compare commits
158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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@v4
|
||||
|
||||
- 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@v4
|
||||
|
||||
- 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
|
||||
*.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,6 +1,211 @@
|
||||
# ident_switch
|
||||
ident_switch plugin for Roundcube
|
||||
# Ultimate Identity Switcher for Roundcube
|
||||
|
||||
This plugin allows users to switch between different accounts (including remote) in single Roundcube session.
|
||||
[](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)
|
||||
|
||||
*Inspired by identities_imap plugin that is no longer supported.*
|
||||
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 🇳🇨
|
||||
|
||||
88
SQL/mysql.initial.sql
Normal file
88
SQL/mysql.initial.sql
Normal file
@@ -0,0 +1,88 @@
|
||||
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`)
|
||||
);
|
||||
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);
|
||||
87
SQL/postgres.initial.sql
Normal file
87
SQL/postgres.initial.sql
Normal file
@@ -0,0 +1,87 @@
|
||||
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,
|
||||
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);
|
||||
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);
|
||||
86
SQL/sqlite.initial.sql
Normal file
86
SQL/sqlite.initial.sql
Normal file
@@ -0,0 +1,86 @@
|
||||
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);
|
||||
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,37 +1,67 @@
|
||||
{
|
||||
"name": "boressoft/ident_switch",
|
||||
"name": "gecka/ident-switch",
|
||||
"type": "roundcube-plugin",
|
||||
"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"],
|
||||
"license": "GPL-3.0+",
|
||||
"license": "AGPL-3.0+",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Boris Gulay",
|
||||
"email": "boris@boressoft.ru",
|
||||
"role": "Developer"
|
||||
"email": "boris@gulay.name",
|
||||
"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": [
|
||||
{
|
||||
"type": "composer",
|
||||
"url": "http://plugins.roundcube.net"
|
||||
"url": "https://plugins.roundcube.net"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.0",
|
||||
"roundcube/plugin-installer": ">=0.1.3"
|
||||
"php": ">=8.2",
|
||||
"roundcube/plugin-installer": ">=0.3.0",
|
||||
"ext-ctype": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"squizlabs/php_codesniffer": "^3.0"
|
||||
},
|
||||
"conflict": {
|
||||
"elm/identity_smtp": "*"
|
||||
"boressoft/ident_switch": "*",
|
||||
"elm/identity_smtp": "*",
|
||||
"toteph42/identity_switch": "*"
|
||||
},
|
||||
"support": {
|
||||
"issues": "https://bitbucket.org/BoresExpress/ident_switch/issues"
|
||||
"issues": "https://github.com/Gecka-Apps/roundcube-ident_switch/issues"
|
||||
},
|
||||
"extra": {
|
||||
"roundcube": {
|
||||
"min-version": "1.1",
|
||||
"min-version": "1.6",
|
||||
"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": "46b2d3558cb304fde0539d1b85971382",
|
||||
"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.6.0"
|
||||
}
|
||||
@@ -1,2 +1,101 @@
|
||||
<?php
|
||||
/**
|
||||
* ident_switch - Configuration file.
|
||||
*
|
||||
* Copyright (C) 2016 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
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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'] = [
|
||||
|
||||
// Domain part of email address
|
||||
'domain.tld' => [
|
||||
|
||||
// IMAP connection: scheme://host:port
|
||||
// Schemes: ssl:// for IMAPS (port 993), tls:// for STARTTLS (port 143).
|
||||
// Required.
|
||||
'imap_host' => 'ssl://mail.domain.tld:993',
|
||||
|
||||
// SMTP connection: scheme://host:port
|
||||
// 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;
|
||||
|
||||
324
ident_switch-form.js
Normal file
324
ident_switch-form.js
Normal file
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* 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 }
|
||||
};
|
||||
|
||||
$(function() {
|
||||
// Apply initial mode visibility
|
||||
var initialMode = $("SELECT[name='_ident_switch.form.common.mode']").val() || 'primary';
|
||||
plugin_switchIdent_mode_onChange(initialMode);
|
||||
plugin_switchIdent_processPreconfig();
|
||||
|
||||
// Bind security change handlers
|
||||
$.each(['imap', 'smtp', 'sieve'], function(i, proto) {
|
||||
var secSel = "SELECT[name='_ident_switch.form." + proto + ".security']";
|
||||
$(secSel).on('change', function() {
|
||||
plugin_switchIdent_onSecurityChange(proto, $(this).val());
|
||||
});
|
||||
});
|
||||
|
||||
// Bind blur handlers for smart placeholder clearing
|
||||
$.each(['imap', 'smtp', 'sieve'], function(i, proto) {
|
||||
var portFld = $("INPUT[name='_ident_switch.form." + proto + ".port']");
|
||||
portFld.on('blur', function() {
|
||||
plugin_switchIdent_clearIfDefault($(this));
|
||||
});
|
||||
var hostFld = $("INPUT[name='_ident_switch.form." + proto + ".host']");
|
||||
hostFld.on('blur', function() {
|
||||
plugin_switchIdent_clearIfDefault($(this));
|
||||
});
|
||||
});
|
||||
|
||||
// IMAP host → update SMTP/Sieve host placeholders
|
||||
$("INPUT[name='_ident_switch.form.imap.host']").on('change blur', 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);
|
||||
});
|
||||
|
||||
// Bind auth change handlers for SMTP and Sieve custom credentials
|
||||
$.each(['smtp', 'sieve'], function(i, proto) {
|
||||
var authSel = "SELECT[name='_ident_switch.form." + proto + ".auth']";
|
||||
$(authSel).on('change', function() {
|
||||
plugin_switchIdent_onAuthChange(proto, $(this).val());
|
||||
});
|
||||
// Apply initial visibility
|
||||
plugin_switchIdent_onAuthChange(proto, $(authSel).val());
|
||||
});
|
||||
|
||||
// Delimiter mode handler
|
||||
$("SELECT[name='_ident_switch.form.imap.delimiter_mode']").on('change', 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
|
||||
$("INPUT[name='_email']").on('change blur', function() {
|
||||
plugin_switchIdent_onEmailChange($(this).val());
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (disVal > 0) {
|
||||
$("INPUT[name='_ident_switch.form.imap.host']").prop("disabled", true);
|
||||
$("SELECT[name='_ident_switch.form.imap.security']").prop("disabled", true);
|
||||
$("INPUT[name='_ident_switch.form.imap.port']").prop("disabled", true);
|
||||
|
||||
$("INPUT[name='_ident_switch.form.smtp.host']").prop("disabled", true);
|
||||
$("SELECT[name='_ident_switch.form.smtp.security']").prop("disabled", true);
|
||||
$("INPUT[name='_ident_switch.form.smtp.port']").prop("disabled", true);
|
||||
$("SELECT[name='_ident_switch.form.smtp.auth']").prop("disabled", true);
|
||||
$("INPUT[name='_ident_switch.form.smtp.username']").prop("disabled", true);
|
||||
$("INPUT[name='_ident_switch.form.smtp.password']").prop("disabled", true);
|
||||
|
||||
$("SELECT[name='_ident_switch.form.imap.delimiter_mode']").prop("disabled", true);
|
||||
$("INPUT[name='_ident_switch.form.imap.delimiter']").prop("disabled", true);
|
||||
|
||||
$("INPUT[name='_ident_switch.form.sieve.host']").prop("disabled", true);
|
||||
$("SELECT[name='_ident_switch.form.sieve.security']").prop("disabled", true);
|
||||
$("INPUT[name='_ident_switch.form.sieve.port']").prop("disabled", true);
|
||||
$("SELECT[name='_ident_switch.form.sieve.auth']").prop("disabled", true);
|
||||
$("INPUT[name='_ident_switch.form.sieve.username']").prop("disabled", true);
|
||||
$("INPUT[name='_ident_switch.form.sieve.password']").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;
|
||||
|
||||
// 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();
|
||||
}
|
||||
311
ident_switch-switch.js
Normal file
311
ident_switch-switch.js
Normal file
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* ident_switch - Account switcher UI and new mail notifications.
|
||||
*
|
||||
* Places the hidden <select> from the footer into the appropriate
|
||||
* skin location (Larry, Classic, Elastic), 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;
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
function plugin_switchIdent_addCbLarry($wrapper, $sw) {
|
||||
var $topRight = $('#topline .topright');
|
||||
if (!$topRight.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sw.css({
|
||||
'font-weight': 'bold',
|
||||
'max-width': '200px',
|
||||
'text-overflow': 'ellipsis'
|
||||
});
|
||||
|
||||
$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': 'transparent',
|
||||
'border': 'none',
|
||||
'font-weight': 'bold',
|
||||
'color': 'inherit',
|
||||
'box-shadow': 'none',
|
||||
'max-width': '200px',
|
||||
'text-overflow': 'ellipsis',
|
||||
'padding': '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();
|
||||
});
|
||||
32
ident_switch.css
Normal file
32
ident_switch.css
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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%;
|
||||
}
|
||||
|
||||
/* 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,20 +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 });
|
||||
}
|
||||
|
||||
function plugin_switchIdent_fixIdent(iid) {
|
||||
if (parseInt(iid) > 0)
|
||||
$("#_from").val(iid);
|
||||
}
|
||||
770
ident_switch.php
770
ident_switch.php
@@ -1,325 +1,529 @@
|
||||
<?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
|
||||
* @author Boris Gulay
|
||||
* @url
|
||||
* Original code licensed under GPL-3.0+.
|
||||
* New contributions licensed under AGPL-3.0+.
|
||||
*
|
||||
* @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
|
||||
{
|
||||
public $task='?(?!login|logout).*';
|
||||
/** @var string Task regex: active on all tasks except login/logout. */
|
||||
public $task = '?(?!login|logout).*';
|
||||
|
||||
private $table = 'ident_switch';
|
||||
private $my_postfix = '_iswitch';
|
||||
/** @var string Database table name for this plugin. */
|
||||
public const TABLE = 'ident_switch';
|
||||
|
||||
// Flags user in database
|
||||
private $db_enabled = 1;
|
||||
private $db_secure = 2;
|
||||
/** @var string Session variable suffix used to store/restore state. */
|
||||
public const MY_POSTFIX = '_iswitch';
|
||||
|
||||
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->add_hook('render_page', array($this, 'on_render_page'));
|
||||
$this->add_hook('smtp_connect', array($this, 'on_smtp_connect'));
|
||||
$this->add_hook('identity_form', array($this, 'on_identity_form'));
|
||||
$this->add_hook('identity_update', array($this, 'on_identity_update'));
|
||||
$this->add_hook('identity_delete', array($this, 'on_identity_delete'));
|
||||
$this->add_hook('template_object_composeheaders', array($this, 'on_template_object_composeheaders'));
|
||||
$this->form = new IdentSwitchForm($this);
|
||||
$this->switcher = new IdentSwitchSwitcher();
|
||||
$this->preconfig = new IdentSwitchPreconfig($this);
|
||||
$this->checker = new IdentSwitchChecker();
|
||||
|
||||
$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();
|
||||
error_log('Task: ' . $args['task'] . ', action: ' . $args['action']);
|
||||
|
||||
if (strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 1)
|
||||
{ // We are impersonating
|
||||
if (strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0) {
|
||||
// We are impersonating
|
||||
$rc->config->set('imap_cache', null);
|
||||
$rc->config->set('messages_cache', false);
|
||||
|
||||
if ($args['task'] == 'mail')
|
||||
{
|
||||
if ($args['task'] === 'mail') {
|
||||
$this->add_texts('localization/');
|
||||
$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;
|
||||
}
|
||||
|
||||
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();
|
||||
error_log('Template: ' . $args['template']);
|
||||
|
||||
match ($rc->task) {
|
||||
'mail' => $this->render_switch($rc, $args),
|
||||
'settings' => $this->include_script('ident_switch-form.js'),
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($rc->task === 'mail') {
|
||||
$this->include_stylesheet('ident_switch.css');
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
$iid = 0;
|
||||
if (is_int($iid_s)) {
|
||||
$iid = $iid_s;
|
||||
} elseif ($iid_s === '-1') {
|
||||
$iid = -1;
|
||||
} elseif (is_string($iid_s) && ctype_digit($iid_s)) {
|
||||
$iid = intval($iid_s);
|
||||
}
|
||||
|
||||
$accNames = [$_SESSION['global_alias'] ?? $rc->user->data['username']];
|
||||
$accValues = [-1];
|
||||
$accSelected = -1;
|
||||
$iidMap = [0 => -1]; // primary account: iid 0 → select value -1
|
||||
|
||||
// Get list of alternative accounts
|
||||
$sOpt = '';
|
||||
$sql = 'SELECT id, label, username FROM ' . $rc->db->table_name($this->table) . ' WHERE user_id = ? AND flags & ? > 0';
|
||||
$q = $rc->db->query($sql, $rc->user->data['user_id'], $this->db_enabled);
|
||||
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(
|
||||
'option',
|
||||
$opts,
|
||||
$r['label']
|
||||
);
|
||||
$sql = "SELECT "
|
||||
. "isw.id, isw.iid, isw.label, isw.username, ii.email"
|
||||
. " 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'];
|
||||
}
|
||||
|
||||
// Render UI if user has extra accounts
|
||||
if (!empty($sOpt))
|
||||
{
|
||||
|
||||
// Add main account
|
||||
$opts = array('value' => -1);
|
||||
if (strcasecmp($_SESSION['username'], $rc->user->data['username']) === 0)
|
||||
$opts['selected'] = 'selected';
|
||||
|
||||
$sOpt = html::tag(
|
||||
'option',
|
||||
$opts,
|
||||
$_SESSION['global_alias'] ? $_SESSION['global_alias'] : $rc->user->data['username']
|
||||
) . $sOpt;
|
||||
|
||||
$this->include_script('ident_switch.js');
|
||||
$sw = html::tag(
|
||||
'select',
|
||||
array(
|
||||
'id' => 'plugin-ident_switch-account',
|
||||
'style' => 'display: none;',
|
||||
'onchange' => 'plugin_switchIdent_switch(this.value);',
|
||||
),
|
||||
$sOpt
|
||||
);
|
||||
$rc->output->add_footer($sw);
|
||||
$lbl = $r['label'] ?: $r['username'] ?: $r['email'];
|
||||
$accNames[] = $lbl;
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
function on_smtp_connect($args)
|
||||
{
|
||||
$rc = rcmail::get_instance();
|
||||
|
||||
// TODO: Rewrite with full settings!
|
||||
|
||||
if (strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 1)
|
||||
{
|
||||
if ($args['smtp_user'] == '%u')
|
||||
$args['smtp_user'] = $rc->user->data['username'];
|
||||
if ($args['smtp_pass'] == '%p')
|
||||
$args['smtp_pass'] = $rc->decrypt($_SESSION['password' . $this->my_postfix]);
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
function on_identity_form($args)
|
||||
{
|
||||
$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 = ?';
|
||||
$q = $rc->db->query($sql, $args['id'], $rc->user->ID);
|
||||
|
||||
if ($rc->db->affected_rows($q))
|
||||
$rc->write_log($this->my_log, 'Deleted associated information for identity with ID = ' . $args['id'] . '.');
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
function on_template_object_composeheaders($args)
|
||||
{
|
||||
if ($args['id'] == '_from')
|
||||
{
|
||||
$rc = rcmail::get_instance();
|
||||
if (strcasecmp($_SESSION['username'], $rc->user->data['username']) !== 0)
|
||||
{
|
||||
if (isset($_SESSION['iid' . $this->my_postfix]))
|
||||
$rc->output->add_script('plugin_switchIdent_fixIdent(' . $_SESSION['iid' . $this->my_postfix] . ');', 'docready');
|
||||
else
|
||||
$rc->write_log($this->my_log, 'Special session variable with active identity ID not found.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function on_switch()
|
||||
{
|
||||
$rc = rcmail::get_instance();
|
||||
|
||||
$my_postfix_len = strlen($this->my_postfix);
|
||||
$identId = rcube_utils::get_input_value('_ident-id', RCUBE_INPUT_POST);
|
||||
|
||||
if (-1 == $identId)
|
||||
{ // Switch to main account
|
||||
$rc->write_log($this->my_log, 'Switching mailbox back to default.');
|
||||
|
||||
foreach ($_SESSION as $k => $v)
|
||||
{
|
||||
if (strncasecmp($k, 'storage', 7) === 0 && substr_compare($k, $this->my_postfix, -$my_postfix_len, $my_postfix_len) === 0)
|
||||
{
|
||||
$_SESSION[$k] = $_SESSION[$k . $this->my_postfix];
|
||||
$rc->session->remove($k . $this->my_postfix);
|
||||
}
|
||||
}
|
||||
$_SESSION['username'] = $rc->user->data['username'];
|
||||
$_SESSION['password'] = $_SESSION['password' . $this->my_postfix];
|
||||
$_SESSION['iid' . $this->my_postfix] = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
$sql = 'SELECT host, flags, port, username, password, iid 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))
|
||||
{
|
||||
$rc->write_log(
|
||||
$this->my_log,
|
||||
'Switching mailbox to one for identity with ID = ' . $r['iid'] . ' (username = \'' . $r['username'] . '\').'
|
||||
);
|
||||
|
||||
$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, $this->my_postfix, -$my_postfix_len, $my_postfix_len) !== 0)
|
||||
{
|
||||
$_SESSION[$k . $this->my_postfix] = $_SESSION[$k];
|
||||
$rc->session->remove($k);
|
||||
}
|
||||
}
|
||||
}
|
||||
$_SESSION['password' . $this->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'];
|
||||
$_SESSION['iid' . $this->my_postfix] = $r['iid'];
|
||||
|
||||
$rc->session->remove('folders');
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Show message in browser
|
||||
$rc->write_log($this->my_log, 'Requested remote mailbox with ID = ' . $identId . ' not found.');
|
||||
if (count($accValues) <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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',
|
||||
'style' => 'display: none; padding: 0;',
|
||||
'onchange' => 'plugin_switchIdent_switch(this.value);',
|
||||
]);
|
||||
$select->add($accNames, $accValues);
|
||||
|
||||
$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>';
|
||||
|
||||
$rc->output->add_footer($html);
|
||||
|
||||
if (!$rc->config->get('ident_switch.check_mail', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rc->output->redirect(
|
||||
array(
|
||||
'_task' => 'mail',
|
||||
'_mbox' => rcube_utils::get_input_value('_mbox', RCUBE_INPUT_GET),
|
||||
)
|
||||
);
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle refresh hook: check new mail on secondary identities.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
264
lib/IdentSwitchChecker.php
Normal file
264
lib/IdentSwitchChecker.php
Normal file
@@ -0,0 +1,264 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
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'] ?? $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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
1330
lib/IdentSwitchForm.php
Normal file
1330
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();
|
||||
|
||||
// IMAP
|
||||
$labels['form.caption'] = 'IMAP';
|
||||
// Separate Account
|
||||
$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
|
||||
$labels['form.enabled'] = 'Enabled';
|
||||
// General
|
||||
$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
|
||||
$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
|
||||
$labels['form.server']='Server URI';
|
||||
// Value in \'Label\' field is too long (32 chars max).
|
||||
$labels['err.label.long'] = 'Value in \'Label\' field is too long (32 chars max).';
|
||||
|
||||
// Username
|
||||
$labels['form.username'] = 'Username';
|
||||
|
||||
// Passwotrd
|
||||
$labels['form.password'] = 'Password';
|
||||
// IMAP
|
||||
$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
|
||||
$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